mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 20:32:20 +03:00
* chore(release): open v3.8.13 development cycle Bump 3.8.12 → 3.8.13 across package.json, lockfile, electron/, open-sse/, and docs/reference/openapi.yaml; add the [3.8.13] cycle placeholder to the root CHANGELOG and the 41 i18n mirrors. Integration branch for the v3.8.13 cycle — fixes/features land here via per-issue PRs and it merges to main at release time. * fix(ci): skip auto-deploy when VPS host is unreachable from the runner (#3299) Integrated into release/v3.8.13 * fix(dev): auto-rebuild better-sqlite3 on Node ABI mismatch at dev startup (#3301) Integrated into release/v3.8.13 * feat(api): accept path-scoped API keys on client API routes (#3300) Integrated into release/v3.8.13 * fix(sse): harden against empty responses causing Copilot Chat failures (#3297) Integrated into release/v3.8.13 * fix(api): remove Completions.me rickroll provider (discussion #3293) (#3302) Integrated into release/v3.8.13 * fix(opencode-provider): extract contextLength from live model catalog (#3298) Integrated into release/v3.8.13 * feat(web-cookie): self-service login infrastructure + auto-refresh daemon (#3292) Integrated into release/v3.8.13 * docs(changelog): record the v3.8.13 PRs merged this round (#3292/#3300/#3297/#3298/#3301/#3302/#3299) * fix(auth): harden URL token extraction — drop query-string fallback, gate to client routes (security follow-up to #3300) (#3309) Security follow-up to #3300 — integrated into release/v3.8.13 * docs: rename resolve-issues → review-issues skill references * fix(dashboard): keep no-auth providers visible under 'Show configured only' (#3290) (#3312) no-auth providers (opencode, duckduckgo-web, theoldllm, veoaifree-web) never create a DB connection row so stats.total stays 0, which the configured-only filter treated as 'unconfigured' and hid them — even though they are always usable and appear unconditionally in /v1/models. filterConfiguredProviderEntries now treats displayAuthType === 'no-auth' as configured. Co-authored-by: uniQta <uniQta@users.noreply.github.com> * fix(cli): resolve update paths relative to script + recursive backup (#3295) (#3313) omniroute update always failed on a global install: - getCurrentVersion() read package.json from process.cwd(), which on a global npm/brew install is the user's working dir, not the package root → null → 'Could not determine current version'. - createBackup() resolved bin/ from cwd too, and passed the 'cli' directory to copyFileSync → EISDIR, swallowed by the catch → 'Failed to create backup'. Both now resolve package.json/bin relative to the script via import.meta.url, and the backup uses cpSync({recursive:true}) so the cli/ directory is copied. Co-authored-by: uniQta <uniQta@users.noreply.github.com> * fix(theoldllm): read upstream body once to avoid [502] body-already-read (#3296) (#3314) On the cached-token path the executor never enters the refresh branch, so the same upstream Response was read with .text() twice (token-rejection check + final body). A Response body is single-use, so the second read threw 'Body is unusable: Body has already been read', caught and surfaced as [502]. Read the body once into finalBody and only re-read after a token-rejection refetch. Co-authored-by: onizukashonan14-png <onizukashonan14-png@users.noreply.github.com> * fix(sse): strip leaked internal tool envelopes from streaming output (#3311) Integrated into release/v3.8.13 * fix(sse): expose Claude + Gemini budget tiers in the antigravity catalog (#3184) (#3303) Integrated into release/v3.8.13 (#3184) * fix(catalog): compute combo context_length from known targets only (#3304) Integrated into release/v3.8.13 — live contextLength + known-targets combo context (#3298 follow-up) * chore(i18n): add message keys for proxy UI + vscode/ollama endpoint (#3307) Integrated into release/v3.8.13 — i18n message keys for proxy UI + vscode/ollama * feat(dashboard): i18n the proxy settings UI (#3310) Integrated into release/v3.8.13 — i18n the proxy settings UI * feat(api): model catalog enrichment + MCP model-catalog tools (#3306) Integrated into release/v3.8.13 — model catalog enrichment + MCP model-catalog tools, reconciled with #3309 URL-token hardening * test(catalog): align Antigravity preview-alias test with #3303 budget tiers #3303 added the Gemini `-high`/`-low` budget tiers to ANTIGRAVITY_PUBLIC_MODELS (user-callable on the Antigravity OAuth backend, verified via #3184), but did not update the catalog-route test that asserted `antigravity/gemini-3.1-pro-high` must NOT be exposed. The assertion now reflects the intended behavior — the client-visible budget alias IS surfaced — while keeping the legacy `gemini-claude-*` alias keys unexposed. Caught running the full catalog suite on the merged release HEAD (the #3303 round only ran the antigravity-aliases and usage-hardening files). * docs(changelog): record the 6 PRs merged this review round into v3.8.13 #3306/#3307/#3310 (New Features — VS Code split: catalog+MCP, i18n keys, proxy UI i18n), #3311/#3303/#3304 (Bug Fixes — SSE envelope sanitizer, antigravity budget tiers, combo known-targets context_length). * chore(release): finalize v3.8.13 changelog and cleanup Finalize the v3.8.13 changelog with release date, maintenance notes, and contributor credits. Update MCP docs to reference the correct tool inventory diagram, exclude nested .claude worktrees from ESLint scans, and tighten a response sanitizer type guard. * fix(dashboard): refresh connections after provider auth import (#3320) Integrated into release/v3.8.13 — refresh connections after provider auth import * fix(codex): strip client-only params on native /responses passthrough (#3317) (#3325) A /v1/responses request against the built-in codex/ provider does an openai-responses -> openai-responses passthrough (CodexExecutor.transformRequest returns the body early for _nativeCodexPassthrough). It forwarded client-only fields verbatim and the Codex upstream rejected them with 400 Unsupported parameter: prompt_cache_retention / safety_identifier / user — breaking Factory Droid (which injects all three). The chat-completions path already strips these (base.ts #1884, openai-responses translator #2770) but the passthrough skips translation. Strip the three fields in the shared block before the passthrough return; user is removed unconditionally since Codex /responses always rejects it. Co-authored-by: tycronk20 <tycronk20@users.noreply.github.com> * fix(dashboard): normalize agent-bridge /state response to stop page crash (#3318) (#3326) The Agent Bridge page seeded a well-shaped initialData default then replaced it wholesale with the raw /api/tools/agent-bridge/state response. The route returns { server, agents } but the UI reads { serverState, agentStates, bypassPatterns, mappings }, so serverState became undefined and AgentBridgeServerCard crashed on serverState.running — surfaced as the full-page 'Internal Server Error' boundary (client render error, not a real 5xx). Add a shared normalizeAgentBridgeState() that maps the route shape into the page contract (server.running/certExists -> serverState) and always returns safe defaults (never undefined serverState). Wired into both the SSR loader (page.tsx) and the polling hook. The legacy 'agents' entry shape differs from AgentStateEntry so it is not coerced; full route<->page contract reconciliation (port, upstreamCa, bypassPatterns, mappings, agentStates) is a follow-up. Co-authored-by: tycronk20 <tycronk20@users.noreply.github.com> * docs: VS Code/Ollama endpoints + env & i18n tooling (#3319) Integrated into release/v3.8.13 — VS Code/Ollama docs + env & i18n tooling * feat(provider): test-all endpoint, rate-limit overrides, visibility f… (#3267) Integrated into release/v3.8.13 — provider test-all endpoint, rate-limit overrides, model visibility * feat: auto-combo optimization, playground model dropdown, only-configured toggle (#3322) Integrated into release/v3.8.13 — auto-combo candidate expansion + playground dropdown + only-configured toggle * feat(api): VS Code Copilot Ollama-compatible BYOK endpoint (#3316) Integrated into release/v3.8.13 — VS Code Copilot Ollama-compatible BYOK endpoint (reconciled with #3306/#3309 auth hardening) * chore(release): document #3320 in the v3.8.13 changelog + contributor credits --------- Co-authored-by: Felipe Almeman <4226997+zhiru@users.noreply.github.com> Co-authored-by: Wilson <pedbookmed@gmail.com> Co-authored-by: Hernan Javier Ardila Sanchez <hjasgr@gmail.com> Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com> Co-authored-by: uniQta <uniQta@users.noreply.github.com> Co-authored-by: onizukashonan14-png <onizukashonan14-png@users.noreply.github.com> Co-authored-by: tycronk20 <tycronk20@users.noreply.github.com> Co-authored-by: Vinayrnani <vinayrnani@gmail.com>
1173 lines
40 KiB
TypeScript
1173 lines
40 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
applyConfiguredUserAgent,
|
|
BaseExecutor,
|
|
getCustomUserAgent,
|
|
mergeAbortSignals,
|
|
mergeUpstreamExtraHeaders,
|
|
setUserAgentHeader,
|
|
} from "../../open-sse/executors/base.ts";
|
|
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
|
|
import { PROVIDERS } from "../../open-sse/config/constants.ts";
|
|
import {
|
|
CLAUDE_CODE_COMPATIBLE_ANTHROPIC_VERSION,
|
|
CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH,
|
|
CONTEXT_1M_BETA_HEADER,
|
|
} from "../../open-sse/services/claudeCodeCompatible.ts";
|
|
|
|
class TestExecutor extends BaseExecutor {
|
|
constructor(config = {}) {
|
|
super("test-provider", {
|
|
baseUrls: [
|
|
"https://primary.example/v1/chat/completions",
|
|
"https://fallback.example/v1/chat/completions",
|
|
],
|
|
headers: { "X-Test-Header": "base" },
|
|
...config,
|
|
});
|
|
}
|
|
|
|
async transformRequest(model, body, stream) {
|
|
return { ...body, transformed: true, model, stream };
|
|
}
|
|
}
|
|
|
|
test("BaseExecutor: openai-compatible buildUrl sanitizes custom chat paths", () => {
|
|
const executor = new BaseExecutor("openai-compatible-test", {});
|
|
const valid = executor.buildUrl("gpt-4.1", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl: "https://proxy.example/v1/",
|
|
chatPath: "/custom/chat/completions",
|
|
},
|
|
});
|
|
const invalid = executor.buildUrl("gpt-4.1", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl: "https://proxy.example/v1/",
|
|
chatPath: "../evil",
|
|
},
|
|
});
|
|
const invalidNullByte = executor.buildUrl("gpt-4.1", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl: "https://proxy.example/v1/",
|
|
chatPath: "/ok\0evil",
|
|
},
|
|
});
|
|
|
|
assert.equal(valid, "https://proxy.example/v1/custom/chat/completions");
|
|
assert.equal(invalid, "https://proxy.example/v1/chat/completions");
|
|
assert.equal(invalidNullByte, "https://proxy.example/v1/chat/completions");
|
|
});
|
|
|
|
test("BaseExecutor: legacy openai-compatible providers honor providerSpecificData.apiType", () => {
|
|
const executor = new BaseExecutor("openai-compatible-sp-openai", {});
|
|
const url = executor.buildUrl("gpt-5.4", true, 0, {
|
|
providerSpecificData: {
|
|
apiType: "responses",
|
|
baseUrl: "https://proxy.example/v1/",
|
|
},
|
|
});
|
|
|
|
assert.equal(url, "https://proxy.example/v1/responses");
|
|
});
|
|
|
|
test("DefaultExecutor.buildUrl handles Gemini, Claude and Qwen variants", () => {
|
|
const gemini = new DefaultExecutor("gemini");
|
|
const claude = new DefaultExecutor("claude");
|
|
const qwen = new DefaultExecutor("qwen");
|
|
|
|
assert.equal(
|
|
gemini.buildUrl("gemini-2.5-flash", false),
|
|
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent"
|
|
);
|
|
assert.equal(
|
|
gemini.buildUrl("gemini-2.5-flash", true),
|
|
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse"
|
|
);
|
|
assert.equal(claude.buildUrl("claude-sonnet-4", true), `${PROVIDERS.claude.baseUrl}?beta=true`);
|
|
assert.equal(qwen.buildUrl("qwen3-coder", true), "https://portal.qwen.ai/v1/chat/completions");
|
|
assert.equal(
|
|
qwen.buildUrl("qwen3-coder", true, 0, {
|
|
providerSpecificData: { resourceUrl: "custom.qwen.ai" },
|
|
}),
|
|
"https://custom.qwen.ai/v1/chat/completions"
|
|
);
|
|
});
|
|
|
|
test("DefaultExecutor.buildUrl uses full chat endpoints for hosted OpenAI-compatible providers", () => {
|
|
const bazaarlink = new DefaultExecutor("bazaarlink");
|
|
const crof = new DefaultExecutor("crof");
|
|
|
|
assert.equal(
|
|
bazaarlink.buildUrl("auto:free", true),
|
|
"https://bazaarlink.ai/api/v1/chat/completions"
|
|
);
|
|
assert.equal(
|
|
crof.buildUrl("gpt-4.1", true),
|
|
"https://crof.ai/v1/chat/completions"
|
|
);
|
|
});
|
|
|
|
test("DefaultExecutor.buildUrl handles openai-compatible and anthropic-compatible providers", () => {
|
|
const openAICompat = new DefaultExecutor("openai-compatible-test");
|
|
const openAIResponsesCompat = new DefaultExecutor("openai-compatible-responses-test");
|
|
const openAILegacyResponsesCompat = new DefaultExecutor("openai-compatible-sp-openai");
|
|
const anthropicCompat = new DefaultExecutor("anthropic-compatible-test");
|
|
const anthropicCcCompat = new DefaultExecutor("anthropic-compatible-cc-test");
|
|
|
|
assert.equal(
|
|
openAICompat.buildUrl("gpt-4.1", true, 0, {
|
|
providerSpecificData: { baseUrl: "https://proxy.example/v1/" },
|
|
}),
|
|
"https://proxy.example/v1/chat/completions"
|
|
);
|
|
assert.equal(
|
|
openAICompat.buildUrl("gpt-4.1", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl: "https://proxy.example/v1/",
|
|
chatPath: "/custom/chat",
|
|
},
|
|
}),
|
|
"https://proxy.example/v1/custom/chat"
|
|
);
|
|
assert.equal(
|
|
openAIResponsesCompat.buildUrl("gpt-4.1", true, 0, {
|
|
providerSpecificData: { baseUrl: "https://proxy.example/v1/" },
|
|
}),
|
|
"https://proxy.example/v1/responses"
|
|
);
|
|
assert.equal(
|
|
openAILegacyResponsesCompat.buildUrl("gpt-5.4", true, 0, {
|
|
providerSpecificData: {
|
|
apiType: "responses",
|
|
baseUrl: "https://proxy.example/v1/",
|
|
},
|
|
}),
|
|
"https://proxy.example/v1/responses"
|
|
);
|
|
assert.equal(
|
|
anthropicCompat.buildUrl("claude-sonnet-4", true, 0, {
|
|
providerSpecificData: { baseUrl: "https://anthropic.example/v1/" },
|
|
}),
|
|
"https://anthropic.example/v1/messages"
|
|
);
|
|
assert.equal(
|
|
anthropicCompat.buildUrl("claude-sonnet-4", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl: "https://anthropic.example/v1/",
|
|
chatPath: "/custom/messages",
|
|
},
|
|
}),
|
|
"https://anthropic.example/v1/custom/messages"
|
|
);
|
|
assert.equal(
|
|
anthropicCcCompat.buildUrl("claude-sonnet-4", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl: "https://cc.example/v1/messages",
|
|
},
|
|
}),
|
|
`https://cc.example${CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH}`
|
|
);
|
|
});
|
|
|
|
test("DefaultExecutor.buildUrl normalizes configurable chat-openai-compat base URLs", () => {
|
|
const bailian = new DefaultExecutor("bailian-coding-plan");
|
|
const heroku = new DefaultExecutor("heroku");
|
|
const databricks = new DefaultExecutor("databricks");
|
|
const azureAi = new DefaultExecutor("azure-ai");
|
|
const watsonx = new DefaultExecutor("watsonx");
|
|
const oci = new DefaultExecutor("oci");
|
|
const sap = new DefaultExecutor("sap");
|
|
const modal = new DefaultExecutor("modal");
|
|
const reka = new DefaultExecutor("reka");
|
|
const maritalk = new DefaultExecutor("maritalk");
|
|
const snowflake = new DefaultExecutor("snowflake");
|
|
const gigachat = new DefaultExecutor("gigachat");
|
|
const siliconflow = new DefaultExecutor("siliconflow");
|
|
|
|
assert.equal(
|
|
bailian.buildUrl("qwen3-coder-plus", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1",
|
|
},
|
|
}),
|
|
"https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1/messages"
|
|
);
|
|
assert.equal(
|
|
heroku.buildUrl("claude-4-sonnet", true, 0, {
|
|
providerSpecificData: { baseUrl: "https://us.inference.heroku.com" },
|
|
}),
|
|
"https://us.inference.heroku.com/v1/chat/completions"
|
|
);
|
|
assert.equal(
|
|
databricks.buildUrl("databricks-gpt-5", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl: "https://adb-1234567890123456.7.azuredatabricks.net/serving-endpoints",
|
|
},
|
|
}),
|
|
"https://adb-1234567890123456.7.azuredatabricks.net/serving-endpoints/chat/completions"
|
|
);
|
|
|
|
assert.equal(
|
|
azureAi.buildUrl("DeepSeek-V3.1", true, 0, {
|
|
providerSpecificData: { baseUrl: "https://my-foundry.services.ai.azure.com" },
|
|
}),
|
|
"https://my-foundry.services.ai.azure.com/openai/v1/chat/completions"
|
|
);
|
|
|
|
assert.equal(
|
|
watsonx.buildUrl("ibm/granite-3-3-8b-instruct", true, 0, {
|
|
providerSpecificData: { baseUrl: "https://ca-tor.ml.cloud.ibm.com" },
|
|
}),
|
|
"https://ca-tor.ml.cloud.ibm.com/ml/gateway/v1/chat/completions"
|
|
);
|
|
assert.equal(
|
|
oci.buildUrl("openai.gpt-oss-20b", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl: "https://inference.generativeai.us-ashburn-1.oci.oraclecloud.com",
|
|
},
|
|
}),
|
|
"https://inference.generativeai.us-ashburn-1.oci.oraclecloud.com/openai/v1/chat/completions"
|
|
);
|
|
assert.equal(
|
|
sap.buildUrl("gpt-4o", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl: "https://sap.example.com/v2/lm/deployments/demo-deployment",
|
|
},
|
|
}),
|
|
"https://sap.example.com/v2/lm/deployments/demo-deployment/chat/completions"
|
|
);
|
|
assert.equal(
|
|
modal.buildUrl("Qwen/Qwen3-4B-Thinking-2507-FP8", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl: "https://alice--demo.modal.run/v1",
|
|
},
|
|
}),
|
|
"https://alice--demo.modal.run/v1/chat/completions"
|
|
);
|
|
assert.equal(
|
|
reka.buildUrl("reka-core", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl: "https://api.reka.ai/v1",
|
|
},
|
|
}),
|
|
"https://api.reka.ai/v1/chat/completions"
|
|
);
|
|
assert.equal(
|
|
maritalk.buildUrl("sabia-4", true, 0, {
|
|
providerSpecificData: {
|
|
baseUrl: "https://chat.maritaca.ai/api/chat/inference",
|
|
},
|
|
}),
|
|
"https://chat.maritaca.ai/api/chat/completions"
|
|
);
|
|
assert.equal(
|
|
snowflake.buildUrl("llama3.3-70b", true, 0, {
|
|
providerSpecificData: { baseUrl: "https://account.snowflakecomputing.com" },
|
|
}),
|
|
"https://account.snowflakecomputing.com/api/v2/cortex/inference:complete"
|
|
);
|
|
assert.equal(
|
|
gigachat.buildUrl("GigaChat-2-Pro", true, 0, {
|
|
providerSpecificData: { baseUrl: "https://gigachat.devices.sberbank.ru/api/v1" },
|
|
}),
|
|
"https://gigachat.devices.sberbank.ru/api/v1/chat/completions"
|
|
);
|
|
assert.equal(
|
|
siliconflow.buildUrl("deepseek-ai/DeepSeek-V3.2", true),
|
|
"https://api.siliconflow.com/v1/chat/completions"
|
|
);
|
|
assert.equal(
|
|
siliconflow.buildUrl("deepseek-ai/DeepSeek-V3.2", true, 0, {
|
|
providerSpecificData: { baseUrl: "https://api.siliconflow.cn/v1" },
|
|
}),
|
|
"https://api.siliconflow.cn/v1/chat/completions"
|
|
);
|
|
});
|
|
|
|
test("DefaultExecutor.buildUrl falls back to OpenAI config for unknown providers", () => {
|
|
const executor = new DefaultExecutor("unknown-provider");
|
|
assert.equal(executor.config.baseUrl, PROVIDERS.openai.baseUrl);
|
|
assert.equal(executor.buildUrl("gpt-4.1", true), PROVIDERS.openai.baseUrl);
|
|
});
|
|
|
|
test("DefaultExecutor.buildUrl applies urlSuffix for zai and glm-coding-apikey", () => {
|
|
const zai = new DefaultExecutor("zai");
|
|
const glmCodingApikey = new DefaultExecutor("glm-coding-apikey");
|
|
assert.equal(
|
|
zai.buildUrl("glm-5", true, 0, {
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/anthropic/v1/messages" },
|
|
}),
|
|
"https://api.z.ai/api/anthropic/v1/messages?beta=true"
|
|
);
|
|
assert.equal(
|
|
glmCodingApikey.buildUrl("glm-4.7", true, 0, {
|
|
providerSpecificData: { baseUrl: "https://api.z.ai/api/anthropic/v1/messages" },
|
|
}),
|
|
"https://api.z.ai/api/anthropic/v1/messages?beta=true"
|
|
);
|
|
assert.equal(zai.buildUrl("glm-5", true), "https://api.z.ai/api/anthropic/v1/messages?beta=true");
|
|
});
|
|
|
|
test("DefaultExecutor.buildUrl applies urlSuffix from registry for unknown providers with suffix", () => {
|
|
const executor = new DefaultExecutor("unknown-provider");
|
|
assert.equal(executor.buildUrl("gpt-4.1", true), PROVIDERS.openai.baseUrl);
|
|
});
|
|
|
|
test("DefaultExecutor.buildHeaders uses x-api-key for zai and glm-coding-apikey", () => {
|
|
const zai = new DefaultExecutor("zai");
|
|
const glmCodingApikey = new DefaultExecutor("glm-coding-apikey");
|
|
const zaiHeaders = zai.buildHeaders({ apiKey: "zai-key" }, true);
|
|
const glmHeaders = glmCodingApikey.buildHeaders({ apiKey: "glm-key" }, true);
|
|
assert.equal(zaiHeaders["x-api-key"], "zai-key");
|
|
assert.equal(glmHeaders["x-api-key"], "glm-key");
|
|
assert.equal(zaiHeaders["Authorization"], undefined);
|
|
assert.equal(glmHeaders["Authorization"], undefined);
|
|
});
|
|
|
|
test("DefaultExecutor.buildHeaders handles Gemini and Claude auth modes", () => {
|
|
const gemini = new DefaultExecutor("gemini");
|
|
const claude = new DefaultExecutor("claude");
|
|
const azureAi = new DefaultExecutor("azure-ai");
|
|
const oci = new DefaultExecutor("oci");
|
|
const sap = new DefaultExecutor("sap");
|
|
const modal = new DefaultExecutor("modal");
|
|
const maritalk = new DefaultExecutor("maritalk");
|
|
|
|
const geminiApiKeyHeaders = gemini.buildHeaders({ apiKey: "gem-key" }, true);
|
|
const geminiOAuthHeaders = gemini.buildHeaders({ accessToken: "gem-token" }, false);
|
|
const claudeApiKeyHeaders = claude.buildHeaders({ apiKey: "claude-key" }, true);
|
|
const claudeOAuthHeaders = claude.buildHeaders({ accessToken: "claude-token" }, false);
|
|
const azureAiHeaders = azureAi.buildHeaders({ apiKey: "azure-ai-key" }, true);
|
|
const ociHeaders = oci.buildHeaders(
|
|
{
|
|
apiKey: "oci-key",
|
|
projectId: "ocid1.generativeaiproject.oc1.us-chicago-1.example",
|
|
},
|
|
true
|
|
);
|
|
const sapHeaders = sap.buildHeaders(
|
|
{
|
|
apiKey: "sap-key",
|
|
providerSpecificData: {
|
|
resourceGroup: "shared",
|
|
},
|
|
},
|
|
true
|
|
);
|
|
const modalHeaders = modal.buildHeaders(
|
|
{
|
|
apiKey: "modal-key",
|
|
},
|
|
true
|
|
);
|
|
const maritalkHeaders = maritalk.buildHeaders({ apiKey: "maritalk-key" }, true);
|
|
|
|
assert.equal(geminiApiKeyHeaders["x-goog-api-key"], "gem-key");
|
|
assert.equal(geminiApiKeyHeaders.Accept, "text/event-stream");
|
|
assert.equal(geminiApiKeyHeaders.Authorization, undefined);
|
|
assert.equal(geminiOAuthHeaders.Authorization, "Bearer gem-token");
|
|
assert.equal(claudeApiKeyHeaders["x-api-key"], "claude-key");
|
|
assert.equal(claudeApiKeyHeaders.Accept, "text/event-stream");
|
|
assert.equal(claudeOAuthHeaders.Authorization, "Bearer claude-token");
|
|
assert.equal(claudeOAuthHeaders["x-api-key"], undefined);
|
|
assert.equal(azureAiHeaders["api-key"], "azure-ai-key");
|
|
assert.equal(azureAiHeaders.Authorization, undefined);
|
|
assert.equal(ociHeaders.Authorization, "Bearer oci-key");
|
|
assert.equal(ociHeaders["OpenAI-Project"], "ocid1.generativeaiproject.oc1.us-chicago-1.example");
|
|
assert.equal(sapHeaders.Authorization, "Bearer sap-key");
|
|
assert.equal(sapHeaders["AI-Resource-Group"], "shared");
|
|
assert.equal(modalHeaders.Authorization, "Bearer modal-key");
|
|
assert.equal(maritalkHeaders.Authorization, "Key maritalk-key");
|
|
});
|
|
|
|
test("DefaultExecutor.buildHeaders handles GLM, default auth and anthropic-compatible headers", () => {
|
|
const glm = new DefaultExecutor("glm");
|
|
const glmt = new DefaultExecutor("glmt");
|
|
const openai = new DefaultExecutor("openai");
|
|
const anthropicCompat = new DefaultExecutor("anthropic-compatible-test");
|
|
|
|
const glmHeaders = glm.buildHeaders({ accessToken: "glm-token" }, false);
|
|
const glmtHeaders = glmt.buildHeaders({ apiKey: "glmt-key" }, false);
|
|
const openaiHeaders = openai.buildHeaders({ apiKey: "sk-openai" }, true);
|
|
const anthropicHeaders = anthropicCompat.buildHeaders({ apiKey: "anth-key" }, true);
|
|
|
|
assert.equal(glmHeaders["x-api-key"], "glm-token");
|
|
assert.equal(glmtHeaders["x-api-key"], "glmt-key");
|
|
assert.equal(openaiHeaders.Authorization, "Bearer sk-openai");
|
|
assert.equal(openaiHeaders.Accept, "text/event-stream");
|
|
assert.equal(anthropicHeaders["x-api-key"], "anth-key");
|
|
assert.equal(anthropicHeaders["anthropic-version"], "2023-06-01");
|
|
assert.equal(anthropicHeaders.Accept, "text/event-stream");
|
|
});
|
|
|
|
test("DefaultExecutor local OpenAI-style providers honor custom base URLs and skip empty bearer headers", () => {
|
|
const lmStudio = new DefaultExecutor("lm-studio");
|
|
const vllm = new DefaultExecutor("vllm");
|
|
|
|
const lmStudioUrl = lmStudio.buildUrl("local-model", true, 0, {
|
|
providerSpecificData: { baseUrl: "http://127.0.0.1:4321/v1" },
|
|
});
|
|
const vllmHeaders = vllm.buildHeaders({}, false);
|
|
|
|
assert.equal(lmStudioUrl, "http://127.0.0.1:4321/v1/chat/completions");
|
|
assert.equal(vllmHeaders.Authorization, undefined);
|
|
assert.equal(vllmHeaders.Accept, "application/json");
|
|
});
|
|
|
|
test("DefaultExecutor.buildHeaders handles Snowflake PATs and GigaChat access tokens", () => {
|
|
const snowflake = new DefaultExecutor("snowflake");
|
|
const gigachat = new DefaultExecutor("gigachat");
|
|
|
|
const snowflakePatHeaders = snowflake.buildHeaders({ apiKey: "pat/test-token" }, false);
|
|
const snowflakeJwtHeaders = snowflake.buildHeaders({ apiKey: "jwt-token" }, false);
|
|
const gigachatHeaders = gigachat.buildHeaders({ accessToken: "gigachat-token" }, false);
|
|
|
|
assert.equal(snowflakePatHeaders.Authorization, "Bearer test-token");
|
|
assert.equal(
|
|
snowflakePatHeaders["X-Snowflake-Authorization-Token-Type"],
|
|
"PROGRAMMATIC_ACCESS_TOKEN"
|
|
);
|
|
assert.equal(snowflakeJwtHeaders.Authorization, "Bearer jwt-token");
|
|
assert.equal(snowflakeJwtHeaders["X-Snowflake-Authorization-Token-Type"], "KEYPAIR_JWT");
|
|
assert.equal(gigachatHeaders.Authorization, "Bearer gigachat-token");
|
|
});
|
|
|
|
test("DefaultExecutor.buildHeaders strips DashScope headers for Qwen API keys and preserves them for OAuth", () => {
|
|
const executor = new DefaultExecutor("qwen");
|
|
|
|
const apiKeyHeaders = executor.buildHeaders({ apiKey: "dash-key" }, true);
|
|
const oauthHeaders = executor.buildHeaders({ accessToken: "oauth-token" }, true);
|
|
|
|
assert.equal(apiKeyHeaders.Authorization, "Bearer dash-key");
|
|
assert.equal(
|
|
Object.keys(apiKeyHeaders).some((key) => key.toLowerCase().startsWith("x-dashscope-")),
|
|
false
|
|
);
|
|
assert.equal(oauthHeaders.Authorization, "Bearer oauth-token");
|
|
assert.equal(oauthHeaders["X-Dashscope-AuthType"], "qwen-oauth");
|
|
assert.equal(oauthHeaders["X-Dashscope-CacheControl"], "enable");
|
|
});
|
|
|
|
test("DefaultExecutor.buildHeaders rotates extra API keys and builds Claude Code compatible headers", () => {
|
|
const openai = new DefaultExecutor("openai");
|
|
const cc = new DefaultExecutor("anthropic-compatible-cc-test");
|
|
|
|
const first = openai.buildHeaders(
|
|
{
|
|
apiKey: "primary",
|
|
connectionId: "conn-rotation",
|
|
providerSpecificData: { extraApiKeys: ["extra-1", "extra-2"] },
|
|
},
|
|
false
|
|
);
|
|
const second = openai.buildHeaders(
|
|
{
|
|
apiKey: "primary",
|
|
connectionId: "conn-rotation",
|
|
providerSpecificData: { extraApiKeys: ["extra-1", "extra-2"] },
|
|
},
|
|
false
|
|
);
|
|
const ccHeaders = cc.buildHeaders(
|
|
{
|
|
apiKey: "cc-key",
|
|
providerSpecificData: { ccSessionId: "session-1" },
|
|
},
|
|
true
|
|
);
|
|
const ccJsonHeaders = cc.buildHeaders(
|
|
{
|
|
apiKey: "cc-key",
|
|
providerSpecificData: { ccSessionId: "session-1" },
|
|
},
|
|
false
|
|
);
|
|
|
|
assert.equal(first.Authorization, "Bearer primary");
|
|
assert.equal(second.Authorization, "Bearer extra-1");
|
|
assert.equal(ccHeaders.Authorization, "Bearer cc-key");
|
|
assert.equal(ccHeaders["x-api-key"], undefined);
|
|
assert.equal(ccHeaders["anthropic-version"], CLAUDE_CODE_COMPATIBLE_ANTHROPIC_VERSION);
|
|
assert.equal(ccHeaders["X-Claude-Code-Session-Id"], "session-1");
|
|
assert.equal(ccHeaders.Accept, "application/json");
|
|
assert.equal(ccJsonHeaders.Accept, "application/json");
|
|
});
|
|
|
|
test("DefaultExecutor.execute uses CC-compatible connection defaults to append 1M beta", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const calls = [];
|
|
const toPlainHeaders = (headers) =>
|
|
headers instanceof Headers
|
|
? Object.fromEntries(headers.entries())
|
|
: Object.fromEntries(
|
|
Object.entries(headers || {}).map(([key, value]) => [
|
|
key,
|
|
value == null ? "" : String(value),
|
|
])
|
|
);
|
|
|
|
globalThis.fetch = async (_url, init = {}) => {
|
|
calls.push({ headers: toPlainHeaders(init.headers) });
|
|
return new Response(JSON.stringify({ ok: true }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
};
|
|
|
|
try {
|
|
const cc = new DefaultExecutor("anthropic-compatible-cc-test");
|
|
await cc.execute({
|
|
model: "claude-sonnet-4-6",
|
|
body: {
|
|
model: "claude-sonnet-4-6",
|
|
messages: [{ role: "user", content: "hi" }],
|
|
max_tokens: 1,
|
|
},
|
|
stream: false,
|
|
credentials: {
|
|
apiKey: "cc-key",
|
|
providerSpecificData: {
|
|
ccSessionId: "session-1",
|
|
},
|
|
},
|
|
clientHeaders: {
|
|
"x-app": "cli",
|
|
"user-agent": "claude-cli/2.1.116 (external, cli)",
|
|
},
|
|
extendedContext: false,
|
|
});
|
|
await cc.execute({
|
|
model: "claude-sonnet-4-6",
|
|
body: {
|
|
model: "claude-sonnet-4-6",
|
|
messages: [{ role: "user", content: "hi" }],
|
|
max_tokens: 1,
|
|
},
|
|
stream: false,
|
|
credentials: {
|
|
apiKey: "cc-key",
|
|
providerSpecificData: {
|
|
ccSessionId: "session-1",
|
|
requestDefaults: { context1m: true },
|
|
},
|
|
},
|
|
extendedContext: false,
|
|
});
|
|
|
|
const anthropicCompat = new DefaultExecutor("anthropic-compatible-test");
|
|
await anthropicCompat.execute({
|
|
model: "claude-sonnet-4-6",
|
|
body: {
|
|
model: "claude-sonnet-4-6",
|
|
messages: [{ role: "user", content: "hi" }],
|
|
max_tokens: 1,
|
|
},
|
|
stream: false,
|
|
credentials: {
|
|
apiKey: "anth-key",
|
|
providerSpecificData: {
|
|
baseUrl: "https://anthropic.example.com/v1",
|
|
},
|
|
},
|
|
extendedContext: true,
|
|
});
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
|
|
assert.equal(calls[0].headers["anthropic-beta"].includes(CONTEXT_1M_BETA_HEADER), false);
|
|
assert.equal(calls[1].headers["anthropic-beta"].includes(CONTEXT_1M_BETA_HEADER), true);
|
|
assert.equal(calls[2].headers["anthropic-beta"], undefined);
|
|
});
|
|
|
|
test("DefaultExecutor.execute only injects adaptive thinking defaults for Claude models that support x-high effort", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const requestBodies = [];
|
|
|
|
globalThis.fetch = async (_url, init = {}) => {
|
|
requestBodies.push(JSON.parse(String(init.body)));
|
|
return new Response(JSON.stringify({ ok: true }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
};
|
|
|
|
try {
|
|
const claude = new DefaultExecutor("claude");
|
|
await claude.execute({
|
|
model: "claude-opus-4-7",
|
|
body: {
|
|
model: "claude-opus-4-7",
|
|
messages: [{ role: "user", content: "hi" }],
|
|
max_tokens: 1,
|
|
},
|
|
stream: false,
|
|
credentials: {
|
|
apiKey: "cc-key",
|
|
providerSpecificData: {
|
|
ccSessionId: "session-1",
|
|
},
|
|
},
|
|
clientHeaders: {
|
|
"x-app": "cli",
|
|
"user-agent": "claude-cli/2.1.116 (external, cli)",
|
|
},
|
|
extendedContext: false,
|
|
});
|
|
|
|
await claude.execute({
|
|
model: "claude-haiku-4-5-20251001",
|
|
body: {
|
|
model: "claude-haiku-4-5-20251001",
|
|
messages: [{ role: "user", content: "hi" }],
|
|
max_tokens: 1,
|
|
},
|
|
stream: false,
|
|
credentials: {
|
|
apiKey: "cc-key",
|
|
providerSpecificData: {
|
|
ccSessionId: "session-1",
|
|
},
|
|
},
|
|
clientHeaders: {
|
|
"x-app": "cli",
|
|
"user-agent": "claude-cli/2.1.116 (external, cli)",
|
|
},
|
|
extendedContext: false,
|
|
});
|
|
|
|
await claude.execute({
|
|
model: "claude-sonnet-4-6",
|
|
body: {
|
|
model: "claude-sonnet-4-6",
|
|
messages: [{ role: "user", content: "hi" }],
|
|
max_tokens: 1,
|
|
thinking: { type: "disabled" },
|
|
},
|
|
stream: false,
|
|
credentials: {
|
|
apiKey: "cc-key",
|
|
providerSpecificData: {
|
|
ccSessionId: "session-1",
|
|
},
|
|
},
|
|
clientHeaders: {
|
|
"x-app": "cli",
|
|
"user-agent": "claude-cli/2.1.116 (external, cli)",
|
|
},
|
|
extendedContext: false,
|
|
});
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
|
|
assert.deepEqual((requestBodies[0] as any).thinking, { type: "adaptive" });
|
|
assert.deepEqual((requestBodies[0] as any).context_management, {
|
|
edits: [{ type: "clear_thinking_20251015", keep: "all" }],
|
|
});
|
|
assert.deepEqual((requestBodies[0] as any).output_config, { effort: "high" });
|
|
|
|
assert.equal((requestBodies[1] as any).thinking, undefined);
|
|
assert.equal((requestBodies[1] as any).context_management, undefined);
|
|
assert.equal((requestBodies[1] as any).output_config, undefined);
|
|
|
|
assert.deepEqual((requestBodies[2] as any).thinking, { type: "disabled" });
|
|
assert.equal((requestBodies[2] as any).context_management, undefined);
|
|
});
|
|
|
|
test("DefaultExecutor.transformRequest injects OpenAI stream usage and preserves model ids with slashes", () => {
|
|
const executor = new DefaultExecutor("openai");
|
|
const body = { model: "zai-org/GLM-5-FP8", messages: [{ role: "user", content: "hi" }] };
|
|
const result = executor.transformRequest("zai-org/GLM-5-FP8", body, true, {});
|
|
|
|
assert.notEqual(result, body);
|
|
assert.equal(result.model, "zai-org/GLM-5-FP8");
|
|
assert.deepEqual((result as any).stream_options, { include_usage: true });
|
|
assert.equal((body as any).stream_options, undefined);
|
|
});
|
|
|
|
test("DefaultExecutor.transformRequest only injects stream usage for OpenAI chat targets", () => {
|
|
const openAICompat = new DefaultExecutor("openai-compatible-test");
|
|
const openAIResponsesCompat = new DefaultExecutor("openai-compatible-responses-test");
|
|
|
|
const chatBody = { model: "gpt-4.1", messages: [{ role: "user", content: "hi" }] };
|
|
const responsesBody = { model: "gpt-4.1", input: "hi" };
|
|
|
|
const chatResult = openAICompat.transformRequest("gpt-4.1", chatBody, true, {
|
|
providerSpecificData: { baseUrl: "https://proxy.example/v1" },
|
|
});
|
|
const responsesResult = openAIResponsesCompat.transformRequest("gpt-4.1", responsesBody, true, {
|
|
providerSpecificData: { baseUrl: "https://proxy.example/v1" },
|
|
});
|
|
|
|
assert.deepEqual((chatResult as any).stream_options, { include_usage: true });
|
|
assert.equal((responsesResult as any).stream_options, undefined);
|
|
});
|
|
|
|
test("DefaultExecutor.transformRequest respects disableStreamOptions for OpenAI chat targets", () => {
|
|
const openAICompat = new DefaultExecutor("openai-compatible-test");
|
|
const chatBody = { model: "gpt-4.1", messages: [{ role: "user", content: "hi" }] };
|
|
|
|
const chatResultDisabled = openAICompat.transformRequest("gpt-4.1", chatBody, true, {
|
|
providerSpecificData: { baseUrl: "https://proxy.example/v1", disableStreamOptions: true },
|
|
});
|
|
|
|
const chatResultEnabled = openAICompat.transformRequest("gpt-4.1", chatBody, true, {
|
|
providerSpecificData: { baseUrl: "https://proxy.example/v1", disableStreamOptions: false },
|
|
});
|
|
|
|
assert.equal((chatResultDisabled as any).stream_options, undefined);
|
|
assert.deepEqual((chatResultEnabled as any).stream_options, { include_usage: true });
|
|
});
|
|
|
|
test("DefaultExecutor.transformRequest strips stream_options from Anthropic-compatible targets", () => {
|
|
const anthropicCompat = new DefaultExecutor("anthropic-compatible-test");
|
|
const anthropicCcCompat = new DefaultExecutor("anthropic-compatible-cc-test");
|
|
|
|
const anthropicBody = {
|
|
model: "claude-sonnet-4-6",
|
|
messages: [{ role: "user", content: "hi" }],
|
|
max_tokens: 1,
|
|
stream_options: { include_usage: true },
|
|
};
|
|
const ccBody = {
|
|
model: "claude-sonnet-4-6",
|
|
messages: [{ role: "user", content: "hi" }],
|
|
max_tokens: 1,
|
|
};
|
|
|
|
const anthropicResult = anthropicCompat.transformRequest(
|
|
"claude-sonnet-4-6",
|
|
anthropicBody,
|
|
true,
|
|
{}
|
|
);
|
|
const ccResult = anthropicCcCompat.transformRequest("claude-sonnet-4-6", ccBody, true, {});
|
|
|
|
assert.notEqual(anthropicResult, anthropicBody);
|
|
assert.equal((anthropicResult as any).stream_options, undefined);
|
|
assert.equal((ccResult as any).stream_options, undefined);
|
|
});
|
|
|
|
test("DefaultExecutor.transformRequest neutralizes incompatible tool_choice for Qwen thinking", () => {
|
|
const executor = new DefaultExecutor("qwen");
|
|
const body = {
|
|
messages: [{ role: "user", content: "hi" }],
|
|
thinking: { type: "enabled" },
|
|
tool_choice: { type: "function", function: { name: "pwd" } },
|
|
};
|
|
const result = executor.transformRequest("qwen3-coder-plus", body, true, {});
|
|
|
|
assert.notEqual(result, body);
|
|
assert.equal((result as any).tool_choice, "auto");
|
|
});
|
|
|
|
test("DefaultExecutor.transformRequest applies GLMT preset defaults without overriding explicit values", () => {
|
|
const executor = new DefaultExecutor("glmt");
|
|
|
|
const autoBody = {
|
|
messages: [{ role: "user", content: "hi" }],
|
|
};
|
|
const autoResult = executor.transformRequest("glm-5.1", autoBody, true, {});
|
|
|
|
assert.notEqual(autoResult, autoBody);
|
|
assert.equal((autoResult as any).max_tokens, 65536);
|
|
(assert as any).equal((autoResult as any).temperature, 0.2);
|
|
(assert as any).deepEqual((autoResult as any).thinking, {
|
|
type: "enabled",
|
|
budget_tokens: 24576,
|
|
});
|
|
|
|
const explicitBody = {
|
|
messages: [{ role: "user", content: "hi" }],
|
|
max_tokens: 4096,
|
|
temperature: 0.7,
|
|
thinking: { type: "enabled" },
|
|
};
|
|
const explicitResult = executor.transformRequest("glm-5.1", explicitBody, true, {});
|
|
|
|
assert.notEqual(explicitResult, explicitBody);
|
|
assert.equal((explicitResult as any).max_tokens, 4096);
|
|
assert.equal((explicitResult as any).temperature, 0.7);
|
|
assert.deepEqual((explicitResult as any).thinking, {
|
|
type: "enabled",
|
|
budget_tokens: 4095,
|
|
});
|
|
});
|
|
|
|
test("BaseExecutor helpers manage custom user agents and upstream extra headers", () => {
|
|
const headers = { "user-agent": "old", Authorization: "Bearer old" };
|
|
|
|
assert.equal(getCustomUserAgent({ customUserAgent: " MyAgent/1.0 " }), "MyAgent/1.0");
|
|
assert.equal(getCustomUserAgent({ customUserAgent: " " }), null);
|
|
|
|
setUserAgentHeader(headers, "MyAgent/2.0");
|
|
assert.equal(headers["User-Agent"], "MyAgent/2.0");
|
|
assert.equal(headers["user-agent"], "MyAgent/2.0");
|
|
|
|
applyConfiguredUserAgent(headers, { customUserAgent: "MyAgent/3.0" });
|
|
assert.equal(headers["User-Agent"], "MyAgent/3.0");
|
|
|
|
mergeUpstreamExtraHeaders(headers, {
|
|
Authorization: "Bearer override",
|
|
"user-agent": "Merged/4.0",
|
|
"X-Upstream": "1",
|
|
});
|
|
assert.equal(headers.Authorization, "Bearer override");
|
|
assert.equal(headers["User-Agent"], "Merged/4.0");
|
|
assert.equal(headers["user-agent"], "Merged/4.0");
|
|
assert.equal(headers["X-Upstream"], "1");
|
|
});
|
|
|
|
test("BaseExecutor.mergeAbortSignals aborts when either source signal aborts", () => {
|
|
const primary = new AbortController();
|
|
const secondary = new AbortController();
|
|
const merged = mergeAbortSignals(primary.signal, secondary.signal);
|
|
|
|
assert.equal(merged.aborted, false);
|
|
const primaryReason = new Error("primary timeout");
|
|
primaryReason.name = "TimeoutError";
|
|
primary.abort(primaryReason);
|
|
assert.equal(merged.aborted, true);
|
|
assert.equal(merged.reason, primaryReason);
|
|
|
|
const otherPrimary = new AbortController();
|
|
const otherSecondary = new AbortController();
|
|
const merged2 = mergeAbortSignals(otherPrimary.signal, otherSecondary.signal);
|
|
const secondaryReason = new Error("client closed");
|
|
otherSecondary.abort(secondaryReason);
|
|
assert.equal(merged2.aborted, true);
|
|
assert.equal(merged2.reason, secondaryReason);
|
|
});
|
|
|
|
test("BaseExecutor.needsRefresh returns true only when expiry is near", () => {
|
|
const executor = new TestExecutor();
|
|
const soon = new Date(Date.now() + 60_000).toISOString();
|
|
const later = new Date(Date.now() + 60 * 60 * 1000).toISOString();
|
|
|
|
assert.equal(executor.needsRefresh({ expiresAt: soon }), true);
|
|
assert.equal(executor.needsRefresh({ expiresAt: later }), false);
|
|
assert.equal(executor.needsRefresh({}), false);
|
|
});
|
|
|
|
test("DefaultExecutor.refreshCredentials returns null without refresh token", async () => {
|
|
const executor = new DefaultExecutor("gemini");
|
|
const result = await executor.refreshCredentials({}, null);
|
|
assert.equal(result, null);
|
|
});
|
|
|
|
test("DefaultExecutor.needsRefresh requests a proactive token for GigaChat", () => {
|
|
const executor = new DefaultExecutor("gigachat");
|
|
|
|
assert.equal(executor.needsRefresh({ apiKey: "base64-basic-credentials" }), true);
|
|
assert.equal(
|
|
executor.needsRefresh({
|
|
apiKey: "base64-basic-credentials",
|
|
accessToken: "existing-token",
|
|
expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
|
}),
|
|
false
|
|
);
|
|
});
|
|
|
|
test("DefaultExecutor.refreshCredentials delegates to OAuth refresh and returns new tokens", async () => {
|
|
const executor = new DefaultExecutor("gemini");
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async (url, options) => {
|
|
assert.match(String(url), /oauth2\.googleapis\.com/);
|
|
assert.equal(options.method, "POST");
|
|
return new Response(
|
|
JSON.stringify({
|
|
access_token: "new-access-token",
|
|
refresh_token: "new-refresh-token",
|
|
expires_in: 3600,
|
|
}),
|
|
{
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
};
|
|
|
|
try {
|
|
const result = await executor.refreshCredentials({ refreshToken: "refresh-me" }, null);
|
|
assert.deepEqual(result, {
|
|
accessToken: "new-access-token",
|
|
refreshToken: "new-refresh-token",
|
|
expiresIn: 3600,
|
|
});
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("DefaultExecutor.refreshCredentials swallows refresh errors and logs them", async () => {
|
|
const executor = new DefaultExecutor("gemini");
|
|
const originalFetch = globalThis.fetch;
|
|
const messages = [];
|
|
globalThis.fetch = async () => {
|
|
throw new Error("network down");
|
|
};
|
|
|
|
try {
|
|
const result = await executor.refreshCredentials(
|
|
{ refreshToken: "refresh-me" },
|
|
{ error: (tag, message) => messages.push({ tag, message }) }
|
|
);
|
|
assert.equal(result, null);
|
|
assert.equal(messages.length, 1);
|
|
assert.match(messages[0].message, /refresh error: network down/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("BaseExecutor.execute returns response metadata and merges headers", async () => {
|
|
const executor = new TestExecutor();
|
|
const originalFetch = globalThis.fetch;
|
|
let captured;
|
|
globalThis.fetch = async (url, options) => {
|
|
captured = { url, options };
|
|
return new Response(JSON.stringify({ ok: true }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
};
|
|
|
|
try {
|
|
const result = await executor.execute({
|
|
model: "gpt-4.1",
|
|
body: { messages: [{ role: "user", content: "hi" }] },
|
|
stream: true,
|
|
credentials: {
|
|
apiKey: "base-key",
|
|
providerSpecificData: { customUserAgent: "CredsAgent/1.0" },
|
|
},
|
|
upstreamExtraHeaders: {
|
|
Authorization: "Bearer override",
|
|
"user-agent": "UpstreamAgent/2.0",
|
|
"X-Trace-Id": "trace-1",
|
|
},
|
|
});
|
|
|
|
assert.equal(result.url, "https://primary.example/v1/chat/completions");
|
|
assert.equal(result.response.status, 200);
|
|
(assert as any).equal((result.transformedBody as any).transformed, true);
|
|
assert.equal((result.transformedBody as any).model, "gpt-4.1");
|
|
assert.equal(result.headers.Authorization, "Bearer override");
|
|
assert.equal(result.headers["User-Agent"], "UpstreamAgent/2.0");
|
|
assert.equal(result.headers["user-agent"], undefined);
|
|
assert.equal(result.headers["X-Trace-Id"], "trace-1");
|
|
assert.equal(result.headers.Accept, "text/event-stream");
|
|
assert.equal(captured.options.body.includes('"transformed":true'), true);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("BaseExecutor.execute refreshes credentials before the request when needed", async () => {
|
|
class RefreshingExecutor extends BaseExecutor {
|
|
constructor() {
|
|
super("refreshing-provider", {
|
|
baseUrl: "https://refresh.example/v1/chat/completions",
|
|
});
|
|
}
|
|
|
|
needsRefresh() {
|
|
return true;
|
|
}
|
|
|
|
async refreshCredentials() {
|
|
return {
|
|
accessToken: "fresh-token",
|
|
expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
|
|
};
|
|
}
|
|
}
|
|
|
|
const executor = new RefreshingExecutor();
|
|
const originalFetch = globalThis.fetch;
|
|
let capturedHeaders;
|
|
globalThis.fetch = async (url, options) => {
|
|
assert.equal(String(url), "https://refresh.example/v1/chat/completions");
|
|
capturedHeaders = options.headers;
|
|
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
};
|
|
|
|
try {
|
|
await executor.execute({
|
|
model: "gpt-4.1",
|
|
body: {},
|
|
stream: false,
|
|
credentials: { apiKey: "stale-token" },
|
|
});
|
|
|
|
assert.equal(capturedHeaders.Authorization, "Bearer fresh-token");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("BaseExecutor.execute falls back to the next base URL after a transport error", async () => {
|
|
const executor = new TestExecutor();
|
|
const originalFetch = globalThis.fetch;
|
|
const calls = [];
|
|
globalThis.fetch = async (url) => {
|
|
calls.push(String(url));
|
|
if (calls.length === 1) {
|
|
throw new Error("first node down");
|
|
}
|
|
return new Response("ok", { status: 200 });
|
|
};
|
|
|
|
try {
|
|
const result = await executor.execute({
|
|
model: "gpt-4.1",
|
|
body: { hello: "world" },
|
|
stream: false,
|
|
credentials: {},
|
|
});
|
|
|
|
assert.deepEqual(calls, [
|
|
"https://primary.example/v1/chat/completions",
|
|
"https://fallback.example/v1/chat/completions",
|
|
]);
|
|
assert.equal(result.url, "https://fallback.example/v1/chat/completions");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("BaseExecutor.execute throws the last error when all URLs fail", async () => {
|
|
const executor = new TestExecutor();
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async () => {
|
|
throw new Error("still down");
|
|
};
|
|
|
|
try {
|
|
await assert.rejects(
|
|
executor.execute({
|
|
model: "gpt-4.1",
|
|
body: {},
|
|
stream: false,
|
|
credentials: {},
|
|
}),
|
|
/still down/
|
|
);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("BaseExecutor.execute propagates aborted requests through the merged signal", async () => {
|
|
const executor = new TestExecutor({ baseUrls: ["https://single.example/v1/chat/completions"] });
|
|
const controller = new AbortController();
|
|
controller.abort();
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
globalThis.fetch = async (url, options) => {
|
|
assert.equal(options.signal.aborted, true);
|
|
const error = new Error(`aborted ${url}`);
|
|
error.name = "AbortError";
|
|
throw error;
|
|
};
|
|
|
|
try {
|
|
await assert.rejects(
|
|
executor.execute({
|
|
model: "gpt-4.1",
|
|
body: {},
|
|
stream: false,
|
|
credentials: {},
|
|
signal: controller.signal,
|
|
}),
|
|
/aborted/
|
|
);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("BaseExecutor.execute clears the startup timeout after headers arrive", async () => {
|
|
const executor = new TestExecutor({ baseUrls: ["https://single.example/v1/chat/completions"] });
|
|
const originalFetch = globalThis.fetch;
|
|
const originalFetchStartTimeoutMs = BaseExecutor.FETCH_START_TIMEOUT_MS;
|
|
let capturedSignal;
|
|
|
|
BaseExecutor.FETCH_START_TIMEOUT_MS = 20;
|
|
globalThis.fetch = async (_url, options) => {
|
|
capturedSignal = options.signal;
|
|
return new Response("ok", {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
};
|
|
|
|
try {
|
|
await executor.execute({
|
|
model: "gpt-4.1",
|
|
body: {},
|
|
stream: true,
|
|
credentials: {},
|
|
});
|
|
|
|
assert.equal(capturedSignal?.aborted, false);
|
|
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
assert.equal(capturedSignal?.aborted, false);
|
|
} finally {
|
|
BaseExecutor.FETCH_START_TIMEOUT_MS = originalFetchStartTimeoutMs;
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
// Regression test for issue #1454: duplicate anthropic-version header when
|
|
// Claude Code CLI headers are detected on the native `claude` provider.
|
|
// The provider config seeds headers with Title-Case "Anthropic-Version" while
|
|
// the Claude-Code patch injects lowercase "anthropic-version". Before the fix,
|
|
// both keys coexisted in the JS object and undici combined their values into
|
|
// "2023-06-01, 2023-06-01", causing a 400 from Anthropic.
|
|
test("DefaultExecutor.execute does not produce duplicate anthropic-version header when Claude Code CLI headers are present", async () => {
|
|
const executor = new DefaultExecutor("claude");
|
|
const originalFetch = globalThis.fetch;
|
|
let capturedHeaders: Record<string, string> = {};
|
|
|
|
globalThis.fetch = async (_url, init = {}) => {
|
|
// Capture raw headers without normalisation so case-variant duplicate keys are visible.
|
|
capturedHeaders = (init.headers as Record<string, string>) || {};
|
|
return new Response(JSON.stringify({ ok: true }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
};
|
|
|
|
try {
|
|
await executor.execute({
|
|
model: "claude-sonnet-4-6",
|
|
body: {
|
|
model: "claude-sonnet-4-6",
|
|
messages: [{ role: "user", content: "hi" }],
|
|
max_tokens: 1,
|
|
},
|
|
stream: false,
|
|
credentials: { accessToken: "oauth-token" },
|
|
clientHeaders: {
|
|
"x-app": "cli",
|
|
"user-agent": "claude-cli/2.1.116 (external, cli)",
|
|
"anthropic-beta": "oauth-2025-04-20",
|
|
},
|
|
});
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
|
|
// Must be exactly one key — not multiple case variants that undici would combine
|
|
const versionKeys = Object.keys(capturedHeaders).filter(
|
|
(k) => k.toLowerCase() === "anthropic-version"
|
|
);
|
|
assert.equal(versionKeys.length, 1, "Duplicate anthropic-version header keys found");
|
|
assert.equal(capturedHeaders[versionKeys[0]], "2023-06-01");
|
|
});
|