mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
committed by
GitHub
parent
826a66f287
commit
5531fc7f05
@@ -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
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
26
open-sse/services/ccWireImageBuiltins.ts
Normal file
26
open-sse/services/ccWireImageBuiltins.ts
Normal 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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
91
tests/unit/agentrouter-cc-wire-image.test.ts
Normal file
91
tests/unit/agentrouter-cc-wire-image.test.ts
Normal 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"));
|
||||
});
|
||||
Reference in New Issue
Block a user