diff --git a/src/adapters/google.ts b/src/adapters/google.ts index 8829dc07846..685cd2177cd 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -57,7 +57,6 @@ const GOOGLE_BREVITY_INSTRUCTION = [ const ANTIGRAVITY_REJECTED_CLAUDE_SDK_PARAGRAPH = "You are a Claude agent, built on Anthropic's Claude Agent SDK."; - /** * CCA Flash generations that reject the Claude-Agent identity paragraph. * @@ -102,6 +101,20 @@ function stripAntigravityRejectedClaudeSdkParagraph(systemText: string): string .join("\n\n"); } +/** + * Strips Claude Code CLI's internal billing header (`x-anthropic-billing-header: ...`) + * at the start of the system prompt, because Cloud Code Assist / Google Antigravity inspects + * `systemInstruction` and rejects requests containing Anthropic billing metadata with + * HTTP 429 RESOURCE_EXHAUSTED. + * + * Matching is restricted to the prompt start (`^` without the `/m` multiline flag) so that + * user prompts discussing billing headers in intermediate lines are never modified, and + * prompts without a billing header preserve their leading whitespace untouched. + */ +function stripAntigravityBillingHeader(systemText: string): string { + return systemText.replace(/^x-anthropic-billing-header:[^\n]*\n*/, ""); +} + /** * Documented output ceiling for a Google-surface model, or `undefined` when the id is not * recognized. @@ -276,6 +289,7 @@ function messagesToGeminiFormat( parsed: OcxParsedRequest, identityModelId: string, stripRejectedClaudeSdkParagraph = false, + isCloudCodeAssist = false, ): { systemInstruction?: unknown; contents: unknown[]; replayedCallIds: string[] } { // Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model // never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream. @@ -285,9 +299,12 @@ function messagesToGeminiFormat( ...(toolCatalogNudge ? [toolCatalogNudge] : []), GOOGLE_BREVITY_INSTRUCTION, ].join("\n\n"), identityModelId); - const systemText = stripRejectedClaudeSdkParagraph - ? stripAntigravityRejectedClaudeSdkParagraph(identifiedSystemText) + let systemText = isCloudCodeAssist + ? stripAntigravityBillingHeader(identifiedSystemText) : identifiedSystemText; + if (stripRejectedClaudeSdkParagraph) { + systemText = stripAntigravityRejectedClaudeSdkParagraph(systemText); + } const systemInstruction = { parts: [{ text: systemText }] }; const contents: unknown[] = []; @@ -833,12 +850,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte && /^gemini-/.test(routedModelId) && !isImageCapableModel(parsed.modelId); // AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation. const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId; - const stripRejectedClaudeSdkParagraph = provider.googleMode === "cloud-code-assist" + const isCloudCodeAssist = provider.googleMode === "cloud-code-assist"; + const stripRejectedClaudeSdkParagraph = isCloudCodeAssist && rejectsClaudeSdkParagraph(parsed.modelId, routedModelId); const { systemInstruction, contents, replayedCallIds } = messagesToGeminiFormat( parsed, identityModelId, stripRejectedClaudeSdkParagraph, + isCloudCodeAssist, ); lastInjectedCallIds = [...replayedCallIds]; lastReasoningReplayScope = parsed._reasoningReplayScope; diff --git a/tests/adapters/google/google-adapter.test.ts b/tests/adapters/google/google-adapter.test.ts index 1dd5a7c97a4..01d8e2602b8 100644 --- a/tests/adapters/google/google-adapter.test.ts +++ b/tests/adapters/google/google-adapter.test.ts @@ -350,6 +350,64 @@ describe("google adapter — Antigravity system prompt compatibility", () => { expect(systemInstructionText(body)).toContain(REJECTED_CLAUDE_SDK_PARAGRAPH); }); + + test("removes x-anthropic-billing-header for Cloud Code Assist models", async () => { + const parsed: OcxParsedRequest = { + ...systemPromptParsed("gemini-3.8-flash"), + context: { + systemPrompt: [ + "x-anthropic-billing-header: cc_version=2.1.236.b88; cc_entrypoint=sdk-cli;", + "You are Claude Code.", + ], + messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }], + tools: [], + }, + }; + const envelope = JSON.parse((await createGoogleAdapter(ccaProvider).buildRequest(parsed)).body) as { + request: Record; + }; + + expect(systemInstructionText(envelope.request)).not.toContain("x-anthropic-billing-header"); + expect(systemInstructionText(envelope.request)).toContain("You are Claude Code."); + }); + + test("preserves x-anthropic-billing-header outside Cloud Code Assist", async () => { + const parsed: OcxParsedRequest = { + ...systemPromptParsed("gemini-3.8-flash"), + context: { + systemPrompt: [ + "x-anthropic-billing-header: cc_version=2.1.236.b88; cc_entrypoint=sdk-cli;", + "You are Claude Code.", + ], + messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }], + tools: [], + }, + }; + const body = await geminiBody(parsed); + + expect(systemInstructionText(body)).toContain("x-anthropic-billing-header"); + expect(systemInstructionText(body)).toContain("You are Claude Code."); + }); + + test("preserves non-leading x-anthropic-billing-header and leading whitespace in Cloud Code Assist", async () => { + const parsed: OcxParsedRequest = { + ...systemPromptParsed("gemini-3.8-flash"), + context: { + systemPrompt: [ + " leading indentation", + "x-anthropic-billing-header: cc_version=2.1.236.b88; cc_entrypoint=sdk-cli;", + ], + messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }], + tools: [], + }, + }; + const envelope = JSON.parse((await createGoogleAdapter(ccaProvider).buildRequest(parsed)).body) as { + request: Record; + }; + + expect(systemInstructionText(envelope.request)).toContain(" leading indentation"); + expect(systemInstructionText(envelope.request)).toContain("x-anthropic-billing-header"); + }); }); describe("google adapter — tool_choice on the wire", () => {