From 67ca037b17e9636c83d7c6b7bf0e53a4e76100c7 Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 17 Jul 2026 17:44:47 +0000 Subject: [PATCH] fix(ai): allowed custom oauth fingerprint headers Added an opt-in compatibility flag for non-official Anthropic OAuth endpoints while preserving authoritative OAuth and Cloudflare credentials. Fixes #5888 --- packages/ai/CHANGELOG.md | 4 ++ packages/ai/src/providers/anthropic.ts | 48 ++++++++++++++----- packages/ai/test/anthropic-alignment.test.ts | 42 ++++++++++++++++ packages/catalog/CHANGELOG.md | 4 ++ packages/catalog/src/compat/anthropic.ts | 1 + packages/catalog/src/types.ts | 5 ++ packages/coding-agent/CHANGELOG.md | 4 ++ .../src/config/models-config-schema.ts | 1 + .../coding-agent/test/model-registry.test.ts | 6 ++- 9 files changed, 101 insertions(+), 14 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index f35650cad..35b75986e 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed custom OAuth Anthropic-compatible endpoints with explicit header overrides still receiving generated Claude Code fingerprint headers instead. ([#5888](https://github.com/can1357/oh-my-pi/issues/5888)) + ## [17.0.2] - 2026-07-17 ### Fixed diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 50bd112f3..3196a8c6d 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -100,6 +100,8 @@ export type AnthropicHeaderOptions = { isCloudflareAiGateway?: boolean; claudeCodeSessionId?: string; claudeCodeBetas?: readonly string[]; + /** Allow explicit fingerprint headers to replace OAuth defaults on non-official endpoints. */ + allowAnthropicHeaderOverrides?: boolean; }; export function normalizeAnthropicBaseUrl(baseUrl?: string): string | undefined { @@ -225,22 +227,36 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record = {}; + const anthropicHeaderOverrides: Record = {}; const filteredEnforcedKeys: string[] = []; - for (const [key, value] of Object.entries(options.modelHeaders ?? {})) { - const lowerKey = key.toLowerCase(); - if (enforcedHeaderKeys.has(lowerKey)) { - // user-agent is always re-applied explicitly. authorization / x-api-key - // are silently re-applied in honoring branches and dropped + logged - // where the branch enforces its own credential. - if (lowerKey === "user-agent") continue; - if (lowerKey === "authorization" && honorAuthorization) continue; - if (lowerKey === "x-api-key" && honorApiKey) continue; - filteredEnforcedKeys.push(key); - continue; + const headerSource = options.modelHeaders; + if (headerSource) { + for (const key in headerSource) { + const value = headerSource[key]; + const lowerKey = key.toLowerCase(); + if (enforcedHeaderKeys.has(lowerKey)) { + if (allowAnthropicHeaderOverrides && overridableAnthropicHeaderKeys.has(lowerKey)) { + anthropicHeaderOverrides[key] = value; + continue; + } + // user-agent is always re-applied explicitly. authorization / x-api-key + // are silently re-applied in honoring branches and dropped + logged + // where the branch enforces its own credential. + if (lowerKey === "user-agent") continue; + if (lowerKey === "authorization" && honorAuthorization) continue; + if (lowerKey === "x-api-key" && honorApiKey) continue; + filteredEnforcedKeys.push(key); + continue; + } + modelHeaders[key] = value; } - modelHeaders[key] = value; } if (filteredEnforcedKeys.length > 0) { // Caller/env-supplied values (options.headers, ANTHROPIC_CUSTOM_HEADERS) @@ -266,7 +282,7 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record key.toLowerCase()), ); +const overridableAnthropicHeaderKeys = new Set( + [...Object.keys(claudeCodeHeaders), "anthropic-beta", "User-Agent", "x-app"].map(key => key.toLowerCase()), +); + const CLAUDE_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:"; function createClaudeBillingHeader(firstUserMessageText: string): string { @@ -2791,6 +2812,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A dynamicHeaders, ), isCloudflareAiGateway: model.provider === "cloudflare-ai-gateway", + allowAnthropicHeaderOverrides: model.compat.allowAnthropicHeaderOverrides, claudeCodeSessionId, claudeCodeBetas: oauthToken ? buildClaudeCodeBetas( diff --git a/packages/ai/test/anthropic-alignment.test.ts b/packages/ai/test/anthropic-alignment.test.ts index 14fa8a1c4..4dbd9a59b 100644 --- a/packages/ai/test/anthropic-alignment.test.ts +++ b/packages/ai/test/anthropic-alignment.test.ts @@ -642,6 +642,48 @@ describe("Anthropic request fingerprint alignment", () => { expect(headers.Authorization).toBe("Bearer sk-ant-oat-test"); }); + it("honors opted-in OAuth fingerprint headers on non-official endpoints (#5888)", () => { + const options = buildAnthropicClientOptions({ + model: buildModel({ + ...ANTHROPIC_MODEL_SPEC, + provider: "custom-anthropic", + baseUrl: "https://proxy.example.com/anthropic", + headers: { + "anthropic-beta": "custom-beta-token", + "x-app": "custom-app-token", + "X-Stainless-Runtime-Version": "custom-runtime-token", + Authorization: "should-not-leak", + }, + compat: { allowAnthropicHeaderOverrides: true }, + }), + apiKey: "sk-ant-oat-test", + stream: true, + }); + + expect(options.defaultHeaders["anthropic-beta"]).toBe("custom-beta-token"); + expect(options.defaultHeaders["x-app"]).toBe("custom-app-token"); + expect(options.defaultHeaders["X-Stainless-Runtime-Version"]).toBe("custom-runtime-token"); + expect(options.defaultHeaders.Authorization).toBe("Bearer sk-ant-oat-test"); + }); + + it("keeps OAuth fingerprint defaults on official endpoints despite the compat opt-in", () => { + const headers = buildAnthropicHeaders({ + apiKey: "sk-ant-oat-test", + baseUrl: "https://api.anthropic.com", + isOAuth: true, + allowAnthropicHeaderOverrides: true, + modelHeaders: { + "anthropic-beta": "custom-beta-token", + "x-app": "custom-app-token", + "X-Stainless-Runtime-Version": "custom-runtime-token", + }, + }); + + expect(headers["anthropic-beta"]).not.toBe("custom-beta-token"); + expect(headers["x-app"]).toBe("cli"); + expect(headers["X-Stainless-Runtime-Version"]).toBe("v24.3.0"); + }); + it("suppresses the client-level X-Api-Key when model.headers carries a custom Authorization (#3391)", () => { const options = buildAnthropicClientOptions({ model: buildModel({ diff --git a/packages/catalog/CHANGELOG.md b/packages/catalog/CHANGELOG.md index c268dc798..e3acbe4ba 100644 --- a/packages/catalog/CHANGELOG.md +++ b/packages/catalog/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added an Anthropic compatibility flag for opting non-official OAuth endpoints into configured Claude Code fingerprint header overrides. ([#5888](https://github.com/can1357/oh-my-pi/issues/5888)) + ## [17.0.2] - 2026-07-17 ### Changed diff --git a/packages/catalog/src/compat/anthropic.ts b/packages/catalog/src/compat/anthropic.ts index 2884d0ead..71f7de8d3 100644 --- a/packages/catalog/src/compat/anthropic.ts +++ b/packages/catalog/src/compat/anthropic.ts @@ -109,6 +109,7 @@ export function buildAnthropicCompat(spec: ModelSpec<"anthropic-messages">): Res signingEndpoint, disableStrictTools: isAzure, disableAdaptiveThinking: false, + allowAnthropicHeaderOverrides: false, supportsEagerToolInputStreaming: official, // Long cache retention is only sent to the official API by default; // proxies opt in explicitly via `compat.supportsLongCacheRetention: true`. diff --git a/packages/catalog/src/types.ts b/packages/catalog/src/types.ts index 8993f0a71..9e6673000 100644 --- a/packages/catalog/src/types.ts +++ b/packages/catalog/src/types.ts @@ -414,6 +414,11 @@ export interface AnthropicCompat { * auto-detected (Z.AI hosts). */ requiresToolResultId?: boolean; + /** + * Allow configured Claude Code fingerprint headers to replace generated + * OAuth defaults on non-official Anthropic endpoints. + */ + allowAnthropicHeaderOverrides?: boolean; /** * Replay unsigned `thinking` blocks from prior assistant turns as native * thinking instead of demoting them to text. Official Anthropic enforces diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 493bd5332..4fad3ee45 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed custom `anthropic-messages` OAuth providers being unable to opt into configured Claude Code fingerprint header overrides. ([#5888](https://github.com/can1357/oh-my-pi/issues/5888)) + ## [17.0.2] - 2026-07-17 ### Added diff --git a/packages/coding-agent/src/config/models-config-schema.ts b/packages/coding-agent/src/config/models-config-schema.ts index c2195a885..ad0b4544a 100644 --- a/packages/coding-agent/src/config/models-config-schema.ts +++ b/packages/coding-agent/src/config/models-config-schema.ts @@ -61,6 +61,7 @@ const OpenAICompatFields = { "supportsImageDetailOriginal?": "boolean", // anthropic-messages compat flags (same `compat` slot, per-api interpretation) "supportsEagerToolInputStreaming?": "boolean", + "allowAnthropicHeaderOverrides?": "boolean", "requiresToolResultId?": "boolean", "replayUnsignedThinking?": "boolean", } as const; diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index 0e4f46d38..590f30762 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -584,6 +584,7 @@ describe("ModelRegistry", () => { api: "anthropic-messages", compat: { supportsEagerToolInputStreaming: true, + allowAnthropicHeaderOverrides: true, }, models: [ { @@ -685,7 +686,10 @@ describe("ModelRegistry", () => { test("custom Anthropic providers can opt into eager tool input streaming", () => { const model = customAnthropicCompat.find("anthropic-proxy", "claude-haiku-4.5"); - expect(model?.compat).toMatchObject({ supportsEagerToolInputStreaming: true }); + expect(model?.compat).toMatchObject({ + supportsEagerToolInputStreaming: true, + allowAnthropicHeaderOverrides: true, + }); }); test("custom Responses providers can disable original image detail", () => {