Skip to content

Commit 40ae1e7

Browse files
authored
fix(shims): strip x-anthropic-billing-header block before forwarding system prompt (Gitlawb#1019)
`getAttributionHeader()` (src/constants/system.ts) builds an `x-anthropic-billing-header: cc_version=...; cc_entrypoint=...; ...` line that gets prepended to the system-prompt block array in src/services/api/claude.ts:1390-1401. The Anthropic API path needs it (server _parse_cc_header consumes it), but the OpenAI / Codex shims joined every text block straight into the outbound `system` / `instructions` payload, so non-Anthropic providers received an Anthropic-only billing string in their prompt — token waste, plus a per-build fingerprint that churns local-model KV cache and any upstream prompt cache (the slowdown unsloth flagged for Claude Code). Fix the two `convertSystemPrompt` helpers (openaiShim.ts:241, codexShim.ts:124) to drop blocks whose text starts with `x-anthropic-billing-header`. Anthropic-bound traffic is unaffected — the block is built into Anthropic-shaped requests directly and never flows through these helpers. Tests: - openaiShim.test.ts: e2e capturedBody assertions on chat-completions + responses-API paths confirm the line is absent and the rest of the system prompt survives. - codexShim.test.ts: convertSystemPrompt is now exported (pure helper) and unit-tested for array + only-attribution + plain-string cases. Closes Gitlawb#607.
1 parent 1020663 commit 40ae1e7

4 files changed

Lines changed: 144 additions & 1 deletion

File tree

src/services/api/codexShim.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
codexStreamToAnthropic,
77
convertAnthropicMessagesToResponsesInput,
88
convertCodexResponseToAnthropicMessage,
9+
convertSystemPrompt,
910
convertToolsToResponsesTools,
1011
} from './codexShim.js'
1112
import { __test as webSearchToolTest } from '../../tools/WebSearchTool/WebSearchTool.js'
@@ -883,3 +884,40 @@ describe('Codex request translation', () => {
883884
)
884885
})
885886
})
887+
888+
describe('convertSystemPrompt', () => {
889+
test('strips Anthropic attribution header block from text-block array (#607)', () => {
890+
const result = convertSystemPrompt([
891+
{
892+
type: 'text',
893+
text:
894+
'x-anthropic-billing-header: cc_version=0.8.0.abc123; ' +
895+
'cc_entrypoint=cli;',
896+
},
897+
{ type: 'text', text: 'You are Claude Code.' },
898+
{ type: 'text', text: 'Project context: bun + react.' },
899+
])
900+
901+
expect(result).not.toContain('x-anthropic-billing-header')
902+
expect(result).not.toContain('cc_version=')
903+
expect(result).toContain('You are Claude Code.')
904+
expect(result).toContain('Project context: bun + react.')
905+
})
906+
907+
test('returns empty string when only the attribution block is present', () => {
908+
const result = convertSystemPrompt([
909+
{
910+
type: 'text',
911+
text: 'x-anthropic-billing-header: cc_version=0.8.0.abc;',
912+
},
913+
])
914+
915+
expect(result).toBe('')
916+
})
917+
918+
test('passes plain string system prompts through untouched', () => {
919+
expect(convertSystemPrompt('You are Claude Code.')).toBe(
920+
'You are Claude Code.',
921+
)
922+
})
923+
})

src/services/api/codexShim.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,18 @@ function normalizeToolUseId(toolUseId: string | undefined): {
121121
}
122122
}
123123

124-
function convertSystemPrompt(system: unknown): string {
124+
export function convertSystemPrompt(system: unknown): string {
125125
if (!system) return ''
126126
if (typeof system === 'string') return system
127127
if (Array.isArray(system)) {
128128
return system
129129
.map((block: { type?: string; text?: string }) =>
130130
block.type === 'text' ? (block.text ?? '') : '',
131131
)
132+
// Drop the Anthropic billing/attribution block — Codex's Responses API
133+
// doesn't parse it and the per-build fingerprint just churns the
134+
// upstream prompt cache.
135+
.filter(text => !text.startsWith('x-anthropic-billing-header'))
132136
.join('\n\n')
133137
}
134138
return String(system)

src/services/api/openaiShim.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4923,3 +4923,99 @@ test('Z.AI: thinking mode enabled when requested', async () => {
49234923
expect(requestBody?.max_completion_tokens).toBeUndefined()
49244924
expect(requestBody?.max_tokens).toBe(1024)
49254925
})
4926+
4927+
test('strips Anthropic attribution header block from chat-completions system prompt (#607)', async () => {
4928+
let capturedBody: Record<string, unknown> | undefined
4929+
4930+
globalThis.fetch = (async (_input, init) => {
4931+
capturedBody = JSON.parse(String(init?.body)) as Record<string, unknown>
4932+
4933+
return new Response(
4934+
JSON.stringify({
4935+
id: 'chatcmpl-1',
4936+
model: 'gpt-4o',
4937+
choices: [
4938+
{
4939+
message: { role: 'assistant', content: 'ok' },
4940+
finish_reason: 'stop',
4941+
},
4942+
],
4943+
usage: { prompt_tokens: 8, completion_tokens: 3, total_tokens: 11 },
4944+
}),
4945+
{ headers: { 'Content-Type': 'application/json' } },
4946+
)
4947+
}) as FetchType
4948+
4949+
const client = createOpenAIShimClient({}) as OpenAIShimClient
4950+
4951+
await client.beta.messages.create({
4952+
model: 'gpt-4o',
4953+
system: [
4954+
{
4955+
type: 'text',
4956+
text:
4957+
'x-anthropic-billing-header: cc_version=0.8.0.abc123; ' +
4958+
'cc_entrypoint=cli;',
4959+
},
4960+
{ type: 'text', text: 'You are Claude Code, helpful assistant.' },
4961+
{ type: 'text', text: 'Project context: bun + react.' },
4962+
],
4963+
messages: [{ role: 'user', content: 'hello' }],
4964+
max_tokens: 64,
4965+
stream: false,
4966+
})
4967+
4968+
const messages = capturedBody?.messages as Array<{ role: string; content: string }>
4969+
const sysMsg = messages.find(m => m.role === 'system')
4970+
expect(sysMsg).toBeDefined()
4971+
expect(sysMsg?.content).not.toContain('x-anthropic-billing-header')
4972+
expect(sysMsg?.content).not.toContain('cc_version=')
4973+
expect(sysMsg?.content).toContain('You are Claude Code, helpful assistant.')
4974+
expect(sysMsg?.content).toContain('Project context: bun + react.')
4975+
})
4976+
4977+
test('strips Anthropic attribution header block from responses-API instructions (#607)', async () => {
4978+
process.env.OPENAI_API_FORMAT = 'responses'
4979+
let capturedBody: Record<string, unknown> | undefined
4980+
4981+
globalThis.fetch = (async (_input, init) => {
4982+
capturedBody = JSON.parse(String(init?.body)) as Record<string, unknown>
4983+
4984+
return new Response(
4985+
JSON.stringify({
4986+
id: 'resp-1',
4987+
model: 'gpt-5.4',
4988+
output: [
4989+
{
4990+
type: 'message',
4991+
role: 'assistant',
4992+
content: [{ type: 'output_text', text: 'ok' }],
4993+
},
4994+
],
4995+
usage: { input_tokens: 8, output_tokens: 3, total_tokens: 11 },
4996+
}),
4997+
{ headers: { 'Content-Type': 'application/json' } },
4998+
)
4999+
}) as FetchType
5000+
5001+
const client = createOpenAIShimClient({ defaultHeaders: {} }) as OpenAIShimClient
5002+
5003+
await client.beta.messages.create({
5004+
model: 'gpt-5.4',
5005+
system: [
5006+
{
5007+
type: 'text',
5008+
text: 'x-anthropic-billing-header: cc_version=0.8.0.abc123; cc_entrypoint=cli;',
5009+
},
5010+
{ type: 'text', text: 'You are Claude Code.' },
5011+
],
5012+
messages: [{ role: 'user', content: 'hello' }],
5013+
max_tokens: 64,
5014+
stream: false,
5015+
})
5016+
5017+
const instructions = capturedBody?.instructions as string
5018+
expect(instructions).not.toContain('x-anthropic-billing-header')
5019+
expect(instructions).not.toContain('cc_version=')
5020+
expect(instructions).toContain('You are Claude Code.')
5021+
})

src/services/api/openaiShim.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@ function convertSystemPrompt(
248248
.map((block: { type?: string; text?: string }) =>
249249
block.type === 'text' ? block.text ?? '' : '',
250250
)
251+
// Drop the Anthropic billing/attribution block — it's only meaningful to
252+
// Anthropic's `_parse_cc_header` and is dead weight (plus a churning
253+
// per-build fingerprint that busts prefix KV cache) for OpenAI-compat
254+
// providers like local Ollama / llama.cpp / Codex pass-throughs.
255+
.filter(text => !text.startsWith('x-anthropic-billing-header'))
251256
.join('\n\n')
252257
}
253258
return String(system)

0 commit comments

Comments
 (0)