Skip to content

Commit 07621a6

Browse files
authored
fix: scrub canonical Anthropic headers from 3P shim requests (#499)
* Stop canonical Anthropic headers from leaking into 3P shim requests The remaining blocker from PR #268 was that canonical Anthropic headers such as `anthropic-version` and `anthropic-beta` could still ride through supported 3P paths even after the earlier x-anthropic/x-claude scrubber work. This tightens header filtering inside the shim itself so direct defaultHeaders, env-driven client setup, providerOverride routing, and per-request header injection all share the same scrubber. Constraint: Preserve non-Anthropic custom headers and provider auth while stripping only Anthropic/OpenClaude-internal headers from 3P requests Rejected: Rely on client.ts filtering alone | direct shim construction and per-request headers would still leave gaps Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep header scrubbing centralized in the shim so new call paths do not reopen 3P leakage bugs Tested: bun test src/services/api/openaiShim.test.ts src/services/api/client.test.ts src/utils/context.test.ts Tested: bun run test:provider Tested: bun run build && node dist/cli.mjs --version Not-tested: bun run typecheck (repository baseline currently fails in many unrelated files) * Keep OpenAI client tests from restoring undefined env as strings The new header-leak regression tests in client.test.ts restored environment variables via direct assignment, which can leave literal "undefined" strings in process.env when the original value was unset. This switches the teardown over to the same restore helper pattern already used in openaiShim.test.ts. Constraint: Keep the fix limited to test hygiene without altering runtime behavior Rejected: Restore only the two env vars Copilot called out | using one helper for all test env restores is simpler and less error-prone Confidence: high Scope-risk: narrow Reversibility: clean Directive: Use restore helpers for env teardown in tests so unset values stay deleted instead of becoming the string "undefined" Tested: bun test src/services/api/client.test.ts src/services/api/openaiShim.test.ts src/utils/context.test.ts Not-tested: Full provider suite (unchanged runtime path) * Prevent GitHub Codex requests from forwarding unsanitized Anthropic headers A base-sync with upstream exposed a separate GitHub+Codex transport branch that still merged per-request headers raw before adding Copilot headers. This keeps the filter aligned across Codex-family paths and adds explicit regression tests for GitHub Codex routing, including providerOverride. Constraint: Must not push or modify GitHub state while validating the reviewer concern Rejected: Leave the GitHub Codex path unchanged | runtime repro showed anthropic-* headers still leaked after the upstream sync Confidence: high Scope-risk: narrow Directive: Keep header scrubbing consistent across every Codex-family transport branch when provider routing changes Tested: bun test src/services/api/openaiShim.test.ts Tested: bun test src/services/api/client.test.ts src/services/api/codexShim.test.ts src/services/api/providerConfig.github.test.ts Tested: bun run build Not-tested: Full repository test suite
1 parent 6924718 commit 07621a6

3 files changed

Lines changed: 419 additions & 15 deletions

File tree

src/services/api/client.test.ts

Lines changed: 157 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type ShimClient = {
1414
const originalFetch = globalThis.fetch
1515
const originalMacro = (globalThis as Record<string, unknown>).MACRO
1616
const originalEnv = {
17+
CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI,
1718
CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI,
1819
GEMINI_API_KEY: process.env.GEMINI_API_KEY,
1920
GEMINI_MODEL: process.env.GEMINI_MODEL,
@@ -25,6 +26,15 @@ const originalEnv = {
2526
OPENAI_MODEL: process.env.OPENAI_MODEL,
2627
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
2728
ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN,
29+
ANTHROPIC_CUSTOM_HEADERS: process.env.ANTHROPIC_CUSTOM_HEADERS,
30+
}
31+
32+
function restoreEnv(key: string, value: string | undefined): void {
33+
if (value === undefined) {
34+
delete process.env[key]
35+
} else {
36+
process.env[key] = value
37+
}
2838
}
2939

3040
beforeEach(() => {
@@ -35,27 +45,31 @@ beforeEach(() => {
3545
process.env.GEMINI_BASE_URL = 'https://gemini.example/v1beta/openai'
3646
process.env.GEMINI_AUTH_MODE = 'api-key'
3747

48+
delete process.env.CLAUDE_CODE_USE_OPENAI
3849
delete process.env.GOOGLE_API_KEY
3950
delete process.env.OPENAI_API_KEY
4051
delete process.env.OPENAI_BASE_URL
4152
delete process.env.OPENAI_MODEL
4253
delete process.env.ANTHROPIC_API_KEY
4354
delete process.env.ANTHROPIC_AUTH_TOKEN
55+
delete process.env.ANTHROPIC_CUSTOM_HEADERS
4456
})
4557

4658
afterEach(() => {
4759
;(globalThis as Record<string, unknown>).MACRO = originalMacro
48-
process.env.CLAUDE_CODE_USE_GEMINI = originalEnv.CLAUDE_CODE_USE_GEMINI
49-
process.env.GEMINI_API_KEY = originalEnv.GEMINI_API_KEY
50-
process.env.GEMINI_MODEL = originalEnv.GEMINI_MODEL
51-
process.env.GEMINI_BASE_URL = originalEnv.GEMINI_BASE_URL
52-
process.env.GEMINI_AUTH_MODE = originalEnv.GEMINI_AUTH_MODE
53-
process.env.GOOGLE_API_KEY = originalEnv.GOOGLE_API_KEY
54-
process.env.OPENAI_API_KEY = originalEnv.OPENAI_API_KEY
55-
process.env.OPENAI_BASE_URL = originalEnv.OPENAI_BASE_URL
56-
process.env.OPENAI_MODEL = originalEnv.OPENAI_MODEL
57-
process.env.ANTHROPIC_API_KEY = originalEnv.ANTHROPIC_API_KEY
58-
process.env.ANTHROPIC_AUTH_TOKEN = originalEnv.ANTHROPIC_AUTH_TOKEN
60+
restoreEnv('CLAUDE_CODE_USE_OPENAI', originalEnv.CLAUDE_CODE_USE_OPENAI)
61+
restoreEnv('CLAUDE_CODE_USE_GEMINI', originalEnv.CLAUDE_CODE_USE_GEMINI)
62+
restoreEnv('GEMINI_API_KEY', originalEnv.GEMINI_API_KEY)
63+
restoreEnv('GEMINI_MODEL', originalEnv.GEMINI_MODEL)
64+
restoreEnv('GEMINI_BASE_URL', originalEnv.GEMINI_BASE_URL)
65+
restoreEnv('GEMINI_AUTH_MODE', originalEnv.GEMINI_AUTH_MODE)
66+
restoreEnv('GOOGLE_API_KEY', originalEnv.GOOGLE_API_KEY)
67+
restoreEnv('OPENAI_API_KEY', originalEnv.OPENAI_API_KEY)
68+
restoreEnv('OPENAI_BASE_URL', originalEnv.OPENAI_BASE_URL)
69+
restoreEnv('OPENAI_MODEL', originalEnv.OPENAI_MODEL)
70+
restoreEnv('ANTHROPIC_API_KEY', originalEnv.ANTHROPIC_API_KEY)
71+
restoreEnv('ANTHROPIC_AUTH_TOKEN', originalEnv.ANTHROPIC_AUTH_TOKEN)
72+
restoreEnv('ANTHROPIC_CUSTOM_HEADERS', originalEnv.ANTHROPIC_CUSTOM_HEADERS)
5973
globalThis.fetch = originalFetch
6074
})
6175

@@ -122,3 +136,135 @@ test('routes Gemini provider requests through the OpenAI-compatible shim', async
122136
model: 'gemini-2.0-flash',
123137
})
124138
})
139+
140+
test('strips Anthropic-specific custom headers before sending OpenAI-compatible shim requests', async () => {
141+
let capturedHeaders: Headers | undefined
142+
143+
process.env.CLAUDE_CODE_USE_OPENAI = '1'
144+
process.env.OPENAI_API_KEY = 'openai-test-key'
145+
process.env.OPENAI_BASE_URL = 'http://example.test/v1'
146+
process.env.OPENAI_MODEL = 'gpt-4o'
147+
process.env.ANTHROPIC_CUSTOM_HEADERS = [
148+
'anthropic-version: 2023-06-01',
149+
'anthropic-beta: prompt-caching-2024-07-31',
150+
'x-anthropic-additional-protection: true',
151+
'x-claude-remote-session-id: remote-123',
152+
'x-app: cli',
153+
'x-safe-header: keep-me',
154+
].join('\n')
155+
156+
globalThis.fetch = (async (_input, init) => {
157+
capturedHeaders = new Headers(init?.headers)
158+
159+
return new Response(
160+
JSON.stringify({
161+
id: 'chatcmpl-openai',
162+
model: 'gpt-4o',
163+
choices: [
164+
{
165+
message: {
166+
role: 'assistant',
167+
content: 'ok',
168+
},
169+
finish_reason: 'stop',
170+
},
171+
],
172+
usage: {
173+
prompt_tokens: 8,
174+
completion_tokens: 3,
175+
total_tokens: 11,
176+
},
177+
}),
178+
{
179+
headers: {
180+
'Content-Type': 'application/json',
181+
},
182+
},
183+
)
184+
}) as FetchType
185+
186+
const client = (await getAnthropicClient({
187+
maxRetries: 0,
188+
model: 'gpt-4o',
189+
})) as unknown as ShimClient
190+
191+
await client.beta.messages.create({
192+
model: 'gpt-4o',
193+
system: 'test system',
194+
messages: [{ role: 'user', content: 'hello' }],
195+
max_tokens: 64,
196+
stream: false,
197+
})
198+
199+
expect(capturedHeaders?.get('anthropic-version')).toBeNull()
200+
expect(capturedHeaders?.get('anthropic-beta')).toBeNull()
201+
expect(capturedHeaders?.get('x-anthropic-additional-protection')).toBeNull()
202+
expect(capturedHeaders?.get('x-claude-remote-session-id')).toBeNull()
203+
expect(capturedHeaders?.get('x-app')).toBeNull()
204+
expect(capturedHeaders?.get('x-safe-header')).toBe('keep-me')
205+
expect(capturedHeaders?.get('authorization')).toBe('Bearer openai-test-key')
206+
})
207+
208+
test('strips Anthropic-specific custom headers on providerOverride shim requests too', async () => {
209+
let capturedHeaders: Headers | undefined
210+
211+
process.env.ANTHROPIC_CUSTOM_HEADERS = [
212+
'anthropic-version: 2023-06-01',
213+
'anthropic-beta: prompt-caching-2024-07-31',
214+
'x-claude-remote-session-id: remote-123',
215+
'x-safe-header: keep-me',
216+
].join('\n')
217+
218+
globalThis.fetch = (async (_input, init) => {
219+
capturedHeaders = new Headers(init?.headers)
220+
221+
return new Response(
222+
JSON.stringify({
223+
id: 'chatcmpl-provider-override',
224+
model: 'gpt-4o',
225+
choices: [
226+
{
227+
message: {
228+
role: 'assistant',
229+
content: 'ok',
230+
},
231+
finish_reason: 'stop',
232+
},
233+
],
234+
usage: {
235+
prompt_tokens: 8,
236+
completion_tokens: 3,
237+
total_tokens: 11,
238+
},
239+
}),
240+
{
241+
headers: {
242+
'Content-Type': 'application/json',
243+
},
244+
},
245+
)
246+
}) as FetchType
247+
248+
const client = (await getAnthropicClient({
249+
maxRetries: 0,
250+
providerOverride: {
251+
model: 'gpt-4o',
252+
baseURL: 'http://example.test/v1',
253+
apiKey: 'provider-test-key',
254+
},
255+
})) as unknown as ShimClient
256+
257+
await client.beta.messages.create({
258+
model: 'unused',
259+
system: 'test system',
260+
messages: [{ role: 'user', content: 'hello' }],
261+
max_tokens: 64,
262+
stream: false,
263+
})
264+
265+
expect(capturedHeaders?.get('anthropic-version')).toBeNull()
266+
expect(capturedHeaders?.get('anthropic-beta')).toBeNull()
267+
expect(capturedHeaders?.get('x-claude-remote-session-id')).toBeNull()
268+
expect(capturedHeaders?.get('x-safe-header')).toBe('keep-me')
269+
expect(capturedHeaders?.get('authorization')).toBe('Bearer provider-test-key')
270+
})

0 commit comments

Comments
 (0)