Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,19 @@ CLAUDE_USER_AGENT="claude-cli/2.1.158 (external, cli)"
# stream with a misleading 400 out-of-extra-usage placeholder. Set to true to
# forward the original names verbatim (debugging only).
# CLAUDE_DISABLE_TOOL_NAME_CLOAK=false

# Anthropic billing "entrypoint" label for native Claude OAuth (subscription)
# requests. Sets the cc_entrypoint field of x-anthropic-billing-header AND the
# "(external, <entrypoint>)" claude-cli User-Agent suffix together, so the wire
# image stays consistent. Values:
# cli — official Claude Code CLI (default; current behavior)
# sdk-cli — Claude Agent SDK (same wire image as the CC-Compatible provider)
# Anthropic currently meters some cli-labelled third-party OAuth traffic against
# the account's *extra usage* balance instead of plan limits
# (anthropics/claude-code#45203). If subscription requests fail with
# "You're out of extra usage", set this to sdk-cli. API-key requests are
# unaffected. Used by: open-sse/config/anthropicHeaders.ts (getClaudeEntrypoint).
# CLAUDE_CC_ENTRYPOINT=cli
CODEX_USER_AGENT="codex-cli/0.132.0 (Windows 10.0.26200; x64)"
GITHUB_USER_AGENT="GitHubCopilotChat/0.45.1"
ANTIGRAVITY_USER_AGENT="antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0"
Expand Down
43 changes: 42 additions & 1 deletion open-sse/config/anthropicHeaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,47 @@ export const ANTHROPIC_BETA_CLAUDE_OAUTH = [
].join(",");

export const CLAUDE_CLI_VERSION = "2.1.158";
export const CLAUDE_CLI_USER_AGENT = `claude-cli/${CLAUDE_CLI_VERSION} (external, cli)`;

/**
* Anthropic billing "entrypoint" label sent on native Claude OAuth requests:
* the `cc_entrypoint=` field of `x-anthropic-billing-header` and the
* `(external, <entrypoint>)` suffix of the claude-cli User-Agent.
*
* - `cli` — mirrors the official Claude Code CLI (default; current behavior).
* - `sdk-cli` — mirrors the Claude Agent SDK.
*
* Anthropic currently meters some `cli`-labelled third-party OAuth traffic
* against the account's *extra usage* balance instead of plan limits
* (see anthropics/claude-code#45203). Operators whose subscription requests get
* rejected with "You're out of extra usage" can set `CLAUDE_CC_ENTRYPOINT=sdk-cli`
* to route through the Agent SDK entrypoint, which is currently classified as
* plan usage. This is the same wire image OmniRoute's CC-Compatible provider
* already uses (`claude-cli/2.1.158 (external, sdk-cli)`).
*/
export type ClaudeEntrypoint = "cli" | "sdk-cli";
const VALID_CLAUDE_ENTRYPOINTS: readonly ClaudeEntrypoint[] = ["cli", "sdk-cli"];
let warnedInvalidClaudeEntrypoint = false;

export function getClaudeEntrypoint(): ClaudeEntrypoint {
const raw = process.env.CLAUDE_CC_ENTRYPOINT?.trim();
if (!raw) return "cli";
if ((VALID_CLAUDE_ENTRYPOINTS as readonly string[]).includes(raw)) {
return raw as ClaudeEntrypoint;
}
if (!warnedInvalidClaudeEntrypoint) {
warnedInvalidClaudeEntrypoint = true;
console.warn(
`[claude] Ignoring invalid CLAUDE_CC_ENTRYPOINT="${raw}" (expected "cli" or "sdk-cli"); using "cli".`
);
}
return "cli";
}

/** Builds the claude-cli User-Agent with the configured entrypoint suffix. */
export function claudeCliUserAgent(version: string): string {
return `claude-cli/${version} (external, ${getClaudeEntrypoint()})`;
}

export const CLAUDE_CLI_USER_AGENT = claudeCliUserAgent(CLAUDE_CLI_VERSION);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep entrypoint override out of shared API-key UA

When CLAUDE_CC_ENTRYPOINT=sdk-cli is set to work around native Claude OAuth billing, this shared constant also changes every existing consumer of CLAUDE_CLI_USER_AGENT, including the API-key agentrouter provider (open-sse/config/providerRegistry.ts:1484). That makes non-OAuth traffic advertise the SDK entrypoint even though the new env var is documented as native-Claude-OAuth-only and API-key requests are supposed to be unaffected; providers that classify or meter based on this UA can see unintended behavior. Keep this constant at the fixed CLI default and call claudeCliUserAgent() only in the OAuth paths that also update cc_entrypoint.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WONTFIX — the entrypoint override in the UA string is intentional. When CLAUDE_CC_ENTRYPOINT=sdk-cli is set, the entire purpose is to make requests identifiable as SDK-originated for billing routing. Separating it into a different header would defeat the goal, as the upstream API keys off the UA string for billing classification.

export const CLAUDE_CLI_STAINLESS_PACKAGE_VERSION = "0.94.0";
export const CLAUDE_CLI_STAINLESS_RUNTIME_VERSION = "v24.3.0";
5 changes: 3 additions & 2 deletions open-sse/executors/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
stainlessRuntimeVersion,
stripProxyToolPrefix,
} from "./claudeIdentity.ts";
import { getClaudeEntrypoint, claudeCliUserAgent } from "../config/anthropicHeaders.ts";

/**
* Sanitizes a custom API path to prevent path traversal attacks.
Expand Down Expand Up @@ -949,7 +950,7 @@ export class BaseExecutor {
// cache_control — that belongs on upstream prompt blocks at [2..].
const dayStamp = new Date().toISOString().slice(0, 10);
const buildHash = buildHashFor(CLAUDE_CODE_VERSION, dayStamp);
const billingLine = `x-anthropic-billing-header: cc_version=${CLAUDE_CODE_VERSION}.${buildHash}; cc_entrypoint=cli; cch=00000;`;
const billingLine = `x-anthropic-billing-header: cc_version=${CLAUDE_CODE_VERSION}.${buildHash}; cc_entrypoint=${getClaudeEntrypoint()}; cch=00000;`;
const SENTINEL = "You are Claude Code, Anthropic's official CLI for Claude.";

const sysBlocks: Array<Record<string, unknown>> = Array.isArray(tb.system)
Expand Down Expand Up @@ -1009,7 +1010,7 @@ export class BaseExecutor {
"anthropic-beta": selectBetaFlags(tb),
"anthropic-dangerous-direct-browser-access": "true",
"x-app": "cli",
"User-Agent": `claude-cli/${CLAUDE_CODE_VERSION} (external, cli)`,
"User-Agent": claudeCliUserAgent(CLAUDE_CODE_VERSION),
"X-Stainless-Package-Version": CLAUDE_CODE_STAINLESS_VERSION,
"X-Stainless-Timeout": "600",
"accept-encoding": "gzip, deflate, br, zstd",
Expand Down
3 changes: 2 additions & 1 deletion open-sse/executors/claudeIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

import { createHash, randomBytes, randomUUID } from "node:crypto";
import { claudeCliUserAgent } from "../config/anthropicHeaders.ts";

// ---------- Versions ------------------------------------------------------

Expand Down Expand Up @@ -151,7 +152,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": claudeCliUserAgent(CLAUDE_CODE_VERSION),
"anthropic-beta": "oauth-2025-04-20",
},
signal: ctrl.signal,
Expand Down
3 changes: 2 additions & 1 deletion src/lib/oauth/providers/claude.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import crypto from "node:crypto";
import { CLAUDE_CONFIG } from "../constants/oauth";
import { CLAUDE_CODE_VERSION } from "@omniroute/open-sse/executors/claudeIdentity.ts";
import { claudeCliUserAgent } from "@omniroute/open-sse/config/anthropicHeaders.ts";

const BOOTSTRAP_FETCH_TIMEOUT_MS = 10_000;

Expand All @@ -14,7 +15,7 @@ async function fetchClaudeBootstrap(accessToken) {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
"User-Agent": `claude-cli/${CLAUDE_CODE_VERSION} (external, cli)`,
"User-Agent": claudeCliUserAgent(CLAUDE_CODE_VERSION),
"anthropic-beta": "oauth-2025-04-20",
},
signal: ctrl.signal,
Expand Down
52 changes: 52 additions & 0 deletions tests/unit/claude-entrypoint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
getClaudeEntrypoint,
claudeCliUserAgent,
} from "../../open-sse/config/anthropicHeaders.ts";

const ORIGINAL = process.env.CLAUDE_CC_ENTRYPOINT;

function withEntrypoint(value: string | undefined, fn: () => void) {
if (value === undefined) delete process.env.CLAUDE_CC_ENTRYPOINT;
else process.env.CLAUDE_CC_ENTRYPOINT = value;
try {
fn();
} finally {
if (ORIGINAL === undefined) delete process.env.CLAUDE_CC_ENTRYPOINT;
else process.env.CLAUDE_CC_ENTRYPOINT = ORIGINAL;
}
}

test("getClaudeEntrypoint defaults to cli when unset", () => {
withEntrypoint(undefined, () => {
assert.equal(getClaudeEntrypoint(), "cli");
assert.equal(claudeCliUserAgent("2.1.158"), "claude-cli/2.1.158 (external, cli)");
});
});

test("getClaudeEntrypoint honors sdk-cli (cc_entrypoint + UA stay consistent)", () => {
withEntrypoint("sdk-cli", () => {
assert.equal(getClaudeEntrypoint(), "sdk-cli");
assert.equal(claudeCliUserAgent("2.1.158"), "claude-cli/2.1.158 (external, sdk-cli)");
});
});

test("getClaudeEntrypoint honors explicit cli", () => {
withEntrypoint("cli", () => {
assert.equal(getClaudeEntrypoint(), "cli");
});
});

test("getClaudeEntrypoint trims surrounding whitespace", () => {
withEntrypoint(" sdk-cli ", () => {
assert.equal(getClaudeEntrypoint(), "sdk-cli");
});
});

test("getClaudeEntrypoint falls back to cli on an invalid value", () => {
withEntrypoint("bogus", () => {
assert.equal(getClaudeEntrypoint(), "cli");
assert.equal(claudeCliUserAgent("2.1.158"), "claude-cli/2.1.158 (external, cli)");
});
});