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 @@ -1066,6 +1066,19 @@ CLAUDE_USER_AGENT="claude-cli/2.1.219 (external, cli)"
# CLAUDE_DISABLE_TOOL_NAME_CLOAK=false
CODEX_USER_AGENT="codex-cli/0.144.1 (Windows 10.0.26200; x64)"
GITHUB_USER_AGENT="GitHubCopilotChat/0.54.0"

# 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
ANTIGRAVITY_USER_AGENT="antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0"
KIRO_USER_AGENT="AWS-SDK-JS/3.0.0 kiro-ide/1.0.0"
# KIRO_VERIFY_FULL_CRC=false # opt-in: full per-frame message CRC validation on the Kiro event stream (debug corrupted streams; prelude CRC + TLS already protect framing)
Expand Down
47 changes: 47 additions & 0 deletions open-sse/config/anthropicHeaders.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
type ClaudeCodeEntrypoint,
CLAUDE_CODE_CLIENT_BILLING_VERSION,
CLAUDE_CODE_CLIENT_BUILD_REVISION,
CLAUDE_CODE_CLIENT_VERSION,
Expand Down Expand Up @@ -141,6 +142,52 @@ export function normalizeAnthropicHeaderVariants(headers: Record<string, string>
export const CLAUDE_CLI_VERSION = CLAUDE_CODE_CLIENT_VERSION;
export const CLAUDE_CLI_BUILD_REVISION = CLAUDE_CODE_CLIENT_BUILD_REVISION;
export const CLAUDE_CLI_BILLING_VERSION = CLAUDE_CODE_CLIENT_BILLING_VERSION;

/**
* 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.
*
* FORK PATCH (CLAUDE_CC_ENTRYPOINT). Upstream >=3.8.49 owns the wire constants
* and the `ClaudeCodeEntrypoint` type + `getClaudeCodeUserAgent(entrypoint)`
* builder; this only adds the env-var override on top of them.
*/
export type ClaudeEntrypoint = ClaudeCodeEntrypoint;
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(): string {
return getClaudeCodeUserAgent(getClaudeEntrypoint());
}
Comment thread
cursor[bot] marked this conversation as resolved.

// Static registry/API-key identity remains the official CLI wire image.
// CLAUDE_CC_ENTRYPOINT applies only at native Claude OAuth call sites.
export const CLAUDE_CLI_USER_AGENT = getClaudeCodeUserAgent("cli");
export const CLAUDE_CLI_STAINLESS_PACKAGE_VERSION = CLAUDE_CODE_SDK_PACKAGE_VERSION;
export const CLAUDE_CLI_STAINLESS_RUNTIME_VERSION = CLAUDE_CODE_RUNTIME_VERSION;
11 changes: 9 additions & 2 deletions open-sse/executors/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ import { sanitizeReasoningEffortForProvider } from "./base/reasoningEffort.ts";
// Reasoning-effort sanitation extracted to a pure leaf; re-exported for external
// importers (mimoThinking service + tests) that import it from "./base.ts".
export { sanitizeReasoningEffortForProvider } from "./base/reasoningEffort.ts";
import { getClaudeEntrypoint, claudeCliUserAgent } from "../config/anthropicHeaders.ts";

/**
* Sanitizes a custom API path to prevent path traversal attacks.
Expand Down Expand Up @@ -1125,7 +1126,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=${CLAUDE_CLI_BILLING_VERSION}; 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 @@ -1195,7 +1196,7 @@ export class BaseExecutor {
),
"anthropic-dangerous-direct-browser-access": "true",
"x-app": "cli",
"User-Agent": `claude-cli/${CLAUDE_CODE_VERSION} (external, cli)`,
"User-Agent": claudeCliUserAgent(),
Comment thread
cursor[bot] marked this conversation as resolved.
"X-Stainless-Package-Version": CLAUDE_CODE_STAINLESS_VERSION,
"X-Stainless-Timeout": "600",
"accept-encoding": "gzip, deflate, br, zstd",
Expand Down Expand Up @@ -1329,6 +1330,12 @@ export class BaseExecutor {
}

mergeUpstreamExtraHeaders(finalHeaders, upstreamExtraHeaders);
// The OAuth billing entrypoint and Claude CLI User-Agent are one wire
// identity. Operator/model extra headers are merged above for all
// providers, but must not split those two fields on native Claude OAuth.
if (this.provider === "claude" && hasClaudeOAuthToken) {
setUserAgentHeader(finalHeaders, claudeCliUserAgent());
}
if (this.provider === "cline" || this.provider === "clinepass") {
applyClineProtocolHeaders(finalHeaders, {
taskId: headers["X-Task-ID"],
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";

import {
CLAUDE_CODE_CLIENT_VERSION,
Expand Down Expand Up @@ -156,7 +157,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(),
"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(),
"anthropic-beta": "oauth-2025-04-20",
},
signal: ctrl.signal,
Expand Down
59 changes: 59 additions & 0 deletions tests/unit/claude-entrypoint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
CLAUDE_CLI_USER_AGENT,
CLAUDE_CLI_VERSION,
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(), `claude-cli/${CLAUDE_CLI_VERSION} (external, cli)`);
});
});

test("getClaudeEntrypoint honors sdk-cli (cc_entrypoint + UA stay consistent)", () => {
withEntrypoint("sdk-cli", () => {
assert.equal(getClaudeEntrypoint(), "sdk-cli");
assert.equal(claudeCliUserAgent(), `claude-cli/${CLAUDE_CLI_VERSION} (external, sdk-cli)`);
assert.equal(
CLAUDE_CLI_USER_AGENT,
`claude-cli/${CLAUDE_CLI_VERSION} (external, cli)`,
"static provider headers must remain CLI-labelled"
);
});
});

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(), `claude-cli/${CLAUDE_CLI_VERSION} (external, cli)`);
});
});
Loading