mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 10:43:43 +03:00
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:
committed by
GitHub
parent
cfb44dab91
commit
35405be602
@@ -177,7 +177,7 @@
|
|||||||
"tests/unit/account-fallback-service.test.ts": 1563,
|
"tests/unit/account-fallback-service.test.ts": 1563,
|
||||||
"tests/unit/batch_api.test.ts": 1324,
|
"tests/unit/batch_api.test.ts": 1324,
|
||||||
"tests/unit/cc-compatible-provider.test.ts": 1217,
|
"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/chatgpt-web.test.ts": 3148,
|
||||||
"tests/unit/combo-routing-engine.test.ts": 3449,
|
"tests/unit/combo-routing-engine.test.ts": 3449,
|
||||||
"tests/unit/db-migration-runner.test.ts": 1499,
|
"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_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.",
|
"_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/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/chatgpt-web.ts": 3241,
|
||||||
"open-sse/executors/codex.ts": 1534,
|
"open-sse/executors/codex.ts": 1534,
|
||||||
"open-sse/executors/cursor.ts": 1560,
|
"open-sse/executors/cursor.ts": 1560,
|
||||||
@@ -365,7 +365,7 @@
|
|||||||
"open-sse/services/rateLimitManager.ts": 1060,
|
"open-sse/services/rateLimitManager.ts": 1060,
|
||||||
"open-sse/translator/response/openai-responses.ts": 1174,
|
"open-sse/translator/response/openai-responses.ts": 1174,
|
||||||
"open-sse/utils/cursorAgentProtobuf.ts": 1505,
|
"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/HomePageClient.tsx": 1381,
|
||||||
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1031,
|
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1031,
|
||||||
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 3117,
|
"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_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_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_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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ import { checkHeapPressureGuard } from "../utils/heapPressure.ts";
|
|||||||
import { normalizeHeaders } from "../utils/headers.ts";
|
import { normalizeHeaders } from "../utils/headers.ts";
|
||||||
import { resolveChatCoreRequestFormat } from "./chatCore/requestFormat.ts";
|
import { resolveChatCoreRequestFormat } from "./chatCore/requestFormat.ts";
|
||||||
import { resolveChatCoreTargetFormat } from "./chatCore/targetFormat.ts";
|
import { resolveChatCoreTargetFormat } from "./chatCore/targetFormat.ts";
|
||||||
|
import { stripStore, usesClaudeBridge } from "./chatCore/agentRouterProtocol.ts";
|
||||||
import { defaultClaudeToolType } from "./chatCore/claudeToolDefaults.ts";
|
import { defaultClaudeToolType } from "./chatCore/claudeToolDefaults.ts";
|
||||||
import { injectSystemPrompt, injectCustomSystemPrompt } from "../services/systemPrompt.ts";
|
import { injectSystemPrompt, injectCustomSystemPrompt } from "../services/systemPrompt.ts";
|
||||||
import { translateRequest, needsTranslation } from "../translator/index.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 { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestDefaults";
|
||||||
import {
|
import {
|
||||||
buildClaudeCodeCompatibleRequest,
|
buildClaudeCodeCompatibleRequest,
|
||||||
isClaudeCodeCompatibleProvider,
|
|
||||||
resolveClaudeCodeCompatibleSessionId,
|
resolveClaudeCodeCompatibleSessionId,
|
||||||
} from "../services/claudeCodeCompatible.ts";
|
} from "../services/claudeCodeCompatible.ts";
|
||||||
import { setGeminiThoughtSignatureMode } from "../services/geminiThoughtSignatureStore.ts";
|
import { setGeminiThoughtSignatureMode } from "../services/geminiThoughtSignatureStore.ts";
|
||||||
@@ -751,6 +751,7 @@ export async function handleChatCore({
|
|||||||
provider,
|
provider,
|
||||||
resolvedModel,
|
resolvedModel,
|
||||||
apiFormat,
|
apiFormat,
|
||||||
|
sourceFormat,
|
||||||
customModelTargetFormat,
|
customModelTargetFormat,
|
||||||
providerSpecificData: credentials?.providerSpecificData,
|
providerSpecificData: credentials?.providerSpecificData,
|
||||||
});
|
});
|
||||||
@@ -1920,15 +1921,7 @@ export async function handleChatCore({
|
|||||||
|
|
||||||
let translatedBody = body;
|
let translatedBody = body;
|
||||||
const isClaudePassthrough = sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.CLAUDE;
|
const isClaudePassthrough = sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.CLAUDE;
|
||||||
// A provider may expose the Claude Code wire image as its default protocol while
|
const isClaudeCodeCompatible = usesClaudeBridge(provider, targetFormat, credentials);
|
||||||
// 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 isClaudeCodeSemanticPassthrough = isClaudeCodeSemanticPassthroughRequest({
|
const isClaudeCodeSemanticPassthrough = isClaudeCodeSemanticPassthroughRequest({
|
||||||
provider,
|
provider,
|
||||||
sourceFormat,
|
sourceFormat,
|
||||||
@@ -2504,10 +2497,7 @@ export async function handleChatCore({
|
|||||||
log?.debug?.("PARAMS", `Renamed max_completion_tokens to max_tokens for ${model}`);
|
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
|
stripStore(translatedBody, provider, targetFormat);
|
||||||
if (provider !== "openai" && "store" in translatedBody) {
|
|
||||||
delete translatedBody.store;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chat clients may send stream_options.include_usage, but OpenAI Responses
|
// Chat clients may send stream_options.include_usage, but OpenAI Responses
|
||||||
// upstreams (including Azure AI Foundry /responses) reject stream_options.
|
// upstreams (including Azure AI Foundry /responses) reject stream_options.
|
||||||
|
|||||||
35
open-sse/handlers/chatCore/agentRouterProtocol.ts
Normal file
35
open-sse/handlers/chatCore/agentRouterProtocol.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
* Pure builder extracted from handleChatCore: derives the per-execution credentials object from the
|
* Pure builder extracted from handleChatCore: derives the per-execution credentials object from the
|
||||||
* resolved request context. Applies the native-Codex passthrough endpoint override, forces
|
* 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
|
* 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.
|
* routes to the OpenAI Responses format, synchronizes AgentRouter's per-request alternate protocol,
|
||||||
* Side-effect-free; behaviour is byte-identical to the previous inline closure.
|
* and threads the Claude Code session id when present. Side-effect-free.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getKimiCodeStaticThinkingPolicy } from "../../config/providers/registry/kimi/coding/runtime.ts";
|
import { getKimiCodeStaticThinkingPolicy } from "../../config/providers/registry/kimi/coding/runtime.ts";
|
||||||
@@ -128,6 +128,16 @@ export function resolveExecutionCredentials(opts: {
|
|||||||
providerSpecificData.targetFormat = targetFormat;
|
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);
|
applyKimiExecutionMetadata(providerSpecificData, provider, targetFormat, modelInfo);
|
||||||
|
|
||||||
const withApiType = {
|
const withApiType = {
|
||||||
|
|||||||
@@ -4,10 +4,11 @@
|
|||||||
*
|
*
|
||||||
* Pure resolution of the provider alias + the upstream target format used to translate the request:
|
* 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
|
* 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`.
|
* 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
|
* Side-effect-free; sits alongside the other request-setup resolvers
|
||||||
* request-setup resolvers (resolveChatCoreRequestSetup / resolveChatCoreRequestFormat).
|
* (resolveChatCoreRequestSetup / resolveChatCoreRequestFormat).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PROVIDER_ID_TO_ALIAS, getModelTargetFormat } from "../../config/providerModels.ts";
|
import { PROVIDER_ID_TO_ALIAS, getModelTargetFormat } from "../../config/providerModels.ts";
|
||||||
@@ -18,16 +19,38 @@ export function resolveChatCoreTargetFormat(opts: {
|
|||||||
provider: string;
|
provider: string;
|
||||||
resolvedModel: string;
|
resolvedModel: string;
|
||||||
apiFormat: string | undefined;
|
apiFormat: string | undefined;
|
||||||
|
sourceFormat?: string;
|
||||||
customModelTargetFormat: string | undefined;
|
customModelTargetFormat: string | undefined;
|
||||||
providerSpecificData: unknown;
|
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 alias = PROVIDER_ID_TO_ALIAS[provider] || provider;
|
||||||
const modelTargetFormat = getModelTargetFormat(alias, resolvedModel);
|
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 =
|
const targetFormat =
|
||||||
apiFormat === "responses"
|
apiFormat === "responses"
|
||||||
? FORMATS.OPENAI_RESPONSES
|
? FORMATS.OPENAI_RESPONSES
|
||||||
: modelTargetFormat || customModelTargetFormat || getTargetFormat(provider, providerSpecificData);
|
: modelTargetFormat ||
|
||||||
|
customModelTargetFormat ||
|
||||||
|
inferredAgentRouterTargetFormat ||
|
||||||
|
getTargetFormat(provider, providerSpecificData);
|
||||||
return { alias, targetFormat };
|
return { alias, targetFormat };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ const DEFAULT_SYSTEM_TRANSFORMS_CLIENT = {
|
|||||||
entrypoint: "sdk-cli",
|
entrypoint: "sdk-cli",
|
||||||
versionFormat: "ex-machina",
|
versionFormat: "ex-machina",
|
||||||
cchAlgo: "sha256-first-user",
|
cchAlgo: "sha256-first-user",
|
||||||
buildRevision: "250",
|
buildRevision: "1f2",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"apiKey": {
|
"apiKey": {
|
||||||
"Accept": "text/event-stream",
|
"Accept": "text/event-stream",
|
||||||
"Content-Type": "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-Arch": "<ARCH>",
|
||||||
"X-Stainless-Lang": "js",
|
"X-Stainless-Lang": "js",
|
||||||
"X-Stainless-OS": "MacOS",
|
"X-Stainless-OS": "MacOS",
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"nonStream": {
|
"nonStream": {
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
"Content-Type": "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-Arch": "<ARCH>",
|
||||||
"X-Stainless-Lang": "js",
|
"X-Stainless-Lang": "js",
|
||||||
"X-Stainless-OS": "MacOS",
|
"X-Stainless-OS": "MacOS",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"oauth": {
|
"oauth": {
|
||||||
"Accept": "text/event-stream",
|
"Accept": "text/event-stream",
|
||||||
"Content-Type": "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-Arch": "<ARCH>",
|
||||||
"X-Stainless-Lang": "js",
|
"X-Stainless-Lang": "js",
|
||||||
"X-Stainless-OS": "MacOS",
|
"X-Stainless-OS": "MacOS",
|
||||||
@@ -874,7 +874,7 @@
|
|||||||
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
||||||
"Anthropic-Version": "2023-06-01",
|
"Anthropic-Version": "2023-06-01",
|
||||||
"Content-Type": "application/json",
|
"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-App": "cli",
|
||||||
"X-Stainless-Arch": "<ARCH>",
|
"X-Stainless-Arch": "<ARCH>",
|
||||||
"X-Stainless-Helper-Method": "stream",
|
"X-Stainless-Helper-Method": "stream",
|
||||||
@@ -892,7 +892,7 @@
|
|||||||
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
||||||
"Anthropic-Version": "2023-06-01",
|
"Anthropic-Version": "2023-06-01",
|
||||||
"Content-Type": "application/json",
|
"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-App": "cli",
|
||||||
"X-Stainless-Arch": "<ARCH>",
|
"X-Stainless-Arch": "<ARCH>",
|
||||||
"X-Stainless-Helper-Method": "stream",
|
"X-Stainless-Helper-Method": "stream",
|
||||||
@@ -911,7 +911,7 @@
|
|||||||
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
||||||
"Anthropic-Version": "2023-06-01",
|
"Anthropic-Version": "2023-06-01",
|
||||||
"Content-Type": "application/json",
|
"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-App": "cli",
|
||||||
"X-Stainless-Arch": "<ARCH>",
|
"X-Stainless-Arch": "<ARCH>",
|
||||||
"X-Stainless-Helper-Method": "stream",
|
"X-Stainless-Helper-Method": "stream",
|
||||||
@@ -1153,16 +1153,16 @@
|
|||||||
"Authorization": "Bearer <TOK>",
|
"Authorization": "Bearer <TOK>",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Openai-Beta": "responses=experimental",
|
"Openai-Beta": "responses=experimental",
|
||||||
"User-Agent": "codex-cli/0.144.1 (<OS>; <ARCH>)",
|
"User-Agent": "codex-cli/0.146.0 (<OS>; <ARCH>)",
|
||||||
"Version": "0.144.1",
|
"Version": "0.146.0",
|
||||||
"X-Codex-Beta-Features": "responses_websockets"
|
"X-Codex-Beta-Features": "responses_websockets"
|
||||||
},
|
},
|
||||||
"nonStream": {
|
"nonStream": {
|
||||||
"Authorization": "Bearer <TOK>",
|
"Authorization": "Bearer <TOK>",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Openai-Beta": "responses=experimental",
|
"Openai-Beta": "responses=experimental",
|
||||||
"User-Agent": "codex-cli/0.144.1 (<OS>; <ARCH>)",
|
"User-Agent": "codex-cli/0.146.0 (<OS>; <ARCH>)",
|
||||||
"Version": "0.144.1",
|
"Version": "0.146.0",
|
||||||
"X-Codex-Beta-Features": "responses_websockets"
|
"X-Codex-Beta-Features": "responses_websockets"
|
||||||
},
|
},
|
||||||
"oauth": {
|
"oauth": {
|
||||||
@@ -1170,8 +1170,8 @@
|
|||||||
"Authorization": "Bearer <TOK>",
|
"Authorization": "Bearer <TOK>",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Openai-Beta": "responses=experimental",
|
"Openai-Beta": "responses=experimental",
|
||||||
"User-Agent": "codex-cli/0.144.1 (<OS>; <ARCH>)",
|
"User-Agent": "codex-cli/0.146.0 (<OS>; <ARCH>)",
|
||||||
"Version": "0.144.1",
|
"Version": "0.146.0",
|
||||||
"X-Codex-Beta-Features": "responses_websockets"
|
"X-Codex-Beta-Features": "responses_websockets"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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() {
|
async function waitForAsyncSideEffects() {
|
||||||
await new Promise((resolve) => setImmediate(resolve));
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||||
@@ -41,7 +48,7 @@ test.after(() => {
|
|||||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
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;
|
let captured: { url: string; headers: Headers; body: Record<string, unknown> } | null = null;
|
||||||
|
|
||||||
globalThis.fetch = async (url, init = {}) => {
|
globalThis.fetch = async (url, init = {}) => {
|
||||||
@@ -97,7 +104,7 @@ test("AgentRouter Responses connections send a native Responses body through cha
|
|||||||
},
|
},
|
||||||
credentials: {
|
credentials: {
|
||||||
apiKey: "test-agentrouter-key",
|
apiKey: "test-agentrouter-key",
|
||||||
providerSpecificData: { targetFormat: "openai-responses" },
|
providerSpecificData: {},
|
||||||
},
|
},
|
||||||
log: noopLog(),
|
log: noopLog(),
|
||||||
clientRawRequest: {
|
clientRawRequest: {
|
||||||
@@ -108,18 +115,22 @@ test("AgentRouter Responses connections send a native Responses body through cha
|
|||||||
userAgent: "codex_cli_rs/0.146.0",
|
userAgent: "codex_cli_rs/0.146.0",
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(result.success, true);
|
|
||||||
assert.ok(captured);
|
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("authorization"), "Bearer test-agentrouter-key");
|
||||||
assert.equal(captured.headers.get("originator"), "codex_cli_rs");
|
assert.equal(captured.headers.get("originator"), "codex_cli_rs");
|
||||||
assert.ok("input" in captured.body);
|
|
||||||
assert.equal("system" in captured.body, false);
|
assert.equal("system" in captured.body, false);
|
||||||
assert.equal("thinking" in captured.body, false);
|
assert.equal("thinking" in captured.body, false);
|
||||||
assert.equal("output_config" 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;
|
let captured: { url: string; headers: Headers; body: Record<string, unknown> } | null = null;
|
||||||
|
|
||||||
globalThis.fetch = async (url, init = {}) => {
|
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 },
|
modelInfo: { provider: "agentrouter", model: "gpt-5.6-sol", extendedContext: false },
|
||||||
credentials: {
|
credentials: {
|
||||||
apiKey: "test-agentrouter-key",
|
apiKey: "test-agentrouter-key",
|
||||||
providerSpecificData: { targetFormat: "openai" },
|
providerSpecificData: {},
|
||||||
},
|
},
|
||||||
log: noopLog(),
|
log: noopLog(),
|
||||||
clientRawRequest: {
|
clientRawRequest: {
|
||||||
@@ -180,7 +191,7 @@ test("AgentRouter OpenAI Chat connections keep the OpenAI request shape through
|
|||||||
assert.equal("output_config" in captured.body, false);
|
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;
|
let captured: { url: string; headers: Headers; body: Record<string, unknown> } | null = null;
|
||||||
|
|
||||||
globalThis.fetch = async (url, init = {}) => {
|
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: {} },
|
credentials: { apiKey: "test-agentrouter-key", providerSpecificData: {} },
|
||||||
log: noopLog(),
|
log: noopLog(),
|
||||||
clientRawRequest: {
|
clientRawRequest: {
|
||||||
endpoint: "/v1/chat/completions",
|
endpoint: "/v1/messages",
|
||||||
body: structuredClone(body),
|
body: structuredClone(body),
|
||||||
headers: new Headers({ accept: "application/json" }),
|
headers: new Headers({ accept: "application/json" }),
|
||||||
},
|
},
|
||||||
userAgent: "codex_cli_rs/0.146.0",
|
userAgent: "claude-cli/2.1.19",
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(result.success, true);
|
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.thinking.type, "adaptive");
|
||||||
assert.equal(captured.body.output_config.effort, "xhigh");
|
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/);
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ import { CLAUDE_CODE_CLIENT_BILLING_VERSION } from "../../src/shared/constants/c
|
|||||||
|
|
||||||
describe("Anthropic billing header fingerprint (#1638)", () => {
|
describe("Anthropic billing header fingerprint (#1638)", () => {
|
||||||
it("uses the immutable build revision captured from the signed CLI", () => {
|
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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ test("DEFAULT_CC_BRIDGE_PIPELINE places billing header at [0] and identity at [1
|
|||||||
DEFAULT_CC_BRIDGE_PIPELINE
|
DEFAULT_CC_BRIDGE_PIPELINE
|
||||||
);
|
);
|
||||||
const blocks = result.body.system as any[];
|
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);
|
assert.equal(blocks[1].text, CLAUDE_AGENT_SDK_IDENTITY);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,22 @@ test("non azure/oci providers never get apiType forcing", () => {
|
|||||||
assert.equal(psd._omnirouteForceResponsesUpstream, undefined);
|
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", () => {
|
test("ccSessionId is threaded into providerSpecificData when present", () => {
|
||||||
const out = resolveExecutionCredentials({
|
const out = resolveExecutionCredentials({
|
||||||
...base,
|
...base,
|
||||||
|
|||||||
@@ -4,11 +4,14 @@
|
|||||||
// upstream target format: apiFormat==="responses" forces OpenAI Responses; otherwise the model's
|
// 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
|
// 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
|
// `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 { test } from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { resolveChatCoreTargetFormat } from "../../open-sse/handlers/chatCore/targetFormat.ts";
|
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 { getTargetFormat } from "../../open-sse/services/provider.ts";
|
||||||
import { FORMATS } from "../../open-sse/translator/formats.ts";
|
import { FORMATS } from "../../open-sse/translator/formats.ts";
|
||||||
|
|
||||||
@@ -24,7 +27,9 @@ function expected(
|
|||||||
const targetFormat =
|
const targetFormat =
|
||||||
apiFormat === "responses"
|
apiFormat === "responses"
|
||||||
? FORMATS.OPENAI_RESPONSES
|
? FORMATS.OPENAI_RESPONSES
|
||||||
: modelTargetFormat || customModelTargetFormat || getTargetFormat(provider, providerSpecificData);
|
: modelTargetFormat ||
|
||||||
|
customModelTargetFormat ||
|
||||||
|
getTargetFormat(provider, providerSpecificData);
|
||||||
return { alias, targetFormat };
|
return { alias, targetFormat };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +38,7 @@ test("apiFormat='responses' short-circuits to OPENAI_RESPONSES (alias still reso
|
|||||||
provider: "openai",
|
provider: "openai",
|
||||||
resolvedModel: "gpt-4o",
|
resolvedModel: "gpt-4o",
|
||||||
apiFormat: "responses",
|
apiFormat: "responses",
|
||||||
|
sourceFormat: FORMATS.OPENAI,
|
||||||
customModelTargetFormat: undefined,
|
customModelTargetFormat: undefined,
|
||||||
providerSpecificData: undefined,
|
providerSpecificData: undefined,
|
||||||
});
|
});
|
||||||
@@ -45,6 +51,7 @@ test("delegates byte-identically for a normal model (no apiFormat / no custom ov
|
|||||||
provider: "openai",
|
provider: "openai",
|
||||||
resolvedModel: "gpt-4o",
|
resolvedModel: "gpt-4o",
|
||||||
apiFormat: undefined,
|
apiFormat: undefined,
|
||||||
|
sourceFormat: FORMATS.OPENAI,
|
||||||
customModelTargetFormat: undefined,
|
customModelTargetFormat: undefined,
|
||||||
providerSpecificData: undefined,
|
providerSpecificData: undefined,
|
||||||
});
|
});
|
||||||
@@ -59,6 +66,7 @@ test("customModelTargetFormat is used when the model has no registry target form
|
|||||||
provider: "openai",
|
provider: "openai",
|
||||||
resolvedModel: customModel,
|
resolvedModel: customModel,
|
||||||
apiFormat: undefined,
|
apiFormat: undefined,
|
||||||
|
sourceFormat: FORMATS.OPENAI,
|
||||||
customModelTargetFormat: "claude",
|
customModelTargetFormat: "claude",
|
||||||
providerSpecificData: undefined,
|
providerSpecificData: undefined,
|
||||||
});
|
});
|
||||||
@@ -71,17 +79,31 @@ test("falls back to getTargetFormat(provider) when neither model nor custom form
|
|||||||
provider: "openai",
|
provider: "openai",
|
||||||
resolvedModel: customModel,
|
resolvedModel: customModel,
|
||||||
apiFormat: undefined,
|
apiFormat: undefined,
|
||||||
|
sourceFormat: FORMATS.OPENAI,
|
||||||
customModelTargetFormat: undefined,
|
customModelTargetFormat: undefined,
|
||||||
providerSpecificData: undefined,
|
providerSpecificData: undefined,
|
||||||
});
|
});
|
||||||
assert.equal(r.targetFormat, getTargetFormat("openai", 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", () => {
|
test("unmapped provider → alias falls back to the provider id", () => {
|
||||||
const r = resolveChatCoreTargetFormat({
|
const r = resolveChatCoreTargetFormat({
|
||||||
provider: "some-unmapped-provider",
|
provider: "some-unmapped-provider",
|
||||||
resolvedModel: "x",
|
resolvedModel: "x",
|
||||||
apiFormat: "responses",
|
apiFormat: "responses",
|
||||||
|
sourceFormat: FORMATS.OPENAI,
|
||||||
customModelTargetFormat: undefined,
|
customModelTargetFormat: undefined,
|
||||||
providerSpecificData: undefined,
|
providerSpecificData: undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.Authorization, "Bearer codex-token");
|
||||||
assert.equal(standardHeaders.Accept, "text/event-stream");
|
assert.equal(standardHeaders.Accept, "text/event-stream");
|
||||||
assert.equal(standardHeaders["chatgpt-account-id"], "workspace-1");
|
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["Openai-Beta"], "responses=experimental");
|
||||||
assert.equal(standardHeaders["X-Codex-Beta-Features"], "responses_websockets");
|
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");
|
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);
|
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");
|
assert.equal(headers["User-Agent"], "custom-codex/9.9.9");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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(CONTEXT_1M_BETA_HEADER), false);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
calls[0].headers["anthropic-beta"].includes(CLAUDE_CODE_COMPATIBLE_REDACT_THINKING_BETA),
|
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(
|
assert.equal(
|
||||||
calls[1].headers["anthropic-beta"].includes(CLAUDE_CODE_COMPATIBLE_REDACT_THINKING_BETA),
|
calls[1].headers["anthropic-beta"].includes(CLAUDE_CODE_COMPATIBLE_REDACT_THINKING_BETA),
|
||||||
true
|
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 }> };
|
const sentBody = JSON.parse(capturedBody) as { system?: Array<{ text?: string }> };
|
||||||
assert.match(
|
assert.match(
|
||||||
sentBody.system?.[0]?.text ?? "",
|
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};$/
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ test("GlmExecutor separates OpenAI-compatible coding headers from Anthropic head
|
|||||||
assert.equal(anthropicHeaders["anthropic-version"], "2023-06-01");
|
assert.equal(anthropicHeaders["anthropic-version"], "2023-06-01");
|
||||||
assert.match(anthropicHeaders["anthropic-beta"], /claude-code-20250219/);
|
assert.match(anthropicHeaders["anthropic-beta"], /claude-code-20250219/);
|
||||||
assert.equal(anthropicHeaders["anthropic-dangerous-direct-browser-access"], "true");
|
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-Lang"], "js");
|
||||||
assert.equal(anthropicHeaders["X-Stainless-Runtime"], "node");
|
assert.equal(anthropicHeaders["X-Stainless-Runtime"], "node");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -159,11 +159,11 @@ test("provider models route merges live Codex models with the local catalog then
|
|||||||
assert.equal(body.discoveredCandidateCount, undefined);
|
assert.equal(body.discoveredCandidateCount, undefined);
|
||||||
assert.deepEqual(seenRequests, [
|
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",
|
authorization: "Bearer codex-access-token",
|
||||||
workspaceId: "account-123",
|
workspaceId: "account-123",
|
||||||
originator: "codex_cli_rs",
|
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",
|
url: "https://raw.githubusercontent.com/openai/codex/refs/heads/main/codex-rs/models-manager/models.json",
|
||||||
|
|||||||
@@ -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.ok(providerRequest, "providerRequest must be present on CC-compatible success");
|
||||||
assert.equal(providerRequest.headers["cf-ray"], undefined);
|
assert.equal(providerRequest.headers["cf-ray"], undefined);
|
||||||
assert.equal(providerRequest.headers.server, undefined);
|
assert.equal(providerRequest.headers.server, undefined);
|
||||||
assert.equal(providerRequest.headers.Accept, "text/event-stream");
|
assert.equal(providerRequest.headers.Accept, "application/json");
|
||||||
assert.match(providerRequest.headers["anthropic-beta"], new RegExp(CONTEXT_1M_BETA_HEADER));
|
assert.doesNotMatch(
|
||||||
|
providerRequest.headers["anthropic-beta"],
|
||||||
|
new RegExp(CONTEXT_1M_BETA_HEADER)
|
||||||
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
providerRequest.headers["anthropic-beta"],
|
providerRequest.headers["anthropic-beta"],
|
||||||
new RegExp(CLAUDE_CODE_COMPATIBLE_REDACT_THINKING_BETA)
|
new RegExp(CLAUDE_CODE_COMPATIBLE_REDACT_THINKING_BETA)
|
||||||
|
|||||||
@@ -542,7 +542,7 @@ const UI_DEFAULTS_SNAPSHOT = {
|
|||||||
entrypoint: "sdk-cli",
|
entrypoint: "sdk-cli",
|
||||||
versionFormat: "ex-machina",
|
versionFormat: "ex-machina",
|
||||||
cchAlgo: "sha256-first-user",
|
cchAlgo: "sha256-first-user",
|
||||||
buildRevision: "250",
|
buildRevision: "1f2",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user