feat(sse): refresh Claude OAuth wire image to claude-cli/2.1.131#2011
Conversation
- Per-OAuth-account identity (cliUserID, accountUUID, sessionId) generated
with the same crypto.randomBytes(32) algorithm real CLI uses, persisted
to providerSpecificData via mapTokens. Token refresh preserves the
device id (the device is the machine, not the credential).
- postExchange fetches /api/claude_cli/bootstrap to populate accountUUID,
email, and organization metadata at OAuth provisioning. Best-effort:
bootstrap failures do not block OAuth.
- Adaptive anthropic-beta selection per request shape (probe / structured
/ full-agent) replaces the static set, matching what real CLI emits
conditionally on body shape.
- Cloak gate widened to cover OAuth tokens (sk-ant-oat) regardless of
upstream client, so non-CLI clients (ForgeCode, Cursor, Cline) routing
through OmniRoute on a Claude Max OAuth credential still emit
CLI-shaped traffic on the user:sessions:claude_code scope.
- Per-request behavior overrides via x-omniroute-effort and
x-omniroute-thinking custom headers. Auto-mirror context_management
whenever thinking is set but context_management isn't, matching the
real-CLI invariant.
- output_format -> output_config.format migration in chatCore for clients
still emitting the deprecated top-level field (Anthropic now returns 400
on it).
- Validation routes Claude OAuth probes through getExecutor("claude")
instead of duplicating the cloak — single source of truth.
- Header order in cliFingerprints matches real-CLI emission (Title-Case
alphabetical, then lowercase alphabetical, then transport).
- Tool cache_control kept on the translator path; cloak strips it for
Claude OAuth requests (real CLI never sets cache_control on tools).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request implements a comprehensive "cloak" for Claude OAuth traffic to ensure it mimics the official Anthropic CLI, addressing issues with rate limiting and prompt cache affinity. Key changes include the introduction of claudeIdentity.ts for session and device ID management, updating the pinned CLI version to 2.1.131, and refining header and body field ordering to match official fingerprints. Feedback focuses on ensuring fingerprinting is applied to all OAuth requests, managing memory for identity caches, adding timeouts to background fetch operations, and centralizing version constants for better maintainability.
| const shouldFingerprint = | ||
| isCliCompatEnabled(this.provider) || | ||
| (this.provider === "claude" && isClaudeCodeClient); |
There was a problem hiding this comment.
The fingerprinting logic (which controls header and body field ordering) should be applied to all Claude OAuth requests to ensure they are indistinguishable from official CLI traffic. Currently, this is only triggered if isClaudeCodeClient is true, but the 'cloak' logic earlier in this method (lines 519-718) correctly applies to any hasClaudeOAuthToken. Missing this ordering on non-CLI clients using OAuth tokens increases the risk of 429 errors on the claude_code scope.
| const shouldFingerprint = | |
| isCliCompatEnabled(this.provider) || | |
| (this.provider === "claude" && isClaudeCodeClient); | |
| const shouldFingerprint = | |
| isCliCompatEnabled(this.provider) || | |
| (this.provider === "claude" && (isClaudeCodeClient || hasClaudeOAuthToken)); |
|
|
||
| // ---------- Session ID (per OAuth account, process lifetime) ------------- | ||
|
|
||
| const sessionCache = new Map<string, string>(); |
There was a problem hiding this comment.
The sessionCache, lazyCliUserIDCache, and accountUuidCache maps are unbounded and will grow indefinitely as new OAuth tokens or seeds are processed. In a long-running server environment, this constitutes a memory leak. Consider using an LRU cache or a Map with a maximum size limit to prune old entries.
| const res = await fetch("https://api.anthropic.com/api/claude_cli/bootstrap", { | ||
| method: "GET", | ||
| headers: { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| Accept: "application/json", | ||
| "User-Agent": `claude-cli/${CLAUDE_CODE_VERSION} (external, cli)`, | ||
| "anthropic-beta": "oauth-2025-04-20", | ||
| }, |
| const res = await fetch("https://api.anthropic.com/api/claude_cli/bootstrap", { | ||
| method: "GET", | ||
| headers: { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| Accept: "application/json", | ||
| "User-Agent": "claude-cli/2.1.131 (external, cli)", | ||
| "anthropic-beta": "oauth-2025-04-20", | ||
| }, |
| headers: { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| Accept: "application/json", | ||
| "User-Agent": "claude-cli/2.1.131 (external, cli)", |
- Widen `shouldFingerprint` gate to also fire on `hasClaudeOAuthToken`,
matching the cloak gate. Fixes ordering on non-CLI OAuth clients.
- Bound identity caches (sessionCache, lazyCliUserIDCache,
accountUuidCache) with FIFO eviction at 10k entries to prevent
unbounded growth in long-running multi-tenant deployments.
- Add 10s AbortController timeout to both bootstrap fetches
(claudeIdentity background path and oauth/providers/claude
provisioning path) so a hung upstream cannot leak sockets or stall
OAuth completion.
- Replace hardcoded "claude-cli/2.1.131" UA in oauth/providers/claude.ts
with the CLAUDE_CODE_VERSION constant from claudeIdentity.ts so future
version bumps are a single-line change.
|
Addressed all issues found by Gemini in a07657b |
|
Thank you @Tentoxa for your contribution! This has been reviewed and is now merged into |
Summary
Refresh the native Claude OAuth wire image to match claude-cli/2.1.131, and
extend the cloak so it fires on any
sk-ant-oattoken regardless of upstreamclient. This resolves the upstream 429s seen when routing Claude Max OAuth
credentials through non-CLI clients (ForgeCode, Cursor, Cline).
Highlights:
cliUserID/accountUUID/sessionIdgenerated andpersisted via
mapTokens; preserved across token refresh.postExchangecalls/api/claude_cli/bootstrapto capture real accountmetadata at provisioning time (best-effort — failure does not block OAuth).
anthropic-betaselection per request shape (probe / structured/ full-agent) replaces the previous static set.
x-omniroute-effortandx-omniroute-thinkingheaders; auto-mirrorcontext_managementwheneverthinkingis set.output_format→output_config.formatinchatCoreto fix the 400 some clients trigger.instead of duplicating cloak logic — single source of truth.
Related Issues
Validation
npm run lintnpm run test:unitnpm run test:coverage>= 60%for statements, lines, functions, and branchesTests Added Or Updated
No new test files. Existing Claude unit suites all pass (69 tests, 12 suites).
Coverage Notes
Touched files in
open-sse/andsrc/lib/are covered by existingclaude-*suites. The new helper moduleopen-sse/executors/claudeIdentity.tsis exercised transitively by
claude-code-parityandanthropic-cache-fingerprint. Happy to add focused unit tests forselectBetaFlags/parseUpstreamMetadataUserId/resolveCliUserIDifreviewers want them.
Reviewer Notes
sk-ant-oattoken detection inaddition to
isClaudeCodeClient. API-key Claude (sk-ant-api) and allother providers are unaffected.
accountUUID/cliUserIDafter re-auth. Pre-existing accounts use abackground bootstrap fetch + in-memory cache as a stopgap.
x-omniroute-effortandx-omniroute-thinkingdocumentedinline in
base.ts; default is pure passthrough.