From bd524640f5c1c9ec34755767be8d027317f79da8 Mon Sep 17 00:00:00 2001 From: LaZzyMan Date: Thu, 21 May 2026 15:43:27 +0800 Subject: [PATCH] Revert "fix(core): set x-api-key alongside Authorization on Anthropic outbound (#4323) (#4342)" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 4b25f9c05c625f4d21de18e0369026f42bcc1fdd. PR #4342 unconditionally injected `x-api-key` alongside `Authorization: Bearer` on every proxy-branch request. This broke IdeaLab-style proxies (e.g. `idealab.alibaba-inc.com/api/anthropic`) which reject requests carrying both headers with HTTP 401: 鉴权header, x-api-key和Authorization不可以同时存在 (auth header: x-api-key and Authorization cannot coexist) The two proxy families have mutually exclusive header contracts — OpenCode-Go-style servers want `x-api-key` only, IdeaLab-style servers want `Authorization` only — so a one-size-fits-all default cannot satisfy both at once. Reverting restores the pre-#4342 default (Bearer only) so IdeaLab users are unblocked. OpenCode-Go-style users can opt in via `customHeaders`: { "customHeaders": { "x-api-key": "" } } `buildHeaders` already merges customHeaders into defaultHeaders (only `anthropic-beta` is reserved for per-request handling), and on the proxy branch the SDK is constructed with `apiKey: null` so it does not emit its own `x-api-key` — the value on the wire comes solely from the user's explicit customHeaders entry, preserving the #4020 env-leak guard. Reopens #4323. --- .../anthropicContentGenerator.test.ts | 91 +------------------ .../anthropicContentGenerator.ts | 16 ---- 2 files changed, 2 insertions(+), 105 deletions(-) diff --git a/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts b/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts index f0c7d582c6..084eab4cfb 100644 --- a/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts +++ b/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.test.ts @@ -104,14 +104,12 @@ describe('AnthropicContentGenerator', () => { vi.restoreAllMocks(); }); - it('uses claude-cli identity (User-Agent + x-app + Bearer auth + x-api-key) for non-Anthropic baseURLs', async () => { + it('uses claude-cli identity (User-Agent + x-app + Bearer auth) for non-Anthropic baseURLs', async () => { // Non-Anthropic-native baseURL → IdeaLab-style proxy path: // - User-Agent presents as `claude-cli/ (external, cli)` // - `x-app: cli` is sent // - SDK is constructed with `authToken` (sends `Authorization: Bearer`) - // - `x-api-key` is *also* added via defaultHeaders so standards-compliant - // Anthropic-compatible servers (OpenCode-Go, Claude proxies — #4323) - // that only read the canonical `x-api-key` header authenticate too. + // rather than `apiKey` (`x-api-key`), avoiding dual-header conflicts. const { AnthropicContentGenerator } = await importGenerator(); void new AnthropicContentGenerator( { @@ -131,91 +129,10 @@ describe('AnthropicContentGenerator', () => { expect(headers['User-Agent']).toContain('claude-cli/1.2.3'); expect(headers['User-Agent']).toContain('(external, cli)'); expect(headers['x-app']).toBe('cli'); - expect(headers['x-api-key']).toBe('test-key'); expect(anthropicState.constructorOptions?.['authToken']).toBe('test-key'); expect(anthropicState.constructorOptions?.['apiKey']).toBeNull(); }); - it('does NOT add x-api-key for Anthropic-native baseURLs (SDK apiKey path already supplies it)', async () => { - // On the native branch the SDK is constructed with `apiKey` and emits - // `x-api-key` itself. Adding the same header via defaultHeaders would - // duplicate it on the wire (and a stale defaultHeaders entry could - // override a future SDK rotation). Keep the native path SDK-driven. - const { AnthropicContentGenerator } = await importGenerator(); - void new AnthropicContentGenerator( - { - model: 'claude-opus-4-7', - apiKey: 'test-key', - baseUrl: 'https://api.anthropic.com', - timeout: 10_000, - maxRetries: 2, - samplingParams: {}, - schemaCompliance: 'auto', - }, - mockConfig, - ); - - const headers = (anthropicState.constructorOptions?.['defaultHeaders'] || - {}) as Record; - expect(headers['x-api-key']).toBeUndefined(); - }); - - it('does NOT add x-api-key when apiKey is falsy on the proxy branch', async () => { - // Guard branch on the `useProxyIdentity && apiKey` predicate: an - // empty / undefined apiKey would otherwise ship `x-api-key:` (empty) - // — a meaningless header that could confuse server-side debugging - // or trip strict input validation. Pin the guard so a future - // refactor that drops the truthy check fails this test, not prod. - const { AnthropicContentGenerator } = await importGenerator(); - void new AnthropicContentGenerator( - { - model: 'claude-test', - apiKey: '', - baseUrl: 'https://example.invalid', - timeout: 10_000, - maxRetries: 2, - samplingParams: {}, - schemaCompliance: 'auto', - }, - mockConfig, - ); - - const headers = (anthropicState.constructorOptions?.['defaultHeaders'] || - {}) as Record; - expect(headers['x-api-key']).toBeUndefined(); - }); - - it('customHeaders cannot override x-api-key on the proxy branch (post-buildHeaders ordering invariant)', async () => { - // The injection lives AFTER `buildHeaders` (which merges customHeaders - // into defaultHeaders), so a user-supplied - // `customHeaders: { 'x-api-key': … }` is overwritten by the canonical - // value. This is a security-relevant invariant: a refactor that - // accidentally moved the injection above the customHeaders merge - // would let user config swap the auth header for an arbitrary value - // — defeating the dual-auth contract. Pin the ordering here so any - // such regression flips this test before review. - const { AnthropicContentGenerator } = await importGenerator(); - void new AnthropicContentGenerator( - { - model: 'claude-test', - apiKey: 'canonical-key', - baseUrl: 'https://example.invalid', - timeout: 10_000, - maxRetries: 2, - samplingParams: {}, - schemaCompliance: 'auto', - customHeaders: { - 'x-api-key': 'user-override', - }, - }, - mockConfig, - ); - - const headers = (anthropicState.constructorOptions?.['defaultHeaders'] || - {}) as Record; - expect(headers['x-api-key']).toBe('canonical-key'); - }); - it('uses QwenCode identity + apiKey auth when baseURL is api.anthropic.com', async () => { // Anthropic-native baseURL: keep the SDK-default `x-api-key` auth and // a truthful `QwenCode` User-Agent (no `x-app` header) so usage isn't @@ -313,7 +230,6 @@ describe('AnthropicContentGenerator', () => { {}) as Record; expect(headers['User-Agent']).toContain('claude-cli/1.2.3'); expect(headers['x-app']).toBe('cli'); - expect(headers['x-api-key']).toBe('test-key'); expect(anthropicState.constructorOptions?.['authToken']).toBe('test-key'); expect(anthropicState.constructorOptions?.['apiKey']).toBeNull(); }); @@ -344,7 +260,6 @@ describe('AnthropicContentGenerator', () => { {}) as Record; expect(headers['User-Agent']).toContain('claude-cli/1.2.3'); expect(headers['x-app']).toBe('cli'); - expect(headers['x-api-key']).toBe('test-key'); expect(anthropicState.constructorOptions?.['authToken']).toBe('test-key'); expect(anthropicState.constructorOptions?.['apiKey']).toBeNull(); }); @@ -400,7 +315,6 @@ describe('AnthropicContentGenerator', () => { {}) as Record; expect(headers['User-Agent']).toContain('claude-cli/1.2.3'); expect(headers['x-app']).toBe('cli'); - expect(headers['x-api-key']).toBe('test-key'); expect(anthropicState.constructorOptions?.['authToken']).toBe('test-key'); expect(anthropicState.constructorOptions?.['apiKey']).toBeNull(); }); @@ -512,7 +426,6 @@ describe('AnthropicContentGenerator', () => { {}) as Record; expect(headers['User-Agent']).toContain('claude-cli/1.2.3'); expect(headers['x-app']).toBe('cli'); - expect(headers['x-api-key']).toBe('idealab-token'); expect(anthropicState.constructorOptions?.['authToken']).toBe( 'idealab-token', ); diff --git a/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts b/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts index 069c47b7c5..2f6ba63cb5 100644 --- a/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts +++ b/packages/core/src/core/anthropicContentGenerator/anthropicContentGenerator.ts @@ -176,22 +176,6 @@ export class AnthropicContentGenerator implements ContentGenerator { // half of the bundle without the other. const useProxyIdentity = !isAnthropicNativeBaseUrl(contentGeneratorConfig); const defaultHeaders = this.buildHeaders(useProxyIdentity); - // On the proxy branch the SDK is constructed with `authToken` so it - // emits `Authorization: Bearer ` natively, but some - // Anthropic-compatible servers (OpenCode-Go, Claude proxy products — - // see #4323) authenticate only on the canonical `x-api-key` header. - // Ship both shapes side-by-side so either family accepts us. We add - // `x-api-key` here (post-buildHeaders) so customHeaders can't override - // it and the SDK-level env back-fill suppression (apiKey: null on the - // SDK side, suppressing the SDK's own ANTHROPIC_API_KEY destructuring - // default) is preserved. `contentGeneratorConfig.apiKey` itself may - // have been env-resolved upstream by `resolveCredentialField`, but - // that's the same value already shipped as `Authorization: Bearer` - // via `authToken` on this very request — adding it as `x-api-key` - // doesn't widen the #4020 leak surface. - if (useProxyIdentity && contentGeneratorConfig.apiKey) { - defaultHeaders['x-api-key'] = contentGeneratorConfig.apiKey; - } const baseURL = contentGeneratorConfig.baseUrl; // Configure fetch options for proxy support and timeout handling. // With proxy, dispatcher timeouts are disabled so SDK timeout controls the