diff --git a/.env.example b/.env.example index e0fa9d6bed..67097b7104 100644 --- a/.env.example +++ b/.env.example @@ -1332,6 +1332,16 @@ CURSOR_USER_AGENT="Cursor/3.4" # Override Codex client version sent in headers independently of the # CODEX_USER_AGENT string. Used by: open-sse/config/codexClient.ts. # CODEX_CLIENT_VERSION=0.144.1 +# +# Override the advertised Claude Code client version independently of +# CLAUDE_USER_AGENT. Anthropic gates some models (Fable 5.1) on this +# value; a UA-only override is not enough (#12417). Used by: +# src/shared/constants/claudeCodeClient.ts. +# CLAUDE_CODE_CLIENT_VERSION=2.1.259 +# +# Override the advertised GitHub Copilot CLI version independently of +# GITHUB_USER_AGENT. Used by: open-sse/config/providerHeaderProfiles.ts. +# GITHUB_COPILOT_CLI_VERSION=1.0.82 # Kill-switch to strip non-standard `codex.*` SSE events (e.g. codex.rate_limits) # from the Codex Responses stream. These frames break the OpenAI SDK's diff --git a/changelog.d/fixes/12417-cli-version-env.md b/changelog.d/fixes/12417-cli-version-env.md new file mode 100644 index 0000000000..5fbe8327a1 --- /dev/null +++ b/changelog.d/fixes/12417-cli-version-env.md @@ -0,0 +1 @@ +- **fix(providers):** add `CLAUDE_CODE_CLIENT_VERSION` and `GITHUB_COPILOT_CLI_VERSION` env overrides so Anthropic/Copilot client-version gates can be unblocked without a rebuild ([#12417](https://github.com/diegosouzapw/OmniRoute/issues/12417)) diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 8b743b2ecc..da053f1db3 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -636,6 +636,8 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] | `CLAUDE_DISABLE_TOOL_NAME_CLOAK` | `false` | `executors/base.ts` + `executors/cliproxyapi.ts` | Set to `1`/`true` to forward third-party harness tool names verbatim to Anthropic on both Anthropic-bound paths (native OAuth and CLIProxyAPI). By default the executor deterministically aliases non-Claude-Code tool names (Claude Code canonical mapping where one exists, otherwise PascalCase) and reverses them on the response via `_toolNameMap`, so harnesses with snake_case tools are not refused as fingerprinted third-party clients. Debugging only. | | `CODEX_USER_AGENT` | `codex-cli/0.142.0 (Windows 10.0.26200; x64)` | When OpenAI updates the Codex CLI | | `CODEX_CLIENT_VERSION` | `0.131.0` | Override Codex client version independently of full UA string | +| `CLAUDE_CODE_CLIENT_VERSION` | `2.1.258` | Override advertised Claude Code version independently of `CLAUDE_USER_AGENT`. Anthropic gates some models on this value (#12417). | +| `GITHUB_COPILOT_CLI_VERSION` | `1.0.81-6` | Override advertised Copilot CLI version independently of `GITHUB_USER_AGENT` | | `GITHUB_USER_AGENT` | `GitHubCopilotChat/0.54.0` | When GitHub Copilot Chat updates | | `ANTIGRAVITY_USER_AGENT` | `antigravity/2.0.1 darwin/arm64` | When Antigravity IDE updates | | `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | When Kiro IDE updates | diff --git a/open-sse/config/anthropicHeaders.ts b/open-sse/config/anthropicHeaders.ts index a030cd1c4a..6625c4dd07 100644 --- a/open-sse/config/anthropicHeaders.ts +++ b/open-sse/config/anthropicHeaders.ts @@ -4,6 +4,8 @@ import { CLAUDE_CODE_CLIENT_VERSION, CLAUDE_CODE_RUNTIME_VERSION, CLAUDE_CODE_SDK_PACKAGE_VERSION, + getClaudeCodeClientBillingVersion, + getClaudeCodeClientVersion, getClaudeCodeUserAgent, } from "@/shared/constants/claudeCodeClient"; import { modelSupportsContext1mBeta } from "../config/context1m.ts"; @@ -166,8 +168,17 @@ export function normalizeAnthropicHeaderVariants(headers: Record } export const CLAUDE_CLI_VERSION = CLAUDE_CODE_CLIENT_VERSION; +export function getClaudeCliVersion(): string { + return getClaudeCodeClientVersion(); +} export const CLAUDE_CLI_BUILD_REVISION = CLAUDE_CODE_CLIENT_BUILD_REVISION; +/** Captured-pin snapshot. Wire billing uses getClaudeCliBillingVersion(). */ export const CLAUDE_CLI_BILLING_VERSION = CLAUDE_CODE_CLIENT_BILLING_VERSION; +export function getClaudeCliBillingVersion(): string { + return getClaudeCodeClientBillingVersion(); +} +/** Module-load snapshot of the pin (or env if set before import). Wire UA uses getClaudeCodeUserAgent(). */ export const CLAUDE_CLI_USER_AGENT = getClaudeCodeUserAgent("cli"); +export { getClaudeCodeUserAgent }; export const CLAUDE_CLI_STAINLESS_PACKAGE_VERSION = CLAUDE_CODE_SDK_PACKAGE_VERSION; export const CLAUDE_CLI_STAINLESS_RUNTIME_VERSION = CLAUDE_CODE_RUNTIME_VERSION; diff --git a/open-sse/config/claudeCodeCompatibleIdentity.ts b/open-sse/config/claudeCodeCompatibleIdentity.ts index b614eea324..29f996278c 100644 --- a/open-sse/config/claudeCodeCompatibleIdentity.ts +++ b/open-sse/config/claudeCodeCompatibleIdentity.ts @@ -2,11 +2,17 @@ import { CLAUDE_CODE_CLIENT_VERSION, CLAUDE_CODE_RUNTIME_VERSION, CLAUDE_CODE_SDK_PACKAGE_VERSION, + getClaudeCodeClientVersion, getClaudeCodeUserAgent, } from "@/shared/constants/claudeCodeClient"; export const CLAUDE_CODE_COMPATIBLE_VERSION = CLAUDE_CODE_CLIENT_VERSION; +export function getClaudeCodeCompatibleVersion(): string { + return getClaudeCodeClientVersion(); +} +/** Module-load snapshot. Wire UA uses getClaudeCodeUserAgent("sdk-cli"). */ export const CLAUDE_CODE_COMPATIBLE_USER_AGENT = getClaudeCodeUserAgent("sdk-cli"); +export { getClaudeCodeUserAgent }; export const CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION = CLAUDE_CODE_SDK_PACKAGE_VERSION; export const CLAUDE_CODE_COMPATIBLE_STAINLESS_RUNTIME_VERSION = CLAUDE_CODE_RUNTIME_VERSION; const CONTEXT_1M_NATIVE_MODELS = ["claude-fable-5-1", "claude-opus-5"]; diff --git a/open-sse/config/cliFingerprints.ts b/open-sse/config/cliFingerprints.ts index efa902e137..8891f73d9d 100644 --- a/open-sse/config/cliFingerprints.ts +++ b/open-sse/config/cliFingerprints.ts @@ -12,7 +12,7 @@ import { isClaudeCodeCompatible } from "../services/provider.ts"; import { getAntigravityUserAgent, - GITHUB_COPILOT_CHAT_USER_AGENT, + getGitHubCopilotChatUserAgent, } from "./providerHeaderProfiles.ts"; import { normalizeCliCompatProviderId } from "@/shared/utils/cliCompat"; @@ -169,7 +169,7 @@ export const CLI_FINGERPRINTS: Record = { "intent_threshold", "intent_content", ], - userAgent: GITHUB_COPILOT_CHAT_USER_AGENT, + userAgent: getGitHubCopilotChatUserAgent, }, antigravity: { headerOrder: [ diff --git a/open-sse/config/glmProvider.ts b/open-sse/config/glmProvider.ts index 3bb6c85210..22d05864e7 100644 --- a/open-sse/config/glmProvider.ts +++ b/open-sse/config/glmProvider.ts @@ -255,6 +255,7 @@ export const GLMT_REQUEST_DEFAULTS = Object.freeze({ }); export const GLM_COUNT_TOKENS_TIMEOUT_MS = 3_000; +/** Module-load snapshot. Wire UA uses getClaudeCodeUserAgent("sdk-cli"). */ export const GLM_CLAUDE_CODE_USER_AGENT = getClaudeCodeUserAgent("sdk-cli"); export const GLM_ANTHROPIC_BETA = [ "claude-code-20250219", @@ -582,7 +583,7 @@ export function buildGlmBaseHeaders(apiKey: string, stream = true): Record = { "copilot-integration-id": GITHUB_COPILOT_INTEGRATION_ID, - "editor-version": GITHUB_COPILOT_EDITOR_VERSION, - "user-agent": GITHUB_COPILOT_CLI_USER_AGENT, + "editor-version": `copilot/${version}`, + "user-agent": `copilot/${version}`, "openai-intent": options.intent || GITHUB_COPILOT_OPENAI_INTENT, "x-interaction-type": GITHUB_COPILOT_INTERACTION_TYPE, "copilot-harness-id": GITHUB_COPILOT_HARNESS_ID, @@ -128,23 +155,25 @@ export function getQwenCliUserAgent(version = QWEN_CLI_VERSION): string { } export function getGitHubCopilotInternalUserHeaders(authorization: string): Record { + const version = getGitHubCopilotCliVersion(); return { Authorization: authorization, Accept: "application/json", "X-GitHub-Api-Version": GITHUB_COPILOT_API_VERSION, - "User-Agent": GITHUB_COPILOT_CHAT_USER_AGENT, - "Editor-Version": GITHUB_COPILOT_EDITOR_VERSION, - "Editor-Plugin-Version": GITHUB_COPILOT_CHAT_PLUGIN_VERSION, + "User-Agent": `GitHubCopilotChat/${version}`, + "Editor-Version": `copilot/${version}`, + "Editor-Plugin-Version": `copilot-chat/${version}`, }; } export function getGitHubCopilotRefreshHeaders(authorization: string): Record { + const version = getGitHubCopilotCliVersion(); return { Authorization: authorization, Accept: "application/json", "User-Agent": GITHUB_COPILOT_REFRESH_USER_AGENT, - "Editor-Version": GITHUB_COPILOT_EDITOR_VERSION, - "Editor-Plugin-Version": GITHUB_COPILOT_REFRESH_PLUGIN_VERSION, + "Editor-Version": `copilot/${version}`, + "Editor-Plugin-Version": `copilot/${version}`, }; } diff --git a/open-sse/config/providers/registry/claude/index.ts b/open-sse/config/providers/registry/claude/index.ts index ba3129678b..8388812300 100644 --- a/open-sse/config/providers/registry/claude/index.ts +++ b/open-sse/config/providers/registry/claude/index.ts @@ -7,7 +7,6 @@ import { ANTHROPIC_VERSION_HEADER, CLAUDE_CLI_STAINLESS_PACKAGE_VERSION, CLAUDE_CLI_STAINLESS_RUNTIME_VERSION, - CLAUDE_CLI_USER_AGENT, resolvePublicCred, } from "../../shared.ts"; diff --git a/open-sse/config/providers/shared.ts b/open-sse/config/providers/shared.ts index 6c67c4eb5f..8b9b45bfec 100644 --- a/open-sse/config/providers/shared.ts +++ b/open-sse/config/providers/shared.ts @@ -16,6 +16,7 @@ import { CLAUDE_CLI_STAINLESS_PACKAGE_VERSION, CLAUDE_CLI_STAINLESS_RUNTIME_VERSION, CLAUDE_CLI_USER_AGENT, + getClaudeCodeUserAgent, } from "../anthropicHeaders.ts"; import { getCodexDefaultHeaders } from "../codexClient.ts"; import { @@ -761,7 +762,7 @@ export function getClaudeCliHeaders(): Record { "Anthropic-Version": ANTHROPIC_VERSION_HEADER, "Anthropic-Beta": ANTHROPIC_BETA_CLAUDE_OAUTH, "Anthropic-Dangerous-Direct-Browser-Access": "true", - "User-Agent": CLAUDE_CLI_USER_AGENT, + "User-Agent": getClaudeCodeUserAgent("cli"), "X-App": "cli", "X-Stainless-Helper-Method": "stream", "X-Stainless-Retry-Count": "0", diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index 11d5f9087c..b18c27e20c 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -6,8 +6,8 @@ import { type AlternateFormat, } from "../config/providers/alternateFormats.ts"; import { - CLAUDE_CLI_BILLING_VERSION, CLAUDE_CLI_STAINLESS_RUNTIME_VERSION, + getClaudeCliBillingVersion, mergeClientAnthropicBeta, normalizeAnthropicHeaderVariants, } from "../config/anthropicHeaders.ts"; @@ -86,7 +86,7 @@ import { } from "../services/contextManager.ts"; import { randomUUID } from "node:crypto"; import { - CLAUDE_CODE_VERSION, + getClaudeCodeVersion, CLAUDE_CODE_STAINLESS_VERSION, buildUserIdJson, getSessionId, @@ -1163,7 +1163,7 @@ export class BaseExecutor { // system[0] (billing) and system[1] (sentinel) must not carry // cache_control — that belongs on upstream prompt blocks at [2..]. - const billingLine = `x-anthropic-billing-header: cc_version=${CLAUDE_CLI_BILLING_VERSION}; cc_entrypoint=cli; cch=00000;`; + const billingLine = `x-anthropic-billing-header: cc_version=${getClaudeCliBillingVersion()}; cc_entrypoint=cli; cch=00000;`; const SENTINEL = "You are Claude Code, Anthropic's official CLI for Claude."; const sysBlocks: Array> = Array.isArray(tb.system) @@ -1259,7 +1259,7 @@ export class BaseExecutor { ), "anthropic-dangerous-direct-browser-access": "true", "x-app": "cli", - "User-Agent": `claude-cli/${CLAUDE_CODE_VERSION} (external, cli)`, + "User-Agent": `claude-cli/${getClaudeCodeVersion()} (external, cli)`, "X-Stainless-Package-Version": CLAUDE_CODE_STAINLESS_VERSION, "X-Stainless-Timeout": "600", "accept-encoding": "gzip, deflate, br, zstd", diff --git a/open-sse/executors/claudeIdentity.ts b/open-sse/executors/claudeIdentity.ts index 78ee1c8b0f..92126d4a45 100644 --- a/open-sse/executors/claudeIdentity.ts +++ b/open-sse/executors/claudeIdentity.ts @@ -13,11 +13,15 @@ import { createHash, randomBytes, randomUUID } from "node:crypto"; import { CLAUDE_CODE_CLIENT_VERSION, CLAUDE_CODE_SDK_PACKAGE_VERSION, + getClaudeCodeClientVersion, } from "@/shared/constants/claudeCodeClient"; // ---------- Versions ------------------------------------------------------ export const CLAUDE_CODE_VERSION = CLAUDE_CODE_CLIENT_VERSION; +export function getClaudeCodeVersion(): string { + return getClaudeCodeClientVersion(); +} /** Bundled @anthropic-ai/sdk version for the pinned CLI release. */ export const CLAUDE_CODE_STAINLESS_VERSION = CLAUDE_CODE_SDK_PACKAGE_VERSION; @@ -156,7 +160,7 @@ export async function fetchClaudeBootstrap(accessToken: string): Promise( + entries: Record, + fn: () => T | Promise +): Promise { + const previous = new Map(); + for (const [key, value] of Object.entries(entries)) { + previous.set(key, process.env[key]); + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + try { + return await fn(); + } finally { + for (const [key, value] of previous.entries()) { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + } +} + +test("#12417 Claude pin stays the captured 2.1.258 binary", () => { + assert.equal(canonical.CLAUDE_CODE_CLIENT_VERSION, "2.1.258"); +}); + +test("#12417 getClaudeCodeClientVersion falls back to the captured pin", async () => { + await withEnv({ CLAUDE_CODE_CLIENT_VERSION: undefined }, () => { + assert.equal(canonical.getClaudeCodeClientVersion(), canonical.CLAUDE_CODE_CLIENT_VERSION); + }); +}); + +test("#12417 getClaudeCodeClientVersion honors a safe env override", async () => { + await withEnv({ CLAUDE_CODE_CLIENT_VERSION: "2.1.259" }, () => { + assert.equal(canonical.getClaudeCodeClientVersion(), "2.1.259"); + assert.equal(canonical.getClaudeCodeUserAgent("cli"), "claude-cli/2.1.259 (external, cli)"); + assert.equal( + canonical.getClaudeCodeUserAgent("sdk-cli"), + "claude-cli/2.1.259 (external, sdk-cli)" + ); + assert.equal( + canonical.getClaudeCodeClientBillingVersion(), + `2.1.259.${canonical.CLAUDE_CODE_CLIENT_BUILD_REVISION}` + ); + }); +}); + +test("#12417 getClaudeCodeClientVersion ignores an unsafe env override", async () => { + await withEnv({ CLAUDE_CODE_CLIENT_VERSION: "bad version value" }, () => { + assert.equal(canonical.getClaudeCodeClientVersion(), canonical.CLAUDE_CODE_CLIENT_VERSION); + }); +}); + +test("#12417 Copilot pin stays the captured 1.0.81-6 CLI", () => { + assert.equal(copilot.GITHUB_COPILOT_CLI_VERSION, "1.0.81-6"); +}); + +test("#12417 getGitHubCopilotCliVersion falls back to the captured pin", async () => { + await withEnv({ GITHUB_COPILOT_CLI_VERSION: undefined }, () => { + assert.equal(copilot.getGitHubCopilotCliVersion(), copilot.GITHUB_COPILOT_CLI_VERSION); + }); +}); + +test("#12417 getGitHubCopilotChatHeaders honors a safe env override", async () => { + await withEnv({ GITHUB_COPILOT_CLI_VERSION: "1.0.82" }, () => { + assert.equal(copilot.getGitHubCopilotCliVersion(), "1.0.82"); + const headers = copilot.getGitHubCopilotChatHeaders(); + assert.equal(headers["user-agent"], "copilot/1.0.82"); + assert.equal(headers["editor-version"], "copilot/1.0.82"); + }); +}); + +test("#12417 getGitHubCopilotCliVersion ignores an unsafe env override", async () => { + await withEnv({ GITHUB_COPILOT_CLI_VERSION: "not a version" }, () => { + assert.equal(copilot.getGitHubCopilotCliVersion(), copilot.GITHUB_COPILOT_CLI_VERSION); + }); +}); + +test("#12417 getClaudeCliHeaders reads the env at call time", async () => { + await withEnv({ CLAUDE_CODE_CLIENT_VERSION: "2.1.259" }, () => { + assert.equal( + claudeHeaders.getClaudeCliHeaders()["User-Agent"], + "claude-cli/2.1.259 (external, cli)" + ); + }); +}); + +test("#12417 applyFingerprint Copilot UA follows the env, pin const does not", async () => { + const fingerprints = await import("../../open-sse/config/cliFingerprints.ts"); + await withEnv({ GITHUB_COPILOT_CLI_VERSION: "1.0.82" }, () => { + const result = fingerprints.applyFingerprint( + "copilot", + { Authorization: "Bearer token", Accept: "application/json" }, + { model: "gpt-4o", messages: [] } + ); + assert.equal(result.headers["User-Agent"], "GitHubCopilotChat/1.0.82"); + assert.equal(copilot.GITHUB_COPILOT_CHAT_USER_AGENT, "GitHubCopilotChat/1.0.81-6"); + }); +}); + +test("#12417 Claude billing pin stays captured while getter follows env", async () => { + const hdr = await import("../../open-sse/config/anthropicHeaders.ts"); + await withEnv({ CLAUDE_CODE_CLIENT_VERSION: "2.1.259" }, () => { + assert.equal(hdr.CLAUDE_CLI_BILLING_VERSION, canonical.CLAUDE_CODE_CLIENT_BILLING_VERSION); + assert.equal( + hdr.getClaudeCliBillingVersion(), + `2.1.259.${canonical.CLAUDE_CODE_CLIENT_BUILD_REVISION}` + ); + }); +});