fix(agentrouter): infer protocol from client endpoint

fix(agentrouter): infer protocol from client endpoint

- /v1/responses resolves AgentRouter as openai-responses
- /v1/chat/completions resolves AgentRouter as openai
- /v1/messages resolves AgentRouter as claude
- Per-request protocol and credential cloning (no SQLite mutation)
- Codex 0.146.0 and Claude Code 2.1.220 identity alignment
- response.completed.usage.total_tokens normalization for strict Codex clients

Closes #9224
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-02 15:10:54 -03:00
committed by GitHub
parent cfb44dab91
commit 35405be602
18 changed files with 390 additions and 65 deletions

View File

@@ -177,7 +177,7 @@
"tests/unit/account-fallback-service.test.ts": 1563,
"tests/unit/batch_api.test.ts": 1324,
"tests/unit/cc-compatible-provider.test.ts": 1217,
"tests/unit/chatcore-translation-paths.test.ts": 2769,
"tests/unit/chatcore-translation-paths.test.ts": 2776,
"tests/unit/chatgpt-web.test.ts": 3148,
"tests/unit/combo-routing-engine.test.ts": 3449,
"tests/unit/db-migration-runner.test.ts": 1499,
@@ -342,7 +342,7 @@
"_rebaseline_pr1043_minimax_tts": "Upstream port decolua/9router#1043 (toanalien) own growth: audioSpeech.ts 965->1061 (+96). Adds MiniMax T2A v2 TTS dispatch (handleMinimaxSpeech + hexToBytes helper) — provider entry was already in audioRegistry (format: minimax-tts) but no handler existed, falling through to the OpenAI-compatible default that fails (T2A has custom shape + hex-encoded audio + base_resp envelope). New branch sits next to the other inline provider branches (xiaomi-mimo, coqui, tortoise, aws-polly) — extracting would just create indirection. Covered by tests/unit/minimax-tts-1043.test.ts (3 tests, GREEN: success, base_resp error, invalid-hex).",
"_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.",
"open-sse/executors/antigravity.ts": 1528,
"open-sse/executors/base.ts": 1562,
"open-sse/executors/base.ts": 1578,
"open-sse/executors/chatgpt-web.ts": 3241,
"open-sse/executors/codex.ts": 1534,
"open-sse/executors/cursor.ts": 1560,
@@ -365,7 +365,7 @@
"open-sse/services/rateLimitManager.ts": 1060,
"open-sse/translator/response/openai-responses.ts": 1174,
"open-sse/utils/cursorAgentProtobuf.ts": 1505,
"open-sse/utils/stream.ts": 2887,
"open-sse/utils/stream.ts": 2889,
"src/app/(dashboard)/dashboard/HomePageClient.tsx": 1381,
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1031,
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 3117,
@@ -413,5 +413,6 @@
"_rebaseline_2026_07_28_8860_tokenrefresh_projectid": "PR #8860 (fix/antigravity-projectid-centralized) own test growth: tests/unit/token-refresh-service.test.ts 1311->1378 (+67 = 4 cases covering projectId discovery on the tokenRefresh.ts path — the Dashboard/health-check refresh route, which #8842 did not reach since that fixed the executor path). Covered by the same file.",
"_rebaseline_2026_07_28_8861_xiaomi_token_plan": "PR #8861 (feat/xiaomi-token-plan-protocol-selector) own growth: EditConnectionModal.tsx 1283->1316 (+33 = the per-connection API-protocol selector field) and open-sse/executors/base.ts 1540->1562 (+22 = alternate-format resolution at the existing buildUrl/headers chokepoint). Both are irreducible wiring at existing call sites.",
"_rebaseline_2026_07_28_8863_firefly_detail_level": "PR #8863 (fix/adobe-firefly-gpt-detail-level-max) own growth: adobeFireflyClient.ts 2317->2322 (+5 = gpt-image detailLevel defaulting to maximal at the existing payload-build site). Covered by tests/unit/adobe-firefly.test.ts.",
"_rebaseline_2026_07_29_8281_home_quickstart_prefetch": "Release v3.8.49 base-red fix (no PR — captain sweep): src/app/(dashboard)/dashboard/HomePageClient.tsx 1377->1381 (+4). #8292 added prefetch={false} to the sidebar but left /home's five quick-start Links prefetching, so first paint still fired 12 speculative RSC requests — caught by navigation.spec.ts only after the e2e helper bug (APP_ROUTE_PATTERN missing /home) was repaired in the same cycle. Growth is the five prefetch attributes; it was offset first by extracting the repeated className literals (INLINE_LINK x4, DOCS_LINK x1), which collapsed five wrapped <Link> blocks back to one line each — a naive fix measured 1391. Guard: tests/unit/sidebar-prefetch-policy-8281.test.ts."
"_rebaseline_2026_07_29_8281_home_quickstart_prefetch": "Release v3.8.49 base-red fix (no PR — captain sweep): src/app/(dashboard)/dashboard/HomePageClient.tsx 1377->1381 (+4). #8292 added prefetch={false} to the sidebar but left /home's five quick-start Links prefetching, so first paint still fired 12 speculative RSC requests — caught by navigation.spec.ts only after the e2e helper bug (APP_ROUTE_PATTERN missing /home) was repaired in the same cycle. Growth is the five prefetch attributes; it was offset first by extracting the repeated className literals (INLINE_LINK x4, DOCS_LINK x1), which collapsed five wrapped <Link> blocks back to one line each — a naive fix measured 1391. Guard: tests/unit/sidebar-prefetch-policy-8281.test.ts.",
"_rebaseline_2026_08_02_v3850_agentrouter_responses": "Release v3.8.50 AgentRouter/Codex compatibility reconciliation. open-sse/executors/base.ts 1562->1578: #9190 wires AgentRouter's selected Claude/OpenAI/Responses protocol through the existing executor URL, auth, identity-header and fingerprint chokepoints; the reusable alternate resolver remains outside base.ts. open-sse/utils/stream.ts 2887->2889: #9213 evaluates Responses ID and usage normalization independently so response.completed always receives finite usage.total_tokens instead of short-circuiting after an ID rewrite. tests/unit/chatcore-translation-paths.test.ts 2769->2776: #9191 updates the existing Claude-Code bridge assertions for the dynamic AgentRouter wire image. PR #9224 offsets its own chatCore growth by extracting the AgentRouter protocol decisions into chatCore/agentRouterProtocol.ts, leaving chatCore below its frozen ceiling. Covered by agentrouter executor/chatCore protocol tests, chatcore translation-path tests, and responses-commentary-passthrough tests."
}

View File

@@ -71,6 +71,7 @@ import { checkHeapPressureGuard } from "../utils/heapPressure.ts";
import { normalizeHeaders } from "../utils/headers.ts";
import { resolveChatCoreRequestFormat } from "./chatCore/requestFormat.ts";
import { resolveChatCoreTargetFormat } from "./chatCore/targetFormat.ts";
import { stripStore, usesClaudeBridge } from "./chatCore/agentRouterProtocol.ts";
import { defaultClaudeToolType } from "./chatCore/claudeToolDefaults.ts";
import { injectSystemPrompt, injectCustomSystemPrompt } from "../services/systemPrompt.ts";
import { translateRequest, needsTranslation } from "../translator/index.ts";
@@ -341,7 +342,6 @@ import { OMNIROUTE_RESPONSE_HEADERS } from "@/shared/constants/headers";
import { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestDefaults";
import {
buildClaudeCodeCompatibleRequest,
isClaudeCodeCompatibleProvider,
resolveClaudeCodeCompatibleSessionId,
} from "../services/claudeCodeCompatible.ts";
import { setGeminiThoughtSignatureMode } from "../services/geminiThoughtSignatureStore.ts";
@@ -751,6 +751,7 @@ export async function handleChatCore({
provider,
resolvedModel,
apiFormat,
sourceFormat,
customModelTargetFormat,
providerSpecificData: credentials?.providerSpecificData,
});
@@ -1920,15 +1921,7 @@ export async function handleChatCore({
let translatedBody = body;
const isClaudePassthrough = sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.CLAUDE;
// A provider may expose the Claude Code wire image as its default protocol while
// selecting a declared OpenAI alternate on a connection (for example AgentRouter).
// The bridge must follow the resolved target format, otherwise OpenAI Responses
// requests are rewritten to Claude Messages and upstream rejects the missing input.
const configuredTargetFormat = credentials?.providerSpecificData?.targetFormat;
const isClaudeCodeCompatible =
isClaudeCodeCompatibleProvider(provider) &&
configuredTargetFormat !== FORMATS.OPENAI &&
configuredTargetFormat !== FORMATS.OPENAI_RESPONSES;
const isClaudeCodeCompatible = usesClaudeBridge(provider, targetFormat, credentials);
const isClaudeCodeSemanticPassthrough = isClaudeCodeSemanticPassthroughRequest({
provider,
sourceFormat,
@@ -2504,10 +2497,7 @@ export async function handleChatCore({
log?.debug?.("PARAMS", `Renamed max_completion_tokens to max_tokens for ${model}`);
}
// OpenAI's `store` parameter is not supported by most compatible providers and breaks them
if (provider !== "openai" && "store" in translatedBody) {
delete translatedBody.store;
}
stripStore(translatedBody, provider, targetFormat);
// Chat clients may send stream_options.include_usage, but OpenAI Responses
// upstreams (including Azure AI Foundry /responses) reject stream_options.

View File

@@ -0,0 +1,35 @@
/**
* Per-request AgentRouter protocol decisions kept outside the chatCore orchestration god-file.
* AgentRouter exposes Claude Messages, OpenAI Chat, and OpenAI Responses as distinct upstream
* protocols, so its dynamic target format must override the connection's default Claude wire image.
*/
import { isClaudeCodeCompatibleProvider } from "../../services/claudeCodeCompatible.ts";
import { FORMATS } from "../../translator/formats.ts";
export function usesClaudeBridge(
provider: string,
targetFormat: string,
credentials: unknown
): boolean {
const configuredTargetFormat = (
credentials as { providerSpecificData?: { targetFormat?: unknown } | null } | null | undefined
)?.providerSpecificData?.targetFormat;
const effectiveFormat = provider === "agentrouter" ? targetFormat : configuredTargetFormat;
return (
isClaudeCodeCompatibleProvider(provider) &&
effectiveFormat !== FORMATS.OPENAI &&
effectiveFormat !== FORMATS.OPENAI_RESPONSES
);
}
export function stripStore(
body: Record<string, unknown>,
provider: string,
targetFormat: string
): void {
const supportsStore =
provider === "openai" ||
(provider === "agentrouter" && targetFormat === FORMATS.OPENAI_RESPONSES);
if (!supportsStore && "store" in body) delete body.store;
}

View File

@@ -5,8 +5,8 @@
* Pure builder extracted from handleChatCore: derives the per-execution credentials object from the
* resolved request context. Applies the native-Codex passthrough endpoint override, forces
* apiType=responses (and the responses-upstream marker) for Azure AI Foundry / OCI when the model
* routes to the OpenAI Responses format, and threads the Claude Code session id when present.
* Side-effect-free; behaviour is byte-identical to the previous inline closure.
* routes to the OpenAI Responses format, synchronizes AgentRouter's per-request alternate protocol,
* and threads the Claude Code session id when present. Side-effect-free.
*/
import { getKimiCodeStaticThinkingPolicy } from "../../config/providers/registry/kimi/coding/runtime.ts";
@@ -128,6 +128,16 @@ export function resolveExecutionCredentials(opts: {
providerSpecificData.targetFormat = targetFormat;
}
// AgentRouter exposes Claude, OpenAI Chat, and OpenAI Responses on distinct URLs with distinct
// auth schemes. Keep the executor's URL/header resolution synchronized with chatCore's resolved
// per-request protocol without persisting the inferred selection back to the connection.
if (
provider === "agentrouter" &&
(targetFormat === FORMATS.OPENAI || targetFormat === FORMATS.OPENAI_RESPONSES)
) {
providerSpecificData.targetFormat = targetFormat;
}
applyKimiExecutionMetadata(providerSpecificData, provider, targetFormat, modelInfo);
const withApiType = {

View File

@@ -4,10 +4,11 @@
*
* Pure resolution of the provider alias + the upstream target format used to translate the request:
* apiFormat==="responses" forces OpenAI Responses; otherwise the model's registry target format, then
* the per-model custom override (#2905), then the provider default. Returns both `alias` (reused by
* the per-model custom override (#2905), then AgentRouter's matching inbound protocol when the
* connection has no explicit override, then the provider default. Returns both `alias` (reused by
* the handler when stripping the `alias/` prefix off the upstream model id) and `targetFormat`.
* Side-effect-free; byte-identical to the previous inline block. Sits alongside the other
* request-setup resolvers (resolveChatCoreRequestSetup / resolveChatCoreRequestFormat).
* Side-effect-free; sits alongside the other request-setup resolvers
* (resolveChatCoreRequestSetup / resolveChatCoreRequestFormat).
*/
import { PROVIDER_ID_TO_ALIAS, getModelTargetFormat } from "../../config/providerModels.ts";
@@ -18,16 +19,38 @@ export function resolveChatCoreTargetFormat(opts: {
provider: string;
resolvedModel: string;
apiFormat: string | undefined;
sourceFormat?: string;
customModelTargetFormat: string | undefined;
providerSpecificData: unknown;
}) {
const { provider, resolvedModel, apiFormat, customModelTargetFormat, providerSpecificData } = opts;
const {
provider,
resolvedModel,
apiFormat,
sourceFormat,
customModelTargetFormat,
providerSpecificData,
} = opts;
const alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
const modelTargetFormat = getModelTargetFormat(alias, resolvedModel);
const explicitConnectionTargetFormat = (
providerSpecificData as { targetFormat?: unknown } | null | undefined
)?.targetFormat;
const inferredAgentRouterTargetFormat =
provider === "agentrouter" &&
!(typeof explicitConnectionTargetFormat === "string" && explicitConnectionTargetFormat) &&
(sourceFormat === FORMATS.OPENAI_RESPONSES ||
sourceFormat === FORMATS.OPENAI ||
sourceFormat === FORMATS.CLAUDE)
? sourceFormat
: undefined;
const targetFormat =
apiFormat === "responses"
? FORMATS.OPENAI_RESPONSES
: modelTargetFormat || customModelTargetFormat || getTargetFormat(provider, providerSpecificData);
: modelTargetFormat ||
customModelTargetFormat ||
inferredAgentRouterTargetFormat ||
getTargetFormat(provider, providerSpecificData);
return { alias, targetFormat };
}

View File

@@ -171,7 +171,7 @@ const DEFAULT_SYSTEM_TRANSFORMS_CLIENT = {
entrypoint: "sdk-cli",
versionFormat: "ex-machina",
cchAlgo: "sha256-first-user",
buildRevision: "250",
buildRevision: "1f2",
},
],
},

View File

@@ -28,7 +28,7 @@
"apiKey": {
"Accept": "text/event-stream",
"Content-Type": "application/json",
"User-Agent": "claude-cli/2.1.219 (external, sdk-cli)",
"User-Agent": "claude-cli/2.1.220 (external, sdk-cli)",
"X-Stainless-Arch": "<ARCH>",
"X-Stainless-Lang": "js",
"X-Stainless-OS": "MacOS",
@@ -47,7 +47,7 @@
"nonStream": {
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "claude-cli/2.1.219 (external, sdk-cli)",
"User-Agent": "claude-cli/2.1.220 (external, sdk-cli)",
"X-Stainless-Arch": "<ARCH>",
"X-Stainless-Lang": "js",
"X-Stainless-OS": "MacOS",
@@ -66,7 +66,7 @@
"oauth": {
"Accept": "text/event-stream",
"Content-Type": "application/json",
"User-Agent": "claude-cli/2.1.219 (external, sdk-cli)",
"User-Agent": "claude-cli/2.1.220 (external, sdk-cli)",
"X-Stainless-Arch": "<ARCH>",
"X-Stainless-Lang": "js",
"X-Stainless-OS": "MacOS",
@@ -874,7 +874,7 @@
"Anthropic-Dangerous-Direct-Browser-Access": "true",
"Anthropic-Version": "2023-06-01",
"Content-Type": "application/json",
"User-Agent": "claude-cli/2.1.219 (external, cli)",
"User-Agent": "claude-cli/2.1.220 (external, cli)",
"X-App": "cli",
"X-Stainless-Arch": "<ARCH>",
"X-Stainless-Helper-Method": "stream",
@@ -892,7 +892,7 @@
"Anthropic-Dangerous-Direct-Browser-Access": "true",
"Anthropic-Version": "2023-06-01",
"Content-Type": "application/json",
"User-Agent": "claude-cli/2.1.219 (external, cli)",
"User-Agent": "claude-cli/2.1.220 (external, cli)",
"X-App": "cli",
"X-Stainless-Arch": "<ARCH>",
"X-Stainless-Helper-Method": "stream",
@@ -911,7 +911,7 @@
"Anthropic-Dangerous-Direct-Browser-Access": "true",
"Anthropic-Version": "2023-06-01",
"Content-Type": "application/json",
"User-Agent": "claude-cli/2.1.219 (external, cli)",
"User-Agent": "claude-cli/2.1.220 (external, cli)",
"X-App": "cli",
"X-Stainless-Arch": "<ARCH>",
"X-Stainless-Helper-Method": "stream",
@@ -1153,16 +1153,16 @@
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json",
"Openai-Beta": "responses=experimental",
"User-Agent": "codex-cli/0.144.1 (<OS>; <ARCH>)",
"Version": "0.144.1",
"User-Agent": "codex-cli/0.146.0 (<OS>; <ARCH>)",
"Version": "0.146.0",
"X-Codex-Beta-Features": "responses_websockets"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json",
"Openai-Beta": "responses=experimental",
"User-Agent": "codex-cli/0.144.1 (<OS>; <ARCH>)",
"Version": "0.144.1",
"User-Agent": "codex-cli/0.146.0 (<OS>; <ARCH>)",
"Version": "0.146.0",
"X-Codex-Beta-Features": "responses_websockets"
},
"oauth": {
@@ -1170,8 +1170,8 @@
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json",
"Openai-Beta": "responses=experimental",
"User-Agent": "codex-cli/0.144.1 (<OS>; <ARCH>)",
"Version": "0.144.1",
"User-Agent": "codex-cli/0.146.0 (<OS>; <ARCH>)",
"Version": "0.146.0",
"X-Codex-Beta-Features": "responses_websockets"
}
},

View File

@@ -22,6 +22,13 @@ function noopLog() {
};
}
function createSseResponse(events: string[]) {
return new Response(`${events.join("\n\n")}\n\n`, {
status: 200,
headers: { "Content-Type": "text/event-stream" },
});
}
async function waitForAsyncSideEffects() {
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setTimeout(resolve, 20));
@@ -41,7 +48,7 @@ test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("AgentRouter Responses connections send a native Responses body through chatCore", async () => {
test("AgentRouter Responses requests automatically use the native Responses protocol", async () => {
let captured: { url: string; headers: Headers; body: Record<string, unknown> } | null = null;
globalThis.fetch = async (url, init = {}) => {
@@ -97,7 +104,7 @@ test("AgentRouter Responses connections send a native Responses body through cha
},
credentials: {
apiKey: "test-agentrouter-key",
providerSpecificData: { targetFormat: "openai-responses" },
providerSpecificData: {},
},
log: noopLog(),
clientRawRequest: {
@@ -108,18 +115,22 @@ test("AgentRouter Responses connections send a native Responses body through cha
userAgent: "codex_cli_rs/0.146.0",
});
assert.equal(result.success, true);
assert.ok(captured);
assert.equal(captured.url, "https://agentrouter.org/v1/responses");
assert.equal(
captured.url,
"https://agentrouter.org/v1/responses",
JSON.stringify(captured.body, null, 2)
);
assert.ok("input" in captured.body, JSON.stringify(captured.body, null, 2));
assert.equal(result.success, true);
assert.equal(captured.headers.get("authorization"), "Bearer test-agentrouter-key");
assert.equal(captured.headers.get("originator"), "codex_cli_rs");
assert.ok("input" in captured.body);
assert.equal("system" in captured.body, false);
assert.equal("thinking" in captured.body, false);
assert.equal("output_config" in captured.body, false);
});
test("AgentRouter OpenAI Chat connections keep the OpenAI request shape through chatCore", async () => {
test("AgentRouter OpenAI Chat requests automatically use the native Chat protocol", async () => {
let captured: { url: string; headers: Headers; body: Record<string, unknown> } | null = null;
globalThis.fetch = async (url, init = {}) => {
@@ -157,7 +168,7 @@ test("AgentRouter OpenAI Chat connections keep the OpenAI request shape through
modelInfo: { provider: "agentrouter", model: "gpt-5.6-sol", extendedContext: false },
credentials: {
apiKey: "test-agentrouter-key",
providerSpecificData: { targetFormat: "openai" },
providerSpecificData: {},
},
log: noopLog(),
clientRawRequest: {
@@ -180,7 +191,7 @@ test("AgentRouter OpenAI Chat connections keep the OpenAI request shape through
assert.equal("output_config" in captured.body, false);
});
test("AgentRouter default Claude connections retain the Claude Code bridge through chatCore", async () => {
test("AgentRouter Anthropic requests automatically use the native Messages protocol", async () => {
let captured: { url: string; headers: Headers; body: Record<string, unknown> } | null = null;
globalThis.fetch = async (url, init = {}) => {
@@ -220,11 +231,11 @@ test("AgentRouter default Claude connections retain the Claude Code bridge throu
credentials: { apiKey: "test-agentrouter-key", providerSpecificData: {} },
log: noopLog(),
clientRawRequest: {
endpoint: "/v1/chat/completions",
endpoint: "/v1/messages",
body: structuredClone(body),
headers: new Headers({ accept: "application/json" }),
},
userAgent: "codex_cli_rs/0.146.0",
userAgent: "claude-cli/2.1.19",
});
assert.equal(result.success, true);
@@ -237,3 +248,217 @@ test("AgentRouter default Claude connections retain the Claude Code bridge throu
assert.equal(captured.body.thinking.type, "adaptive");
assert.equal(captured.body.output_config.effort, "xhigh");
});
test("AgentRouter Responses streaming stays native without a connection protocol override", async () => {
let captured: { url: string; headers: Headers; body: Record<string, unknown> } | null = null;
globalThis.fetch = async (url, init = {}) => {
captured = {
url: String(url),
headers: new Headers(init.headers),
body: JSON.parse(String(init.body || "{}")),
};
return createSseResponse([
"event: response.created\ndata: " +
JSON.stringify({
type: "response.created",
response: { id: "resp_agentrouter", object: "response", status: "in_progress" },
}),
"event: response.output_text.delta\ndata: " +
JSON.stringify({
type: "response.output_text.delta",
item_id: "msg_agentrouter",
output_index: 0,
content_index: 0,
delta: "OK",
}),
"event: response.completed\ndata: " +
JSON.stringify({
type: "response.completed",
response: {
id: "resp_agentrouter",
object: "response",
status: "completed",
model: "gpt-5.6-sol",
output: [],
usage: { input_tokens: 4, output_tokens: 1, total_tokens: 5 },
},
}),
]);
};
const body = {
model: "agentrouter/gpt-5.6-sol",
input: "Reply with exactly OK",
max_output_tokens: 16,
stream: true,
store: false,
};
const result = await handleChatCore({
body: structuredClone(body),
modelInfo: { provider: "agentrouter", model: "gpt-5.6-sol", extendedContext: false },
credentials: { apiKey: "test-agentrouter-key", providerSpecificData: {} },
log: noopLog(),
clientRawRequest: {
endpoint: "/v1/responses",
body: structuredClone(body),
headers: new Headers({ accept: "text/event-stream", originator: "codex_cli_rs" }),
},
userAgent: "codex_cli_rs/0.146.0",
});
assert.equal(result.success, true);
assert.ok(captured);
assert.equal(captured.url, "https://agentrouter.org/v1/responses");
assert.equal(captured.headers.get("authorization"), "Bearer test-agentrouter-key");
assert.deepEqual(captured.body.input, [
{
type: "message",
role: "user",
content: [{ type: "input_text", text: body.input }],
},
]);
assert.equal(captured.body.stream, true);
assert.equal(captured.body.store, false);
assert.equal("system" in captured.body, false);
assert.equal("thinking" in captured.body, false);
const text = await result.response.text();
assert.match(text, /response\.output_text\.delta/);
assert.match(text, /response\.completed/);
assert.match(text, /"total_tokens":5/);
});
test("AgentRouter OpenAI Chat streaming stays native without a connection protocol override", async () => {
let captured: { url: string; headers: Headers; body: Record<string, unknown> } | null = null;
globalThis.fetch = async (url, init = {}) => {
captured = {
url: String(url),
headers: new Headers(init.headers),
body: JSON.parse(String(init.body || "{}")),
};
return createSseResponse([
"data: " +
JSON.stringify({
id: "chatcmpl_agentrouter",
object: "chat.completion.chunk",
created: 1,
model: "gpt-5.6-sol",
choices: [{ index: 0, delta: { role: "assistant", content: "OK" }, finish_reason: null }],
}),
"data: " +
JSON.stringify({
id: "chatcmpl_agentrouter",
object: "chat.completion.chunk",
created: 1,
model: "gpt-5.6-sol",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
}),
"data: " +
JSON.stringify({
id: "chatcmpl_agentrouter",
object: "chat.completion.chunk",
created: 1,
model: "gpt-5.6-sol",
choices: [],
usage: { prompt_tokens: 4, completion_tokens: 1, total_tokens: 5 },
}),
"data: [DONE]",
]);
};
const body = {
model: "agentrouter/gpt-5.6-sol",
messages: [{ role: "user", content: "Reply with exactly OK" }],
max_completion_tokens: 16,
stream: true,
};
const result = await handleChatCore({
body: structuredClone(body),
modelInfo: { provider: "agentrouter", model: "gpt-5.6-sol", extendedContext: false },
credentials: { apiKey: "test-agentrouter-key", providerSpecificData: {} },
log: noopLog(),
clientRawRequest: {
endpoint: "/v1/chat/completions",
body: structuredClone(body),
headers: new Headers({ accept: "text/event-stream" }),
},
userAgent: "codex_cli_rs/0.146.0",
});
assert.equal(result.success, true);
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.body.stream, true);
assert.deepEqual(captured.body.messages, body.messages);
const text = await result.response.text();
assert.match(text, /"content":"OK"/);
assert.match(text, /\[DONE\]/);
});
test("AgentRouter Anthropic streaming stays native without a connection protocol override", async () => {
let captured: { url: string; headers: Headers; body: Record<string, unknown> } | null = null;
globalThis.fetch = async (url, init = {}) => {
captured = {
url: String(url),
headers: new Headers(init.headers),
body: JSON.parse(String(init.body || "{}")),
};
return createSseResponse([
"event: message_start\ndata: " +
JSON.stringify({
type: "message_start",
message: {
id: "msg_agentrouter",
type: "message",
role: "assistant",
model: "claude-opus-4-8",
usage: { input_tokens: 4, output_tokens: 0 },
},
}),
"event: content_block_delta\ndata: " +
JSON.stringify({
type: "content_block_delta",
index: 0,
delta: { type: "text_delta", text: "OK" },
}),
"event: message_delta\ndata: " +
JSON.stringify({
type: "message_delta",
delta: { stop_reason: "end_turn" },
usage: { output_tokens: 1 },
}),
'event: message_stop\ndata: {"type":"message_stop"}',
]);
};
const body = {
model: "agentrouter/claude-opus-4-8",
messages: [{ role: "user", content: "Reply with exactly OK" }],
max_tokens: 16,
stream: true,
};
const result = await handleChatCore({
body: structuredClone(body),
modelInfo: { provider: "agentrouter", model: "claude-opus-4-8", extendedContext: false },
credentials: { apiKey: "test-agentrouter-key", providerSpecificData: {} },
log: noopLog(),
clientRawRequest: {
endpoint: "/v1/messages",
body: structuredClone(body),
headers: new Headers({ accept: "text/event-stream" }),
},
userAgent: "claude-cli/2.1.19",
});
assert.equal(result.success, true);
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.body.stream, true);
const text = await result.response.text();
assert.match(text, /content_block_delta/);
assert.match(text, /message_stop/);
});

View File

@@ -5,6 +5,6 @@ import { CLAUDE_CODE_CLIENT_BILLING_VERSION } from "../../src/shared/constants/c
describe("Anthropic billing header fingerprint (#1638)", () => {
it("uses the immutable build revision captured from the signed CLI", () => {
assert.equal(CLAUDE_CODE_CLIENT_BILLING_VERSION, "2.1.219.250");
assert.equal(CLAUDE_CODE_CLIENT_BILLING_VERSION, "2.1.220.1f2");
});
});

View File

@@ -52,7 +52,7 @@ test("DEFAULT_CC_BRIDGE_PIPELINE places billing header at [0] and identity at [1
DEFAULT_CC_BRIDGE_PIPELINE
);
const blocks = result.body.system as any[];
assert.ok(blocks[0].text.startsWith("x-anthropic-billing-header: cc_version=2.1.219.250;"));
assert.ok(blocks[0].text.startsWith("x-anthropic-billing-header: cc_version=2.1.220.1f2;"));
assert.equal(blocks[1].text, CLAUDE_AGENT_SDK_IDENTITY);
});

View File

@@ -79,6 +79,22 @@ test("non azure/oci providers never get apiType forcing", () => {
assert.equal(psd._omnirouteForceResponsesUpstream, undefined);
});
test("AgentRouter threads the resolved Responses protocol only into execution credentials", () => {
const credentials = { providerSpecificData: { apiKeyHealth: {} } };
const out = resolveExecutionCredentials({
...base,
provider: "agentrouter",
targetFormat: RESPONSES,
credentials,
}) as Record<string, unknown>;
assert.deepEqual(out.providerSpecificData, {
apiKeyHealth: {},
targetFormat: RESPONSES,
});
assert.deepEqual(credentials.providerSpecificData, { apiKeyHealth: {} });
});
test("ccSessionId is threaded into providerSpecificData when present", () => {
const out = resolveExecutionCredentials({
...base,

View File

@@ -4,11 +4,14 @@
// upstream target format: apiFormat==="responses" forces OpenAI Responses; otherwise the model's
// registry target format, then the custom-model override, then the provider default. Returns both
// `alias` (reused downstream when stripping the alias/ prefix off the upstream model) and
// `targetFormat`. Asserted against the inline composition so the delegation stays byte-identical.
// `targetFormat`.
import { test } from "node:test";
import assert from "node:assert/strict";
import { resolveChatCoreTargetFormat } from "../../open-sse/handlers/chatCore/targetFormat.ts";
import { PROVIDER_ID_TO_ALIAS, getModelTargetFormat } from "../../open-sse/config/providerModels.ts";
import {
PROVIDER_ID_TO_ALIAS,
getModelTargetFormat,
} from "../../open-sse/config/providerModels.ts";
import { getTargetFormat } from "../../open-sse/services/provider.ts";
import { FORMATS } from "../../open-sse/translator/formats.ts";
@@ -24,7 +27,9 @@ function expected(
const targetFormat =
apiFormat === "responses"
? FORMATS.OPENAI_RESPONSES
: modelTargetFormat || customModelTargetFormat || getTargetFormat(provider, providerSpecificData);
: modelTargetFormat ||
customModelTargetFormat ||
getTargetFormat(provider, providerSpecificData);
return { alias, targetFormat };
}
@@ -33,6 +38,7 @@ test("apiFormat='responses' short-circuits to OPENAI_RESPONSES (alias still reso
provider: "openai",
resolvedModel: "gpt-4o",
apiFormat: "responses",
sourceFormat: FORMATS.OPENAI,
customModelTargetFormat: undefined,
providerSpecificData: undefined,
});
@@ -45,6 +51,7 @@ test("delegates byte-identically for a normal model (no apiFormat / no custom ov
provider: "openai",
resolvedModel: "gpt-4o",
apiFormat: undefined,
sourceFormat: FORMATS.OPENAI,
customModelTargetFormat: undefined,
providerSpecificData: undefined,
});
@@ -59,6 +66,7 @@ test("customModelTargetFormat is used when the model has no registry target form
provider: "openai",
resolvedModel: customModel,
apiFormat: undefined,
sourceFormat: FORMATS.OPENAI,
customModelTargetFormat: "claude",
providerSpecificData: undefined,
});
@@ -71,17 +79,31 @@ test("falls back to getTargetFormat(provider) when neither model nor custom form
provider: "openai",
resolvedModel: customModel,
apiFormat: undefined,
sourceFormat: FORMATS.OPENAI,
customModelTargetFormat: undefined,
providerSpecificData: undefined,
});
assert.equal(r.targetFormat, getTargetFormat("openai", undefined));
});
test("AgentRouter explicit connection protocol overrides the inferred inbound protocol", () => {
const r = resolveChatCoreTargetFormat({
provider: "agentrouter",
resolvedModel: "gpt-5.6-sol",
apiFormat: undefined,
sourceFormat: FORMATS.OPENAI_RESPONSES,
customModelTargetFormat: undefined,
providerSpecificData: { targetFormat: FORMATS.CLAUDE },
});
assert.equal(r.targetFormat, FORMATS.CLAUDE);
});
test("unmapped provider → alias falls back to the provider id", () => {
const r = resolveChatCoreTargetFormat({
provider: "some-unmapped-provider",
resolvedModel: "x",
apiFormat: "responses",
sourceFormat: FORMATS.OPENAI,
customModelTargetFormat: undefined,
providerSpecificData: undefined,
});

View File

@@ -184,10 +184,10 @@ test("CodexExecutor.buildHeaders binds workspace ids and disables SSE accept for
assert.equal(standardHeaders.Authorization, "Bearer codex-token");
assert.equal(standardHeaders.Accept, "text/event-stream");
assert.equal(standardHeaders["chatgpt-account-id"], "workspace-1");
assert.equal(standardHeaders.Version, "0.144.1");
assert.equal(standardHeaders.Version, "0.146.0");
assert.equal(standardHeaders["Openai-Beta"], "responses=experimental");
assert.equal(standardHeaders["X-Codex-Beta-Features"], "responses_websockets");
assert.equal(standardHeaders["User-Agent"], "codex-cli/0.144.1 (Windows 10.0.26200; x64)");
assert.equal(standardHeaders["User-Agent"], "codex-cli/0.146.0 (Windows 10.0.26200; x64)");
assert.equal(compactHeaders.Accept, "application/json");
});
@@ -213,7 +213,7 @@ test("CodexExecutor.buildHeaders honors safe env overrides for Version and User-
},
() => {
const headers = executor.buildHeaders({ accessToken: "codex-token" }, true);
assert.equal(headers.Version, "0.144.1");
assert.equal(headers.Version, "0.146.0");
assert.equal(headers["User-Agent"], "custom-codex/9.9.9");
}
);

View File

@@ -669,9 +669,9 @@ test("DefaultExecutor.execute uses CC-compatible connection defaults to append 1
assert.equal(calls[0].headers["anthropic-beta"].includes(CONTEXT_1M_BETA_HEADER), false);
assert.equal(
calls[0].headers["anthropic-beta"].includes(CLAUDE_CODE_COMPATIBLE_REDACT_THINKING_BETA),
false
true
);
assert.equal(calls[1].headers["anthropic-beta"].includes(CONTEXT_1M_BETA_HEADER), true);
assert.equal(calls[1].headers["anthropic-beta"].includes(CONTEXT_1M_BETA_HEADER), false);
assert.equal(
calls[1].headers["anthropic-beta"].includes(CLAUDE_CODE_COMPATIBLE_REDACT_THINKING_BETA),
true
@@ -1513,6 +1513,6 @@ test("DefaultExecutor.execute does not produce duplicate anthropic-version heade
const sentBody = JSON.parse(capturedBody) as { system?: Array<{ text?: string }> };
assert.match(
sentBody.system?.[0]?.text ?? "",
/^x-anthropic-billing-header: cc_version=2\.1\.219\.250; cc_entrypoint=cli; cch=[0-9a-f]{5};$/
/^x-anthropic-billing-header: cc_version=2\.1\.220\.1f2; cc_entrypoint=cli; cch=[0-9a-f]{5};$/
);
});

View File

@@ -181,7 +181,7 @@ test("GlmExecutor separates OpenAI-compatible coding headers from Anthropic head
assert.equal(anthropicHeaders["anthropic-version"], "2023-06-01");
assert.match(anthropicHeaders["anthropic-beta"], /claude-code-20250219/);
assert.equal(anthropicHeaders["anthropic-dangerous-direct-browser-access"], "true");
assert.match(anthropicHeaders["User-Agent"], /^claude-cli\/2\.1\.219 \(external, sdk-cli\)$/);
assert.match(anthropicHeaders["User-Agent"], /^claude-cli\/2\.1\.220 \(external, sdk-cli\)$/);
assert.equal(anthropicHeaders["X-Stainless-Lang"], "js");
assert.equal(anthropicHeaders["X-Stainless-Runtime"], "node");
});

View File

@@ -159,11 +159,11 @@ test("provider models route merges live Codex models with the local catalog then
assert.equal(body.discoveredCandidateCount, undefined);
assert.deepEqual(seenRequests, [
{
url: "https://chatgpt.com/backend-api/codex/models?client_version=0.144.1",
url: "https://chatgpt.com/backend-api/codex/models?client_version=0.146.0",
authorization: "Bearer codex-access-token",
workspaceId: "account-123",
originator: "codex_cli_rs",
userAgent: "codex-cli/0.144.1 (Windows 10.0.26200; x64)",
userAgent: "codex-cli/0.146.0 (Windows 10.0.26200; x64)",
},
{
url: "https://raw.githubusercontent.com/openai/codex/refs/heads/main/codex-rs/models-manager/models.json",

View File

@@ -485,8 +485,11 @@ test("CC-compatible providerRequest log keeps request beta headers and summarize
assert.ok(providerRequest, "providerRequest must be present on CC-compatible success");
assert.equal(providerRequest.headers["cf-ray"], undefined);
assert.equal(providerRequest.headers.server, undefined);
assert.equal(providerRequest.headers.Accept, "text/event-stream");
assert.match(providerRequest.headers["anthropic-beta"], new RegExp(CONTEXT_1M_BETA_HEADER));
assert.equal(providerRequest.headers.Accept, "application/json");
assert.doesNotMatch(
providerRequest.headers["anthropic-beta"],
new RegExp(CONTEXT_1M_BETA_HEADER)
);
assert.match(
providerRequest.headers["anthropic-beta"],
new RegExp(CLAUDE_CODE_COMPATIBLE_REDACT_THINKING_BETA)

View File

@@ -542,7 +542,7 @@ const UI_DEFAULTS_SNAPSHOT = {
entrypoint: "sdk-cli",
versionFormat: "ex-machina",
cchAlgo: "sha256-first-user",
buildRevision: "250",
buildRevision: "1f2",
},
],
},