mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 10:52:17 +03:00
fix(providers): add CLAUDE_CODE_CLIENT_VERSION and GITHUB_COPILOT_CLI_VERSION env overrides (#12632)
Validado em lote numa worktree combinada com os 4 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo e **119/119** nos 9 arquivos de teste que trazem. Três dos quatro conflitavam apenas no `config/quality/file-size-baseline.json`, todos de forma aditiva (chaves `_rebaseline_` distintas que devem coexistir); resolvidos com validação de JSON a cada passo. Registro que o **#12637 não é duplicata do #12566**, apesar do título quase idêntico: o autor documenta que aquele escopou o cooldown de preflight por família e este cobre o `genericQuotaFetcher`, que é o que o roteamento reset-aware efetivamente chama. Traz também validação ao vivo em VPS (imagem X500, `onmi-gemini3.6` → HTTP 200), satisfazendo a Hard Rule #18. Obrigado, @HouMinXi.
This commit is contained in:
10
.env.example
10
.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
|
||||
|
||||
1
changelog.d/fixes/12417-cli-version-env.md
Normal file
1
changelog.d/fixes/12417-cli-version-env.md
Normal file
@@ -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))
|
||||
@@ -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 |
|
||||
|
||||
@@ -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<string, string>
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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<string, CliFingerprint> = {
|
||||
"intent_threshold",
|
||||
"intent_content",
|
||||
],
|
||||
userAgent: GITHUB_COPILOT_CHAT_USER_AGENT,
|
||||
userAgent: getGitHubCopilotChatUserAgent,
|
||||
},
|
||||
antigravity: {
|
||||
headerOrder: [
|
||||
|
||||
@@ -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<strin
|
||||
"anthropic-version": ANTHROPIC_VERSION_HEADER,
|
||||
"anthropic-beta": GLM_ANTHROPIC_BETA,
|
||||
"anthropic-dangerous-direct-browser-access": "true",
|
||||
"User-Agent": GLM_CLAUDE_CODE_USER_AGENT,
|
||||
"User-Agent": getClaudeCodeUserAgent("sdk-cli"),
|
||||
"X-Stainless-Lang": "js",
|
||||
"X-Stainless-Runtime": "node",
|
||||
"X-Stainless-Retry-Count": "0",
|
||||
|
||||
@@ -9,12 +9,38 @@ import type { AntigravityClientProfile } from "@/shared/constants/antigravityCli
|
||||
// returns a narrower list. Version strings track the live-captured CLI 1.0.81-6.
|
||||
export const GITHUB_COPILOT_API_VERSION = "2026-08-01";
|
||||
export const GITHUB_COPILOT_CLI_VERSION = "1.0.81-6";
|
||||
const GITHUB_COPILOT_VERSION_OVERRIDE_ENV = "GITHUB_COPILOT_CLI_VERSION";
|
||||
const SAFE_COPILOT_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$/;
|
||||
|
||||
function getSafeCopilotEnvValue(name: string, pattern: RegExp): string | null {
|
||||
const raw = typeof process === "undefined" ? undefined : process.env?.[name];
|
||||
if (typeof raw !== "string") return null;
|
||||
const normalized = raw.trim();
|
||||
if (!normalized || !pattern.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/** Captured pin, overridable via GITHUB_COPILOT_CLI_VERSION (#12417). */
|
||||
export function getGitHubCopilotCliVersion(): string {
|
||||
return (
|
||||
getSafeCopilotEnvValue(GITHUB_COPILOT_VERSION_OVERRIDE_ENV, SAFE_COPILOT_VERSION_PATTERN) ||
|
||||
GITHUB_COPILOT_CLI_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
export const GITHUB_COPILOT_EDITOR_VERSION = `copilot/${GITHUB_COPILOT_CLI_VERSION}`;
|
||||
export const GITHUB_COPILOT_CHAT_PLUGIN_VERSION = `copilot-chat/${GITHUB_COPILOT_CLI_VERSION}`;
|
||||
export const GITHUB_COPILOT_CHAT_USER_AGENT = `GitHubCopilotChat/${GITHUB_COPILOT_CLI_VERSION}`;
|
||||
export const GITHUB_COPILOT_CLI_USER_AGENT = `copilot/${GITHUB_COPILOT_CLI_VERSION}`;
|
||||
export const GITHUB_COPILOT_REFRESH_PLUGIN_VERSION = `copilot/${GITHUB_COPILOT_CLI_VERSION}`;
|
||||
export const GITHUB_COPILOT_REFRESH_USER_AGENT = "GithubCopilot/1.0";
|
||||
|
||||
/** Request-time Copilot Chat UA. Pin consts above stay for lockstep tests (#12417). */
|
||||
export function getGitHubCopilotChatUserAgent(): string {
|
||||
return `GitHubCopilotChat/${getGitHubCopilotCliVersion()}`;
|
||||
}
|
||||
export const GITHUB_COPILOT_INTEGRATION_ID = "copilot-developer-cli";
|
||||
export const GITHUB_COPILOT_OPENAI_INTENT = "conversation-agent";
|
||||
export const GITHUB_COPILOT_INTERACTION_TYPE = "conversation-user";
|
||||
@@ -62,10 +88,11 @@ export function getGitHubCopilotChatHeaders(
|
||||
// send exactly the CLI's set. The `copilot-integration-id` (copilot-developer-cli)
|
||||
// is the catalog-unlock lever; the stable X-Client-Machine-Id is the CLI's
|
||||
// per-install device fingerprint.
|
||||
const version = getGitHubCopilotCliVersion();
|
||||
const headers: Record<string, string> = {
|
||||
"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<string, string> {
|
||||
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<string, string> {
|
||||
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}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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<string, string> {
|
||||
"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",
|
||||
|
||||
@@ -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<Record<string, unknown>> = 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",
|
||||
|
||||
@@ -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<ClaudeB
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"User-Agent": `claude-cli/${CLAUDE_CODE_VERSION} (external, cli)`,
|
||||
"User-Agent": `claude-cli/${getClaudeCodeVersion()} (external, cli)`,
|
||||
"anthropic-beta": "oauth-2025-04-20",
|
||||
},
|
||||
signal: ctrl.signal,
|
||||
|
||||
@@ -24,6 +24,7 @@ import { createHash } from "node:crypto";
|
||||
import {
|
||||
CLAUDE_CODE_CLIENT_BUILD_REVISION,
|
||||
CLAUDE_CODE_CLIENT_VERSION,
|
||||
getClaudeCodeClientVersion,
|
||||
} from "@/shared/constants/claudeCodeClient";
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
@@ -122,6 +123,9 @@ export const CCH_SALT = "59cf53e54c78";
|
||||
export const CCH_POSITIONS = [4, 7, 20] as const;
|
||||
/** Default `cc_version=` value embedded in the billing header. */
|
||||
export const DEFAULT_CLAUDE_CODE_VERSION = CLAUDE_CODE_CLIENT_VERSION;
|
||||
export function getDefaultClaudeCodeVersion(): string {
|
||||
return getClaudeCodeClientVersion();
|
||||
}
|
||||
/** Identity sentinel prepended for Claude Agent SDK callers. */
|
||||
export const CLAUDE_AGENT_SDK_IDENTITY =
|
||||
"You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
||||
@@ -292,7 +296,7 @@ export function buildBillingHeaderValue(
|
||||
messages: Message[],
|
||||
options: BuildBillingHeaderOptions
|
||||
): string {
|
||||
const version = options.version || DEFAULT_CLAUDE_CODE_VERSION;
|
||||
const version = options.version || getDefaultClaudeCodeVersion();
|
||||
const firstUserText = extractFirstUserMessageText(messages);
|
||||
|
||||
const suffix =
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ANTHROPIC_VERSION_HEADER } from "../config/anthropicHeaders.ts";
|
||||
import {
|
||||
CLAUDE_CODE_COMPATIBLE_STAINLESS_PACKAGE_VERSION,
|
||||
CLAUDE_CODE_COMPATIBLE_STAINLESS_RUNTIME_VERSION,
|
||||
CLAUDE_CODE_COMPATIBLE_USER_AGENT,
|
||||
getClaudeCodeUserAgent,
|
||||
} from "../config/claudeCodeCompatibleIdentity.ts";
|
||||
import { supportsClaudeMaxEffort, supportsXHighEffort } from "../config/providerModels.ts";
|
||||
import { prepareClaudeRequest } from "../translator/helpers/claudeHelper.ts";
|
||||
@@ -183,7 +183,7 @@ export function buildClaudeCodeCompatibleHeaders(
|
||||
}),
|
||||
"anthropic-dangerous-direct-browser-access": "true",
|
||||
"x-app": "cli",
|
||||
"User-Agent": CLAUDE_CODE_COMPATIBLE_USER_AGENT,
|
||||
"User-Agent": getClaudeCodeUserAgent("sdk-cli"),
|
||||
"X-Stainless-Retry-Count": "0",
|
||||
"X-Stainless-Timeout": String(CLAUDE_CODE_COMPATIBLE_STAINLESS_TIMEOUT_SECONDS),
|
||||
"X-Stainless-Lang": "js",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
*/
|
||||
|
||||
import { safePercentage } from "@/shared/utils/formatting";
|
||||
import { CLAUDE_CODE_VERSION, fetchClaudeBootstrap } from "../../executors/claudeIdentity.ts";
|
||||
import { getClaudeCodeVersion, fetchClaudeBootstrap } from "../../executors/claudeIdentity.ts";
|
||||
import { isClaudeOauthUsageCoolingDown, markClaudeOauthUsage429 } from "../claudeUsageCooldown.ts";
|
||||
import { toRecord } from "./scalars.ts";
|
||||
import { type UsageQuota, parseResetTime } from "./quota.ts";
|
||||
@@ -71,7 +71,7 @@ export async function getClaudeUsage(accessToken?: string) {
|
||||
"Accept-Encoding": "gzip, compress, deflate, br",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": `claude-code/${CLAUDE_CODE_VERSION}`,
|
||||
"User-Agent": `claude-code/${getClaudeCodeVersion()}`,
|
||||
"anthropic-beta": "oauth-2025-04-20",
|
||||
},
|
||||
signal: ctrl.signal,
|
||||
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
GITHUB_COPILOT_CHAT_USER_AGENT,
|
||||
GITHUB_COPILOT_EDITOR_VERSION,
|
||||
} from "@omniroute/open-sse/config/providerHeaderProfiles.ts";
|
||||
// userAgent / editorVersion on GITHUB_CONFIG are captured-pin snapshots for
|
||||
// lockstep tests. Request construction must call getGitHubCopilotChatUserAgent()
|
||||
// (#12417) — see providers/github.ts and providers/ghe-copilot.ts.
|
||||
import {
|
||||
GROK_BUILD_DEVICE_CODE_URL,
|
||||
GROK_BUILD_OAUTH_ISSUER,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from "node:crypto";
|
||||
import { CLAUDE_CONFIG } from "../constants/oauth";
|
||||
import { CLAUDE_CODE_VERSION } from "@omniroute/open-sse/executors/claudeIdentity.ts";
|
||||
import { getClaudeCodeVersion } from "@omniroute/open-sse/executors/claudeIdentity.ts";
|
||||
|
||||
const BOOTSTRAP_FETCH_TIMEOUT_MS = 10_000;
|
||||
|
||||
@@ -14,7 +14,7 @@ async function fetchClaudeBootstrap(accessToken) {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"User-Agent": `claude-cli/${CLAUDE_CODE_VERSION} (external, cli)`,
|
||||
"User-Agent": `claude-cli/${getClaudeCodeVersion()} (external, cli)`,
|
||||
"anthropic-beta": "oauth-2025-04-20",
|
||||
},
|
||||
signal: ctrl.signal,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getGitHubCopilotChatUserAgent } from "@omniroute/open-sse/config/providerHeaderProfiles.ts";
|
||||
import { GHE_COPILOT_CONFIG } from "../constants/oauth";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
@@ -72,7 +73,7 @@ export const gheCopilot = {
|
||||
Authorization: `Bearer ${tokens.access_token}`,
|
||||
Accept: "application/json",
|
||||
"X-GitHub-Api-Version": GHE_COPILOT_CONFIG.apiVersion,
|
||||
"User-Agent": GHE_COPILOT_CONFIG.userAgent,
|
||||
"User-Agent": getGitHubCopilotChatUserAgent(),
|
||||
},
|
||||
});
|
||||
const copilotToken = copilotRes.ok ? await copilotRes.json() : {};
|
||||
@@ -81,7 +82,7 @@ export const gheCopilot = {
|
||||
Authorization: `Bearer ${tokens.access_token}`,
|
||||
Accept: "application/json",
|
||||
"X-GitHub-Api-Version": GHE_COPILOT_CONFIG.apiVersion,
|
||||
"User-Agent": GHE_COPILOT_CONFIG.userAgent,
|
||||
"User-Agent": getGitHubCopilotChatUserAgent(),
|
||||
},
|
||||
});
|
||||
const userInfo = userRes.ok ? await userRes.json() : {};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getGitHubCopilotChatUserAgent } from "@omniroute/open-sse/config/providerHeaderProfiles.ts";
|
||||
import { GITHUB_CONFIG } from "../constants/oauth";
|
||||
|
||||
export const github = {
|
||||
@@ -56,7 +57,7 @@ export const github = {
|
||||
Authorization: `Bearer ${tokens.access_token}`,
|
||||
Accept: "application/json",
|
||||
"X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion,
|
||||
"User-Agent": GITHUB_CONFIG.userAgent,
|
||||
"User-Agent": getGitHubCopilotChatUserAgent(),
|
||||
},
|
||||
});
|
||||
const copilotToken = copilotRes.ok ? await copilotRes.json() : {};
|
||||
@@ -66,7 +67,7 @@ export const github = {
|
||||
Authorization: `Bearer ${tokens.access_token}`,
|
||||
Accept: "application/json",
|
||||
"X-GitHub-Api-Version": GITHUB_CONFIG.apiVersion,
|
||||
"User-Agent": GITHUB_CONFIG.userAgent,
|
||||
"User-Agent": getGitHubCopilotChatUserAgent(),
|
||||
},
|
||||
});
|
||||
const userInfo = userRes.ok ? await userRes.json() : {};
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
*
|
||||
* Keep this leaf dependency-free so server executors, compatibility bridges,
|
||||
* and client-facing identity presets can share one source of truth.
|
||||
*
|
||||
* `CLAUDE_CODE_CLIENT_VERSION` is the captured pin. Runtime callers that
|
||||
* advertise the version on the wire must go through getClaudeCodeClientVersion()
|
||||
* so operators can bump past Anthropic's model gate without a rebuild (#12417).
|
||||
*/
|
||||
export const CLAUDE_CODE_CLIENT_VERSION = "2.1.258";
|
||||
export const CLAUDE_CODE_CLIENT_BUILD_REVISION = "1e2";
|
||||
@@ -12,6 +16,27 @@ export const CLAUDE_CODE_RUNTIME_VERSION = "v26.3.0";
|
||||
|
||||
export type ClaudeCodeEntrypoint = "cli" | "sdk-cli";
|
||||
|
||||
const CLAUDE_VERSION_OVERRIDE_ENV = "CLAUDE_CODE_CLIENT_VERSION";
|
||||
const SAFE_HEADER_TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$/;
|
||||
|
||||
function getSafeEnvValue(name: string, pattern: RegExp): string | null {
|
||||
const raw = typeof process === "undefined" ? undefined : process.env?.[name];
|
||||
if (typeof raw !== "string") return null;
|
||||
const normalized = raw.trim();
|
||||
if (!normalized || !pattern.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function getClaudeCodeClientVersion(): string {
|
||||
return getSafeEnvValue(CLAUDE_VERSION_OVERRIDE_ENV, SAFE_HEADER_TOKEN_PATTERN) || CLAUDE_CODE_CLIENT_VERSION;
|
||||
}
|
||||
|
||||
export function getClaudeCodeClientBillingVersion(): string {
|
||||
return `${getClaudeCodeClientVersion()}.${CLAUDE_CODE_CLIENT_BUILD_REVISION}`;
|
||||
}
|
||||
|
||||
export function getClaudeCodeUserAgent(entrypoint: ClaudeCodeEntrypoint): string {
|
||||
return `claude-cli/${CLAUDE_CODE_CLIENT_VERSION} (external, ${entrypoint})`;
|
||||
return `claude-cli/${getClaudeCodeClientVersion()} (external, ${entrypoint})`;
|
||||
}
|
||||
|
||||
130
tests/unit/cli-client-version-env-12417.test.ts
Normal file
130
tests/unit/cli-client-version-env-12417.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Regression for #12417 — Anthropic gates models (Fable 5.1) on the advertised
|
||||
* Claude Code client version. Codex already has CODEX_CLIENT_VERSION; Claude
|
||||
* and Copilot did not. A CLAUDE_USER_AGENT override is not enough: billing
|
||||
* (`cc_version=`), stainless headers, and the four identity aliases all read
|
||||
* the captured pin.
|
||||
*
|
||||
* Env override is a safe token (same shape as Codex). Garbage is ignored.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const canonical = await import("../../src/shared/constants/claudeCodeClient.ts");
|
||||
const copilot = await import("../../open-sse/config/providerHeaderProfiles.ts");
|
||||
const claudeHeaders = await import("../../open-sse/config/providers/shared.ts");
|
||||
|
||||
async function withEnv<T>(
|
||||
entries: Record<string, string | undefined>,
|
||||
fn: () => T | Promise<T>
|
||||
): Promise<T> {
|
||||
const previous = new Map<string, string | undefined>();
|
||||
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}`
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user