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:
Bob.Hou
2026-09-03 22:43:38 -04:00
committed by GitHub
parent 57d7c8bc88
commit 36be267a17
21 changed files with 257 additions and 29 deletions

View File

@@ -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

View 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))

View File

@@ -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 |

View File

@@ -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;

View File

@@ -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"];

View File

@@ -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: [

View File

@@ -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",

View File

@@ -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}`,
};
}

View File

@@ -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";

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,

View File

@@ -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 =

View File

@@ -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",

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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() : {};

View File

@@ -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() : {};

View File

@@ -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})`;
}

View 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}`
);
});
});