feat(providers): route built-in agentrouter through dynamic CC wire image (#6056) (#6255)

feat(providers): route built-in agentrouter through dynamic CC wire image (#6056). TDD-covered (agentrouter-cc-wire-image.test.ts). Base-reds only. Integrated into release/v3.8.45.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-05 07:41:15 -03:00
committed by GitHub
parent 826a66f287
commit 5531fc7f05
7 changed files with 189 additions and 40 deletions

View File

@@ -5,6 +5,7 @@
### ✨ New Features
- **feat(providers):** add **Yuanbao (web)** as a cookie-session provider ([#6196](https://github.com/diegosouzapw/OmniRoute/issues/6196)) — `yuanbao-web` (Tencent Yuanbao, `yuanbao.tencent.com`) with cookie-only auth (`hy_user`/`hy_token` + public agent id), SSE→OpenAI translation incl. `reasoning_content`, exposing DeepSeek V3/R1 + Hunyuan / Hunyuan-T1. Regression guard: `tests/unit/providers-yuanbao-web.test.ts`. `together-web` was **deferred** (no verifiable web-session endpoint — needs a captured request) and `huggingchat-web` **dropped** (the existing `huggingchat` already is a web-cookie provider). (thanks @chirag127)
- **feat(providers):** route the built-in **agentrouter** through the dynamic Claude-Code wire image ([#6056](https://github.com/diegosouzapw/OmniRoute/issues/6056)) — a small static allow-set (`CC_WIRE_IMAGE_BUILTINS` in `open-sse/services/ccWireImageBuiltins.ts`), consulted by `isClaudeCodeCompatible` / `isClaudeCodeCompatibleProvider` / `applyFingerprint`, makes agentrouter adopt the CC wire-image headers + fingerprint **while guarding the CC baseUrl/auth branches** so it keeps its own registry `baseUrl` and `x-api-key` auth. Regression guard: `tests/unit/agentrouter-cc-wire-image.test.ts` (asserts the wire image is applied AND agentrouter's baseUrl/auth are preserved). Live WAF-acceptance against agentrouter.org is a VPS validation follow-up (Hard Rule #18).
### 🐛 Bug Fixes

View File

@@ -1,14 +1,4 @@
import type { RegistryEntry } from "../../shared.ts";
import {
getClaudeCliHeaders,
mapStainlessOs,
mapStainlessArch,
ANTHROPIC_BETA_CLAUDE_OAUTH,
ANTHROPIC_VERSION_HEADER,
CLAUDE_CLI_STAINLESS_PACKAGE_VERSION,
CLAUDE_CLI_STAINLESS_RUNTIME_VERSION,
CLAUDE_CLI_USER_AGENT,
} from "../../shared.ts";
export const agentrouterProvider: RegistryEntry = {
id: "agentrouter",
@@ -19,7 +9,11 @@ export const agentrouterProvider: RegistryEntry = {
authType: "apikey",
authHeader: "x-api-key",
defaultContextLength: 128000,
headers: getClaudeCliHeaders(),
// No static `headers` here: agentrouter now adopts the DYNAMIC Claude-Code
// wire image via CC_WIRE_IMAGE_BUILTINS (#6056) — the fingerprint/headers are
// applied by buildProviderHeaders + applyFingerprint, keeping this entry's
// own baseUrl + x-api-key auth. A static fingerprint here would drift and
// trip AgentRouter's WAF ("unauthorized client detected").
models: [
{ id: "claude-opus-4-6", name: "Claude 4.6 Opus" },
{ id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" },

View File

@@ -0,0 +1,26 @@
/**
* Built-in provider ids that must adopt the dynamic Claude-Code wire image
* (fingerprint headers/order + system transforms + `?beta=true` chat path)
* WITHOUT inheriting the Claude-Code-Compatible family's default anthropic
* baseUrl / Bearer auth.
*
* These providers keep their own registry `baseUrl` and auth scheme
* (e.g. `agentrouter` → `https://agentrouter.org/v1/messages` + `x-api-key`),
* while the two CC predicates (`isClaudeCodeCompatible` /
* `isClaudeCodeCompatibleProvider`) and `applyFingerprint` treat them as CC
* for the wire-image concerns only. The CC-baseUrl / CC-Bearer branches in
* `buildProviderUrl` / `buildProviderHeaders` are guarded so the registry
* baseUrl + auth are preserved.
*
* Single source of truth — imported by both predicates so they never diverge.
* See issue #6056.
*/
export const CC_WIRE_IMAGE_BUILTINS: ReadonlySet<string> = new Set(["agentrouter"]);
/**
* True when `provider` is a built-in that adopts the dynamic Claude-Code wire
* image while keeping its own registry baseUrl + auth.
*/
export function usesCcWireImage(provider: unknown): boolean {
return typeof provider === "string" && CC_WIRE_IMAGE_BUILTINS.has(provider);
}

View File

@@ -15,6 +15,7 @@ import {
import { applyClaudeCodeCompatibleThinkingDisplay } from "./claudeCodeCompatibleThinkingDisplay.ts";
import { obfuscateInBody } from "./claudeCodeObfuscation.ts";
import { applySystemTransformPipeline, PROVIDER_CC_BRIDGE } from "./systemTransforms.ts";
import { usesCcWireImage } from "./ccWireImageBuiltins.ts";
import {
fixToolPairs,
fixToolAdjacency,
@@ -95,7 +96,12 @@ function supportsClaudeXHighEffort(model: string | null | undefined): boolean {
}
export function isClaudeCodeCompatibleProvider(provider: string | null | undefined): boolean {
return typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX);
return (
(typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX)) ||
// Built-in providers (e.g. agentrouter) that adopt the dynamic CC wire image
// while keeping their own registry baseUrl + auth (#6056).
usesCcWireImage(provider)
);
}
export function stripAnthropicMessagesSuffix(baseUrl: string | null | undefined): string {

View File

@@ -8,6 +8,7 @@ import {
} from "./claudeCodeCompatible.ts";
import { getClaudeCodeCompatibleRequestDefaults } from "@/lib/providers/requestDefaults";
import { buildClineHeaders } from "@/shared/utils/clineAuth";
import { usesCcWireImage } from "./ccWireImageBuiltins.ts";
const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";
const OPENAI_COMPATIBLE_DEFAULTS = {
@@ -29,7 +30,12 @@ function isAnthropicCompatible(provider) {
}
export function isClaudeCodeCompatible(provider) {
return typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX);
return (
(typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX)) ||
// Built-in providers (e.g. agentrouter) that adopt the dynamic CC wire image
// while keeping their own registry baseUrl + auth (#6056).
usesCcWireImage(provider)
);
}
export function getOpenAICompatibleType(
@@ -256,6 +262,15 @@ export function buildProviderUrl(
providerSpecificData?: Record<string, unknown> | null;
} = {}
) {
// Built-in CC-wire-image providers (e.g. agentrouter): keep the registry's
// OWN baseUrl (NOT the CC family's anthropic default) but adopt the CC chat
// path so the request still targets `<registry-baseUrl>?beta=true` (#6056).
if (usesCcWireImage(provider)) {
const entry = getRegistryEntry(provider);
const config = getProviderConfig(provider);
const baseUrl = options?.baseUrl || entry?.baseUrl || config.baseUrl;
return joinClaudeCodeCompatibleUrl(baseUrl, CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH);
}
if (isOpenAICompatible(provider)) {
const providerSpecificData = options?.providerSpecificData || null;
const apiType = getOpenAICompatibleType(provider, providerSpecificData);
@@ -318,12 +333,27 @@ export function buildProviderHeaders(provider, credentials, stream = true, body
const ccRequestDefaults = getClaudeCodeCompatibleRequestDefaults(
credentials?.providerSpecificData
);
return buildClaudeCodeCompatibleHeaders(
const ccHeaders = buildClaudeCodeCompatibleHeaders(
token,
stream,
credentials?.providerSpecificData?.ccSessionId,
{ redactThinking: ccRequestDefaults.redactThinking === true }
);
// Built-in CC-wire-image providers (e.g. agentrouter): adopt the CC wire
// image headers but keep the registry's OWN auth scheme (e.g. x-api-key)
// instead of the CC family's Bearer auth (#6056).
if (usesCcWireImage(provider)) {
delete ccHeaders["Authorization"];
const authHeader = entry?.authHeader || "bearer";
if (authHeader === "x-api-key") {
if (token) ccHeaders["x-api-key"] = token;
} else if (authHeader === "key") {
if (token) ccHeaders["Authorization"] = `Key ${token}`;
} else {
ccHeaders["Authorization"] = `Bearer ${token}`;
}
}
return ccHeaders;
}
if (isAnthropicCompatible(provider)) {
if (credentials.apiKey) {

View File

@@ -27,64 +27,65 @@
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07",
"Anthropic-Dangerous-Direct-Browser-Access": "true",
"Anthropic-Version": "2023-06-01",
"Content-Type": "application/json",
"User-Agent": "claude-cli/2.1.195 (external, cli)",
"X-App": "cli",
"User-Agent": "claude-cli/2.1.195 (external, sdk-cli)",
"X-Stainless-Arch": "<ARCH>",
"X-Stainless-Helper-Method": "stream",
"X-Stainless-Lang": "js",
"X-Stainless-Os": "<OS>",
"X-Stainless-OS": "MacOS",
"X-Stainless-Package-Version": "0.94.0",
"X-Stainless-Retry-Count": "0",
"X-Stainless-Runtime": "node",
"X-Stainless-Runtime-Version": "v24.3.0",
"X-Stainless-Timeout": "600",
"x-api-key": "<CRED>"
"accept-encoding": "gzip, deflate, br, zstd",
"anthropic-beta": "claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24",
"anthropic-dangerous-direct-browser-access": "true",
"anthropic-version": "2023-06-01",
"x-api-key": "<CRED>",
"x-app": "cli"
},
"nonStream": {
"Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07",
"Anthropic-Dangerous-Direct-Browser-Access": "true",
"Anthropic-Version": "2023-06-01",
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "claude-cli/2.1.195 (external, cli)",
"X-App": "cli",
"User-Agent": "claude-cli/2.1.195 (external, sdk-cli)",
"X-Stainless-Arch": "<ARCH>",
"X-Stainless-Helper-Method": "stream",
"X-Stainless-Lang": "js",
"X-Stainless-Os": "<OS>",
"X-Stainless-OS": "MacOS",
"X-Stainless-Package-Version": "0.94.0",
"X-Stainless-Retry-Count": "0",
"X-Stainless-Runtime": "node",
"X-Stainless-Runtime-Version": "v24.3.0",
"X-Stainless-Timeout": "600",
"x-api-key": "<CRED>"
"accept-encoding": "gzip, deflate, br, zstd",
"anthropic-beta": "claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24",
"anthropic-dangerous-direct-browser-access": "true",
"anthropic-version": "2023-06-01",
"x-api-key": "<CRED>",
"x-app": "cli"
},
"oauth": {
"Accept": "text/event-stream",
"Anthropic-Beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24,structured-outputs-2025-12-15,fast-mode-2026-02-01,redact-thinking-2026-02-12,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01,extended-cache-ttl-2025-04-11,cache-diagnosis-2026-04-07",
"Anthropic-Dangerous-Direct-Browser-Access": "true",
"Anthropic-Version": "2023-06-01",
"Content-Type": "application/json",
"User-Agent": "claude-cli/2.1.195 (external, cli)",
"X-App": "cli",
"User-Agent": "claude-cli/2.1.195 (external, sdk-cli)",
"X-Stainless-Arch": "<ARCH>",
"X-Stainless-Helper-Method": "stream",
"X-Stainless-Lang": "js",
"X-Stainless-Os": "<OS>",
"X-Stainless-OS": "MacOS",
"X-Stainless-Package-Version": "0.94.0",
"X-Stainless-Retry-Count": "0",
"X-Stainless-Runtime": "node",
"X-Stainless-Runtime-Version": "v24.3.0",
"X-Stainless-Timeout": "600",
"x-api-key": "<CRED>"
"accept-encoding": "gzip, deflate, br, zstd",
"anthropic-beta": "claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24",
"anthropic-dangerous-direct-browser-access": "true",
"anthropic-version": "2023-06-01",
"x-api-key": "<CRED>",
"x-app": "cli"
}
},
"url": {
"nonStream": "https://agentrouter.org/v1/messages",
"stream": "https://agentrouter.org/v1/messages"
"nonStream": "https://agentrouter.org/v1/messages?beta=true",
"stream": "https://agentrouter.org/v1/messages?beta=true"
}
},
"agy": {

View File

@@ -0,0 +1,91 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
isClaudeCodeCompatible,
buildProviderUrl,
buildProviderHeaders,
} from "../../open-sse/services/provider.ts";
import { isClaudeCodeCompatibleProvider } from "../../open-sse/services/claudeCodeCompatible.ts";
import {
CC_WIRE_IMAGE_BUILTINS,
usesCcWireImage,
} from "../../open-sse/services/ccWireImageBuiltins.ts";
import { CLAUDE_CODE_COMPATIBLE_USER_AGENT } from "../../open-sse/services/claudeCodeCompatible.ts";
import { CLAUDE_CLI_USER_AGENT } from "../../open-sse/config/anthropicHeaders.ts";
import { applyFingerprint } from "../../open-sse/config/cliFingerprints.ts";
// Regression guard for #6056 — the built-in `agentrouter` provider must route
// through the DYNAMIC Claude-Code wire image (fingerprint headers + `?beta=true`
// chat path) while KEEPING its own registry baseUrl + x-api-key auth.
test("agentrouter is registered in the CC-wire-image built-in allow-set", () => {
assert.ok(CC_WIRE_IMAGE_BUILTINS.has("agentrouter"));
assert.equal(usesCcWireImage("agentrouter"), true);
assert.equal(usesCcWireImage("claude"), false);
assert.equal(usesCcWireImage(null), false);
});
test("(a) both CC predicates return true for agentrouter", () => {
assert.equal(isClaudeCodeCompatible("agentrouter"), true);
assert.equal(isClaudeCodeCompatibleProvider("agentrouter"), true);
});
test("(a) predicates are unaffected for non-allow-set providers", () => {
// Official Claude OAuth provider must NOT be treated as CC-compatible.
assert.equal(isClaudeCodeCompatible("claude"), false);
assert.equal(isClaudeCodeCompatibleProvider("claude"), false);
// Genuine CC-family providers still match via the prefix.
assert.equal(isClaudeCodeCompatible("anthropic-compatible-cc-foo"), true);
assert.equal(isClaudeCodeCompatibleProvider("anthropic-compatible-cc-foo"), true);
});
test("(b) agentrouter outbound headers carry the dynamic CC wire image", () => {
const headers = buildProviderHeaders("agentrouter", { apiKey: "sk-agentrouter" }, true);
// CC wire image markers (not the static getClaudeCliHeaders() shape).
assert.equal(headers["User-Agent"], CLAUDE_CODE_COMPATIBLE_USER_AGENT);
assert.notEqual(headers["User-Agent"], CLAUDE_CLI_USER_AGENT);
assert.equal(headers["x-app"], "cli");
assert.equal(headers["anthropic-dangerous-direct-browser-access"], "true");
assert.ok(headers["anthropic-beta"], "expected the CC anthropic-beta header");
assert.ok(headers["X-Stainless-Package-Version"], "expected CC X-Stainless anchors");
});
test("(b) applyFingerprint selects the claude-code-compatible fingerprint for agentrouter", () => {
const { headers } = applyFingerprint(
"agentrouter",
buildProviderHeaders("agentrouter", { apiKey: "sk-agentrouter" }, true),
{ model: "claude-opus-4-6", messages: [] }
);
// Fingerprint reordering keeps the CC wire image + the preserved x-api-key auth.
assert.equal(headers["x-api-key"], "sk-agentrouter");
assert.equal(headers["User-Agent"], CLAUDE_CODE_COMPATIBLE_USER_AGENT);
});
test("(c) CRUX: agentrouter keeps its OWN x-api-key auth (NOT CC Bearer)", () => {
const headers = buildProviderHeaders("agentrouter", { apiKey: "sk-agentrouter" }, true);
assert.equal(headers["x-api-key"], "sk-agentrouter");
assert.equal(headers["Authorization"], undefined);
});
test("(c) CRUX: agentrouter keeps its OWN registry baseUrl + ?beta=true", () => {
const url = buildProviderUrl("agentrouter", "claude-opus-4-6", true);
assert.equal(url, "https://agentrouter.org/v1/messages?beta=true");
// NOT the CC-family anthropic default baseUrl.
assert.ok(!url.includes("api.anthropic.com"));
});
test("(c) real CC-family provider still uses the CC default baseUrl + Bearer auth", () => {
// The wire-image guard must NOT leak into genuine anthropic-compatible-cc-* providers.
const headers = buildProviderHeaders(
"anthropic-compatible-cc-foo",
{ apiKey: "sk-foo" },
true
);
assert.equal(headers["Authorization"], "Bearer sk-foo");
assert.equal(headers["x-api-key"], undefined);
const url = buildProviderUrl("anthropic-compatible-cc-foo", "claude-sonnet-4-6", true);
assert.ok(url.includes("api.anthropic.com"));
});