diff --git a/open-sse/config/codexClient.ts b/open-sse/config/codexClient.ts index 339886a8e6..0cb1b05772 100644 --- a/open-sse/config/codexClient.ts +++ b/open-sse/config/codexClient.ts @@ -1,4 +1,9 @@ -const DEFAULT_CODEX_CLIENT_VERSION = "0.144.1"; +import { + DEFAULT_CODEX_CLIENT_VERSION, + getCodexCliRsHeaders as buildCodexCliRsHeaders, +} from "@/shared/constants/codexClient"; + +export { DEFAULT_CODEX_CLIENT_VERSION } from "@/shared/constants/codexClient"; const DEFAULT_CODEX_USER_AGENT_PLATFORM = "Windows 10.0.26200"; const DEFAULT_CODEX_USER_AGENT_ARCH = "x64"; const CODEX_VERSION_OVERRIDE_ENV = "CODEX_CLIENT_VERSION"; @@ -42,6 +47,10 @@ export function getCodexDefaultHeaders(): Record { }; } +export function getCodexCliRsHeaders(): Record { + return buildCodexCliRsHeaders(getCodexClientVersion()); +} + export function normalizeCodexSessionId(value: unknown): string | null { if (typeof value !== "string") return null; const normalized = value.trim(); diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts index 393e6a4459..9893a08fd4 100644 --- a/open-sse/config/freeModelCatalog.data.ts +++ b/open-sse/config/freeModelCatalog.data.ts @@ -19,10 +19,9 @@ import type { FreeModelBudget } from "./freeModelCatalog.ts"; export const FREE_CATALOG_CURATED_AT = "2026-07-22"; export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [ - { provider: "agentrouter", modelId: "claude-opus-4-6", displayName: "Claude 4.6 Opus", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" }, - { provider: "agentrouter", modelId: "claude-haiku-4-5-20251001", displayName: "Claude 4.5 Haiku", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" }, - { provider: "agentrouter", modelId: "glm-5.1", displayName: "GLM 5.1", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" }, - { provider: "agentrouter", modelId: "deepseek-v3.2", displayName: "DeepSeek V3.2", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" }, + { provider: "agentrouter", modelId: "claude-opus-4-8", displayName: "Claude Opus 4.8", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" }, + { provider: "agentrouter", modelId: "claude-opus-5", displayName: "Claude Opus 5", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" }, + { provider: "agentrouter", modelId: "gpt-5.6-sol", displayName: "GPT-5.6 Sol", monthlyTokens: 0, creditTokens: 200000000, freeType: "one-time-initial", poolKey: "agentrouter", tos: "caution" }, { provider: "agy", modelId: "claude-opus-4-6-thinking", displayName: "Claude Opus 4.6 (Thinking)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" }, { provider: "agy", modelId: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6 (Thinking)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" }, { provider: "agy", modelId: "gemini-3.1-pro-low", displayName: "Gemini 3.1 Pro (Low)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "agy", tos: "avoid" }, diff --git a/open-sse/config/providers/registry/agentrouter/index.ts b/open-sse/config/providers/registry/agentrouter/index.ts index ebe9d4598f..9846e03fe4 100644 --- a/open-sse/config/providers/registry/agentrouter/index.ts +++ b/open-sse/config/providers/registry/agentrouter/index.ts @@ -1,4 +1,5 @@ import type { RegistryEntry } from "../../shared.ts"; +import { getCodexCliRsHeaders } from "../../../codexClient.ts"; export const agentrouterProvider: RegistryEntry = { id: "agentrouter", @@ -8,6 +9,22 @@ export const agentrouterProvider: RegistryEntry = { baseUrl: "https://agentrouter.org/v1/messages", authType: "apikey", authHeader: "x-api-key", + alternateFormats: [ + { + format: "openai", + baseUrl: "https://agentrouter.org/v1/chat/completions", + authHeader: "bearer", + headers: getCodexCliRsHeaders(), + label: "OpenAI-compatible (Codex)", + }, + { + format: "openai-responses", + baseUrl: "https://agentrouter.org/v1/responses", + authHeader: "bearer", + headers: getCodexCliRsHeaders(), + label: "OpenAI Responses (Codex)", + }, + ], defaultContextLength: 128000, // No static `headers` here: agentrouter now adopts the DYNAMIC Claude-Code // wire image via CC_WIRE_IMAGE_BUILTINS (#6056) — the fingerprint/headers are @@ -15,10 +32,9 @@ export const agentrouterProvider: RegistryEntry = { // own baseUrl + x-api-key auth. A static fingerprint here would drift and // trip AgentRouter's WAF ("unauthorized client detected"). models: [ - { id: "claude-opus-4-6", name: "Claude 4.6 Opus" }, - { id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" }, - { id: "glm-5.1", name: "GLM 5.1" }, - { id: "deepseek-v3.2", name: "DeepSeek V3.2" }, + { id: "claude-opus-4-8", name: "Claude Opus 4.8" }, + { id: "claude-opus-5", name: "Claude Opus 5" }, + { id: "gpt-5.6-sol", name: "GPT-5.6 Sol" }, ], passthroughModels: true, }; diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index 8149f03d48..d30e122d1c 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -45,6 +45,7 @@ import { } from "../services/apiKeyRotator.ts"; import type { KeyHealth } from "../services/apiKeyRotator.ts"; import { getOpenAICompatibleType, isClaudeCodeCompatible } from "../services/provider.ts"; +import { usesCcWireImage } from "../services/ccWireImageBuiltins.ts"; import { runWithOnPersist, getRefreshLeadMs, @@ -249,7 +250,10 @@ function collectThinkingConfigs(body: unknown): Array> { if (!body || typeof body !== "object") return []; const root = body as Record; const configs: Array> = []; - const envelopes: unknown[] = [root.generationConfig, (root.request as Record | undefined)?.generationConfig]; + const envelopes: unknown[] = [ + root.generationConfig, + (root.request as Record | undefined)?.generationConfig, + ]; for (const env of envelopes) { if (!env || typeof env !== "object") continue; const tc = (env as Record).thinkingConfig; @@ -445,6 +449,12 @@ export class BaseExecutor { ); } + protected usesClaudeCodeProtocol(credentials: ProviderCredentials | null): boolean { + if (!isClaudeCodeCompatible(this.provider)) return false; + const format = this.resolveAlternate(credentials)?.format; + return format !== "openai" && format !== "openai-responses"; + } + /** * Resolve the effective API key via extra-keys round-robin rotation. * Mutates `credentials.providerSpecificData.selectedKeyId` on rotation. @@ -834,15 +844,16 @@ export class BaseExecutor { ); } - const ccRequestDefaults = isClaudeCodeCompatible(this.provider) + const usesClaudeCodeProtocol = this.usesClaudeCodeProtocol(requestCredentials); + const fingerprintProvider = + usesCcWireImage(this.provider) && !usesClaudeCodeProtocol ? "codex" : this.provider; + const ccRequestDefaults = usesClaudeCodeProtocol ? getClaudeCodeCompatibleRequestDefaults(requestCredentials?.providerSpecificData) : {}; const shouldForwardExtendedContext = - extendedContext && - modelSupportsContext1mBeta(model) && - !isClaudeCodeCompatible(this.provider); + extendedContext && modelSupportsContext1mBeta(model) && !usesClaudeCodeProtocol; const shouldForwardCcCompatibleContext1m = - isClaudeCodeCompatible(this.provider) && + usesClaudeCodeProtocol && ccRequestDefaults.context1m === true && !modelHasNativeContext1m(model); if (shouldForwardExtendedContext || shouldForwardCcCompatibleContext1m) { @@ -922,8 +933,8 @@ export class BaseExecutor { !activeCredentials?.apiKey; if ( - this.provider === "claude" && - (isClaudeCodeClient || hasClaudeOAuthToken) && + ((this.provider === "claude" && (isClaudeCodeClient || hasClaudeOAuthToken)) || + usesClaudeCodeProtocol) && typeof transformedBody === "object" && transformedBody !== null ) { @@ -1200,6 +1211,11 @@ export class BaseExecutor { if (ccKeysLower.has(key.toLowerCase())) delete headers[key]; } Object.assign(headers, ccHeaders); + if (usesCcWireImage(this.provider) && usesClaudeCodeProtocol) { + delete headers["Authorization"]; + headers["x-api-key"] = + activeCredentials?.apiKey || activeCredentials?.accessToken || ""; + } delete headers["X-Stainless-Helper-Method"]; // OS/arch follow the host running the signed binary. Runtime version @@ -1246,7 +1262,7 @@ export class BaseExecutor { // (tool_result must be in immediately next message). // Only apply for Claude/Claude-compatible — OpenAI allows results // spread across multiple subsequent messages. - const isClaude = this.provider === "claude" || isClaudeCodeCompatible(this.provider); + const isClaude = this.provider === "claude" || usesClaudeCodeProtocol; // For Claude, fixToolAdjacency may strip tool_use blocks whose // tool_result isn't in the next message; re-run fixToolPairs to // drop any tool_result orphaned by that strip (discussion #2410). @@ -1265,7 +1281,7 @@ export class BaseExecutor { // at this final dispatch point — the single chokepoint every Claude // routing mode (grouped/raw/combo) and the native passthrough share, // before fingerprinting and CCH signing serialize the body. - if (this.provider === "claude" || isClaudeCodeCompatible(this.provider)) { + if (this.provider === "claude" || usesClaudeCodeProtocol) { enforceThinkingTemperature(transformedBody as Record); } @@ -1282,7 +1298,7 @@ export class BaseExecutor { // `contextEditingDisabled` (set by the 400-fallback) suppresses re-injection // when a fresh `transformedBody` is built for a retry/fallback URL. if ( - (this.provider === "claude" || isClaudeCodeCompatible(this.provider)) && + (this.provider === "claude" || usesClaudeCodeProtocol) && contextEditing?.enabled && !contextEditingDisabled ) { @@ -1298,17 +1314,17 @@ export class BaseExecutor { let bodyString = JSON.stringify(transformedBody); const shouldFingerprint = - isCliCompatEnabled(this.provider) || + isCliCompatEnabled(fingerprintProvider) || (this.provider === "claude" && (isClaudeCodeClient || hasClaudeOAuthToken)); if (shouldFingerprint) { - const fingerprinted = applyFingerprint(this.provider, headers, transformedBody); + const fingerprinted = applyFingerprint(fingerprintProvider, headers, transformedBody); finalHeaders = fingerprinted.headers; bodyString = fingerprinted.bodyString; } // CCH signing — replaces the cch=00000 placeholder in the billing // header with an xxHash64 integrity token over the serialized body. - if (isClaudeCodeCompatible(this.provider) || this.provider === "claude") { + if (usesClaudeCodeProtocol || this.provider === "claude") { bodyString = await signRequestBody(bodyString); } @@ -1396,7 +1412,7 @@ export class BaseExecutor { contextEditingDisabled = true; delete (transformedBody as Record).context_management; let retryBody = JSON.stringify(transformedBody); - if (isClaudeCodeCompatible(this.provider) || this.provider === "claude") { + if (usesClaudeCodeProtocol || this.provider === "claude") { retryBody = await signRequestBody(retryBody); } log?.debug?.( @@ -1435,7 +1451,7 @@ export class BaseExecutor { thinkingBudgetClampedMax = upstreamMax; if (clampNestedThinkingBudget(transformedBody, upstreamMax)) { let retryBody = JSON.stringify(transformedBody); - if (isClaudeCodeCompatible(this.provider) || this.provider === "claude") { + if (usesClaudeCodeProtocol || this.provider === "claude") { retryBody = await signRequestBody(retryBody); } log?.info?.( @@ -1466,7 +1482,7 @@ export class BaseExecutor { strippedFields.add(offending); delete (transformedBody as Record)[offending]; let retryBody = JSON.stringify(transformedBody); - if (isClaudeCodeCompatible(this.provider) || this.provider === "claude") { + if (usesClaudeCodeProtocol || this.provider === "claude") { retryBody = await signRequestBody(retryBody); } log?.debug?.( @@ -1491,7 +1507,7 @@ export class BaseExecutor { addParamToBlocklist(this.provider, autoLearned, model); delete (transformedBody as Record)[autoLearned]; let retryBody = JSON.stringify(transformedBody); - if (isClaudeCodeCompatible(this.provider) || this.provider === "claude") { + if (usesClaudeCodeProtocol || this.provider === "claude") { retryBody = await signRequestBody(retryBody); } log?.info?.( diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index 3d56a5f5d2..98e53ab03c 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -55,6 +55,7 @@ import { forwardOpencodeClientHeaders } from "../utils/opencodeHeaders.ts"; import { resolveZaiUrl } from "./default/zaiFormatOverride.ts"; import { acquireNvidiaConcurrencySlot } from "./default/nvidiaConcurrencyGate.ts"; import { resolveAlibabaProviderBaseUrl } from "@/shared/constants/alibabaProviderRegions"; +import { usesCcWireImage } from "../services/ccWireImageBuiltins.ts"; import type { PoolConfig } from "../services/sessionPool/types.ts"; @@ -291,6 +292,10 @@ export class DefaultExecutor extends BaseExecutor { case "minimax": case "minimax-cn": return `${this.config.baseUrl}?beta=true`; + case "agentrouter": + return this.usesClaudeCodeProtocol(credentials) + ? `${this.config.baseUrl}?beta=true` + : this.config.baseUrl; case "gemini": return `${this.config.baseUrl}/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`; default: { @@ -413,7 +418,7 @@ export class DefaultExecutor extends BaseExecutor { applyClineAuthHeaders(headers, credentials, effectiveKey, clientHeaders, false); break; default: - if (isClaudeCodeCompatible(this.provider)) { + if (this.usesClaudeCodeProtocol(credentials)) { const ccRequestDefaults = getClaudeCodeCompatibleRequestDefaults( credentials?.providerSpecificData ); @@ -423,6 +428,10 @@ export class DefaultExecutor extends BaseExecutor { credentials?.providerSpecificData?.ccSessionId, { redactThinking: ccRequestDefaults.redactThinking === true } ); + if (usesCcWireImage(this.provider)) { + delete ccHeaders["Authorization"]; + ccHeaders["x-api-key"] = effectiveKey || credentials.accessToken || ""; + } // CC nodes are also anthropic-compatible-*, so honor operator custom // headers here (the early return skips the shared block below). applyCustomHeaders(ccHeaders, credentials.providerSpecificData?.customHeaders); diff --git a/src/shared/constants/claudeCodeClient.ts b/src/shared/constants/claudeCodeClient.ts index 0dfa308856..415068bc2d 100644 --- a/src/shared/constants/claudeCodeClient.ts +++ b/src/shared/constants/claudeCodeClient.ts @@ -4,8 +4,8 @@ * Keep this leaf dependency-free so server executors, compatibility bridges, * and client-facing identity presets can share one source of truth. */ -export const CLAUDE_CODE_CLIENT_VERSION = "2.1.219"; -export const CLAUDE_CODE_CLIENT_BUILD_REVISION = "250"; +export const CLAUDE_CODE_CLIENT_VERSION = "2.1.220"; +export const CLAUDE_CODE_CLIENT_BUILD_REVISION = "1f2"; export const CLAUDE_CODE_CLIENT_BILLING_VERSION = `${CLAUDE_CODE_CLIENT_VERSION}.${CLAUDE_CODE_CLIENT_BUILD_REVISION}`; export const CLAUDE_CODE_SDK_PACKAGE_VERSION = "0.94.0"; export const CLAUDE_CODE_RUNTIME_VERSION = "v26.3.0"; diff --git a/src/shared/constants/clientIdentityProfiles.ts b/src/shared/constants/clientIdentityProfiles.ts index 3798ff04c2..35770fae17 100644 --- a/src/shared/constants/clientIdentityProfiles.ts +++ b/src/shared/constants/clientIdentityProfiles.ts @@ -15,6 +15,7 @@ */ import { getClaudeCodeUserAgent } from "./claudeCodeClient"; +import { getCodexCliRsHeaders } from "./codexClient"; export interface ClientIdentityProfile { readonly id: string; @@ -40,10 +41,7 @@ const CLAUDE_CLI_PROFILE: ClientIdentityProfile = Object.freeze({ const CODEX_CLI_PROFILE: ClientIdentityProfile = Object.freeze({ id: "codex-cli", label: "Codex CLI", - headers: Object.freeze({ - "User-Agent": "codex_cli_rs/0.144.1", - originator: "codex_cli_rs", - }), + headers: Object.freeze(getCodexCliRsHeaders()), }); const GEMINI_CLI_PROFILE: ClientIdentityProfile = Object.freeze({ diff --git a/src/shared/constants/codexClient.ts b/src/shared/constants/codexClient.ts new file mode 100644 index 0000000000..af303d0947 --- /dev/null +++ b/src/shared/constants/codexClient.ts @@ -0,0 +1,11 @@ +export const DEFAULT_CODEX_CLIENT_VERSION = "0.146.0"; +export const CODEX_CLI_RS_ORIGINATOR = "codex_cli_rs"; + +export function getCodexCliRsHeaders( + version = DEFAULT_CODEX_CLIENT_VERSION +): Record { + return { + "User-Agent": `${CODEX_CLI_RS_ORIGINATOR}/${version}`, + originator: CODEX_CLI_RS_ORIGINATOR, + }; +} diff --git a/tests/unit/agentrouter-cc-wire-image.test.ts b/tests/unit/agentrouter-cc-wire-image.test.ts index 1cc334990d..ad6d824535 100644 --- a/tests/unit/agentrouter-cc-wire-image.test.ts +++ b/tests/unit/agentrouter-cc-wire-image.test.ts @@ -56,7 +56,7 @@ test("(b) applyFingerprint selects the claude-code-compatible fingerprint for ag const { headers } = applyFingerprint( "agentrouter", buildProviderHeaders("agentrouter", { apiKey: "sk-agentrouter" }, true), - { model: "claude-opus-4-6", messages: [] } + { model: "claude-opus-4-8", messages: [] } ); // Fingerprint reordering keeps the CC wire image + the preserved x-api-key auth. assert.equal(headers["x-api-key"], "sk-agentrouter"); @@ -70,7 +70,7 @@ test("(c) CRUX: agentrouter keeps its OWN x-api-key auth (NOT CC Bearer)", () => }); test("(c) CRUX: agentrouter keeps its OWN registry baseUrl + ?beta=true", () => { - const url = buildProviderUrl("agentrouter", "claude-opus-4-6", true); + const url = buildProviderUrl("agentrouter", "claude-opus-4-8", true); assert.equal(url, "https://agentrouter.org/v1/messages?beta=true"); // NOT the CC-family anthropic default baseUrl. assert.ok(!url.includes("api.anthropic.com")); @@ -78,11 +78,7 @@ test("(c) CRUX: agentrouter keeps its OWN registry baseUrl + ?beta=true", () => test("(c) real CC-family provider still uses the CC default baseUrl + Bearer auth", () => { // The wire-image guard must NOT leak into genuine anthropic-compatible-cc-* providers. - const headers = buildProviderHeaders( - "anthropic-compatible-cc-foo", - { apiKey: "sk-foo" }, - true - ); + const headers = buildProviderHeaders("anthropic-compatible-cc-foo", { apiKey: "sk-foo" }, true); assert.equal(headers["Authorization"], "Bearer sk-foo"); assert.equal(headers["x-api-key"], undefined); diff --git a/tests/unit/agentrouter-executor-protocols.test.ts b/tests/unit/agentrouter-executor-protocols.test.ts new file mode 100644 index 0000000000..7b7befdf0a --- /dev/null +++ b/tests/unit/agentrouter-executor-protocols.test.ts @@ -0,0 +1,139 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { DefaultExecutor } from "../../open-sse/executors/default.ts"; +import { getClaudeCodeUserAgent } from "../../src/shared/constants/claudeCodeClient.ts"; + +test("AgentRouter default dispatch uses the Claude Code wire image and x-api-key auth", async () => { + const originalFetch = globalThis.fetch; + let captured: { url: string; headers: Headers; body: string } | null = null; + + globalThis.fetch = async (url, init = {}) => { + captured = { + url: String(url), + headers: new Headers(init.headers), + body: String(init.body || ""), + }; + return new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }; + + try { + const executor = new DefaultExecutor("agentrouter"); + await executor.execute({ + model: "claude-opus-4-8", + body: { + model: "claude-opus-4-8", + messages: [{ role: "user", content: "Reply only OK" }], + max_tokens: 8, + }, + stream: false, + credentials: { apiKey: "test-agentrouter-key" }, + }); + } finally { + globalThis.fetch = originalFetch; + } + + assert.ok(captured); + assert.equal(captured.url, "https://agentrouter.org/v1/messages?beta=true"); + assert.equal(captured.headers.get("x-api-key"), "test-agentrouter-key"); + assert.equal(captured.headers.get("authorization"), null); + assert.equal(captured.headers.get("user-agent"), getClaudeCodeUserAgent("cli")); + assert.equal(captured.headers.get("x-app"), "cli"); + assert.match(captured.body, /cc_version=[^;]+; cc_entrypoint=cli; cch=(?!00000)[0-9a-f]{5};/); +}); + +test("AgentRouter OpenAI Chat dispatch uses Codex identity without Claude-only behavior", async () => { + const originalFetch = globalThis.fetch; + let captured: { url: string; headers: Headers; body: Record } | null = null; + + globalThis.fetch = async (url, init = {}) => { + captured = { + url: String(url), + headers: new Headers(init.headers), + body: JSON.parse(String(init.body || "{}")), + }; + return new Response(JSON.stringify({ choices: [] }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }; + + try { + const executor = new DefaultExecutor("agentrouter"); + await executor.execute({ + model: "gpt-5.6-sol", + body: { + model: "gpt-5.6-sol", + messages: [{ role: "user", content: "Reply only OK" }], + max_completion_tokens: 8, + }, + stream: false, + credentials: { + apiKey: "test-agentrouter-key", + providerSpecificData: { targetFormat: "openai" }, + }, + }); + } finally { + globalThis.fetch = originalFetch; + } + + assert.ok(captured); + assert.equal(captured.url, "https://agentrouter.org/v1/chat/completions"); + assert.equal(captured.headers.get("authorization"), "Bearer test-agentrouter-key"); + assert.equal(captured.headers.get("x-api-key"), null); + assert.equal(captured.headers.get("user-agent"), "codex_cli_rs/0.146.0"); + assert.equal(captured.headers.get("originator"), "codex_cli_rs"); + assert.equal(captured.headers.get("x-app"), null); + assert.equal(captured.headers.get("anthropic-version"), null); + assert.equal(captured.body.messages instanceof Array, true); + assert.doesNotMatch(JSON.stringify(captured.body), /x-anthropic-billing-header|cc_version=/); +}); + +test("AgentRouter OpenAI Responses dispatch uses the Responses endpoint and Codex identity", async () => { + const originalFetch = globalThis.fetch; + let captured: { url: string; headers: Headers; body: Record } | null = null; + + globalThis.fetch = async (url, init = {}) => { + captured = { + url: String(url), + headers: new Headers(init.headers), + body: JSON.parse(String(init.body || "{}")), + }; + return new Response(JSON.stringify({ id: "resp_1", object: "response", output: [] }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }; + + try { + const executor = new DefaultExecutor("agentrouter"); + await executor.execute({ + model: "gpt-5.6-sol", + body: { + model: "gpt-5.6-sol", + input: "Reply only OK", + max_output_tokens: 8, + }, + stream: false, + credentials: { + apiKey: "test-agentrouter-key", + providerSpecificData: { targetFormat: "openai-responses" }, + }, + }); + } finally { + globalThis.fetch = originalFetch; + } + + assert.ok(captured); + assert.equal(captured.url, "https://agentrouter.org/v1/responses"); + assert.equal(captured.headers.get("authorization"), "Bearer test-agentrouter-key"); + assert.equal(captured.headers.get("user-agent"), "codex_cli_rs/0.146.0"); + assert.equal(captured.headers.get("originator"), "codex_cli_rs"); + assert.equal(captured.headers.get("x-app"), null); + assert.equal(captured.headers.get("anthropic-beta"), null); + assert.equal(captured.body.input, "Reply only OK"); + assert.equal(captured.body.max_output_tokens, 8); +}); diff --git a/tests/unit/agentrouter-live-catalog.test.ts b/tests/unit/agentrouter-live-catalog.test.ts new file mode 100644 index 0000000000..cd68ac2184 --- /dev/null +++ b/tests/unit/agentrouter-live-catalog.test.ts @@ -0,0 +1,21 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { FREE_MODEL_BUDGETS } from "../../open-sse/config/freeModelCatalog.ts"; +import { getRegistryEntry } from "../../open-sse/config/providerRegistry.ts"; + +const LIVE_AGENTROUTER_MODEL_IDS = ["claude-opus-4-8", "claude-opus-5", "gpt-5.6-sol"]; + +test("AgentRouter registry fallback matches the authenticated live catalog", () => { + const ids = getRegistryEntry("agentrouter") + ?.models.map((model) => model.id) + .sort(); + assert.deepEqual(ids, [...LIVE_AGENTROUTER_MODEL_IDS].sort()); +}); + +test("AgentRouter free-model fallback exposes only currently routable model IDs", () => { + const ids = FREE_MODEL_BUDGETS.filter((model) => model.provider === "agentrouter") + .map((model) => model.modelId) + .sort(); + assert.deepEqual(ids, [...LIVE_AGENTROUTER_MODEL_IDS].sort()); +}); diff --git a/tests/unit/agentrouter-models-discovery-7016.test.ts b/tests/unit/agentrouter-models-discovery-7016.test.ts index 122028b7cf..1c3d8eb0e0 100644 --- a/tests/unit/agentrouter-models-discovery-7016.test.ts +++ b/tests/unit/agentrouter-models-discovery-7016.test.ts @@ -41,21 +41,18 @@ test("agentrouter discovery parseResponse maps an OpenAI-style model list", () = const models = cfg.parseResponse!({ object: "list", data: [ - { id: "claude-opus-4-6", object: "model", owned_by: "anthropic" }, - { id: "glm-5.1", object: "model", owned_by: "zhipu" }, + { id: "claude-opus-4-8", object: "model", owned_by: "anthropic" }, + { id: "gpt-5.6-sol", object: "model", owned_by: "openai" }, ], }); - assert.deepEqual( - models, - [ - { id: "claude-opus-4-6", object: "model", owned_by: "anthropic" }, - { id: "glm-5.1", object: "model", owned_by: "zhipu" }, - ] - ); + assert.deepEqual(models, [ + { id: "claude-opus-4-8", object: "model", owned_by: "anthropic" }, + { id: "gpt-5.6-sol", object: "model", owned_by: "openai" }, + ]); // Tolerant of the alternate `models` envelope shape too. - assert.deepEqual(cfg.parseResponse!({ models: [{ id: "deepseek-v3.2" }] }), [ - { id: "deepseek-v3.2" }, + assert.deepEqual(cfg.parseResponse!({ models: [{ id: "claude-opus-5" }] }), [ + { id: "claude-opus-5" }, ]); }); diff --git a/tests/unit/autoCombo/paid-model-filter-6512.test.ts b/tests/unit/autoCombo/paid-model-filter-6512.test.ts index 30423d4225..3474e69d16 100644 --- a/tests/unit/autoCombo/paid-model-filter-6512.test.ts +++ b/tests/unit/autoCombo/paid-model-filter-6512.test.ts @@ -10,9 +10,9 @@ import assert from "node:assert/strict"; import { filterPaidOnlyCandidates } from "../../../open-sse/services/autoCombo/paidModelFilter.ts"; -// `agentrouter/claude-opus-4-6` is a documented free model (FREE_MODEL_BUDGETS); +// `agentrouter/claude-opus-4-8` is a documented free model (FREE_MODEL_BUDGETS); // `openai/gpt-4o` is paid (openai has no documented free models). -const FREE = { provider: "agentrouter", model: "claude-opus-4-6" }; +const FREE = { provider: "agentrouter", model: "claude-opus-4-8" }; const PAID = { provider: "openai", model: "gpt-4o" }; test("hidePaidModels OFF (default) returns the pool UNCHANGED (identity, regression guard)", () => { @@ -27,7 +27,7 @@ test("hidePaidModels ON drops the paid-only backend, keeps the free one", () => assert.deepEqual( result, [FREE], - "openai/gpt-4o (paid) must be excluded; agentrouter/claude-opus-4-6 (free) kept" + "openai/gpt-4o (paid) must be excluded; agentrouter/claude-opus-4-8 (free) kept" ); }); @@ -37,7 +37,12 @@ test("hidePaidModels ON with an all-paid pool degrades to an empty pool", () => }); test("hidePaidModels ON preserves extra candidate fields on kept entries", () => { - const enriched = { provider: "agentrouter", model: "claude-opus-4-6", connectionId: "abc", extra: 1 }; + const enriched = { + provider: "agentrouter", + model: "claude-opus-4-8", + connectionId: "abc", + extra: 1, + }; const result = filterPaidOnlyCandidates([enriched, PAID], true); assert.deepEqual(result, [enriched], "generic filter must not strip candidate fields"); }); diff --git a/tests/unit/claude-codex-identity-version-sync.test.ts b/tests/unit/claude-codex-identity-version-sync.test.ts index 756b7faeae..848050df18 100644 --- a/tests/unit/claude-codex-identity-version-sync.test.ts +++ b/tests/unit/claude-codex-identity-version-sync.test.ts @@ -38,10 +38,10 @@ test("Claude CLI version constants are in lockstep across all 4 sources", () => ); }); -test("Claude CLI wire versions match the captured 2.1.219 binary", () => { - assert.equal(canonical.CLAUDE_CODE_CLIENT_VERSION, "2.1.219"); - assert.equal(canonical.CLAUDE_CODE_CLIENT_BUILD_REVISION, "250"); - assert.equal(canonical.CLAUDE_CODE_CLIENT_BILLING_VERSION, "2.1.219.250"); +test("Claude CLI wire versions match the captured 2.1.220 binary", () => { + assert.equal(canonical.CLAUDE_CODE_CLIENT_VERSION, "2.1.220"); + assert.equal(canonical.CLAUDE_CODE_CLIENT_BUILD_REVISION, "1f2"); + assert.equal(canonical.CLAUDE_CODE_CLIENT_BILLING_VERSION, "2.1.220.1f2"); assert.equal(canonical.CLAUDE_CODE_SDK_PACKAGE_VERSION, "0.94.0"); assert.equal(canonical.CLAUDE_CODE_RUNTIME_VERSION, "v26.3.0"); assert.equal( @@ -57,8 +57,9 @@ test("Claude CLI wire versions match the captured 2.1.219 binary", () => { assert.equal(hdr.CLAUDE_CLI_BILLING_VERSION, canonical.CLAUDE_CODE_CLIENT_BILLING_VERSION); }); -test("Codex client is pinned to the captured 0.144.1 release", () => { - assert.equal(codexCfg.getCodexClientVersion(), "0.144.1"); - assert.equal(codexCfg.getCodexUserAgent(), "codex-cli/0.144.1 (Windows 10.0.26200; x64)"); - assert.equal(codexCfg.getCodexDefaultHeaders().Version, "0.144.1"); +test("Codex client is pinned to the captured 0.146.0 release", () => { + assert.equal(codexCfg.getCodexClientVersion(), "0.146.0"); + assert.equal(codexCfg.getCodexUserAgent(), "codex-cli/0.146.0 (Windows 10.0.26200; x64)"); + assert.equal(codexCfg.getCodexDefaultHeaders().Version, "0.146.0"); + assert.equal(codexCfg.getCodexCliRsHeaders()["User-Agent"], "codex_cli_rs/0.146.0"); }); diff --git a/tests/unit/client-identity-profiles.test.ts b/tests/unit/client-identity-profiles.test.ts index d592e68a61..c169df22e8 100644 --- a/tests/unit/client-identity-profiles.test.ts +++ b/tests/unit/client-identity-profiles.test.ts @@ -39,11 +39,11 @@ test("getClientIdentityProfileHeaders: unknown profile id falls back to no heade test("getClientIdentityProfileHeaders: known CLI profiles expose their preset headers", () => { const claudeCli = getClientIdentityProfileHeaders("claude-cli"); - assert.equal(claudeCli["User-Agent"], "claude-cli/2.1.219 (external, cli)"); + assert.equal(claudeCli["User-Agent"], "claude-cli/2.1.220 (external, cli)"); assert.equal(claudeCli["X-App"], "cli"); const codexCli = getClientIdentityProfileHeaders("codex-cli"); - assert.equal(codexCli["User-Agent"], "codex_cli_rs/0.144.1"); + assert.equal(codexCli["User-Agent"], "codex_cli_rs/0.146.0"); assert.equal(codexCli.originator, "codex_cli_rs"); const geminiCli = getClientIdentityProfileHeaders("gemini-cli"); @@ -55,7 +55,7 @@ test("getClientIdentityProfileHeaders: returns a fresh mutable copy (catalog sta headers["User-Agent"] = "tampered"; assert.equal( CLIENT_IDENTITY_PROFILES["claude-cli"].headers["User-Agent"], - "claude-cli/2.1.219 (external, cli)" + "claude-cli/2.1.220 (external, cli)" ); }); @@ -80,7 +80,7 @@ test("a selected profile's headers land in providerSpecificData.customHeaders", customHeaders: { ...profileHeaders, "X-Operator-Set": "keep-me" }, }; - assert.equal(providerSpecificData.customHeaders["User-Agent"], "codex_cli_rs/0.144.1"); + assert.equal(providerSpecificData.customHeaders["User-Agent"], "codex_cli_rs/0.146.0"); assert.equal(providerSpecificData.customHeaders.originator, "codex_cli_rs"); assert.equal(providerSpecificData.customHeaders["X-Operator-Set"], "keep-me"); }); @@ -100,7 +100,7 @@ test("profile headers merged into customHeaders survive applyCustomHeaders sanit true ) as Record; - assert.equal(headers["User-Agent"], "claude-cli/2.1.219 (external, cli)"); + assert.equal(headers["User-Agent"], "claude-cli/2.1.220 (external, cli)"); assert.equal(headers["X-App"], "cli"); assert.equal(headers["Authorization"], "Bearer test-key"); });