From 6f74786384bca4b5ff37bc9da58941f9b62d16a9 Mon Sep 17 00:00:00 2001 From: jg-noncelogic Date: Thu, 12 Feb 2026 09:01:28 -0500 Subject: [PATCH] fix(antigravity): opus 4.6 forward-compat model + thinking signature sanitization bypass (#14218) Two fixes for Google Antigravity (Cloud Code Assist) reliability: 1. Forward-compat model fallback: pi-ai's model registry doesn't include claude-opus-4-6-thinking. Add resolveAntigravityOpus46ForwardCompatModel() that clones the opus-4-5 template so the correct api ("google-gemini-cli") and baseUrl are preserved. Fixes #13765. 2. Fix thinking.signature rejection: The API returns Claude thinking blocks without signatures, then rejects them on replay. The existing sanitizer strips unsigned blocks, but the orphaned-user-message path in attempt.ts bypassed it by reading directly from disk. Now applies sanitizeAntigravityThinkingBlocks at that code path. Co-authored-by: Claude Opus 4.6 --- src/agents/pi-embedded-runner/google.ts | 2 +- src/agents/pi-embedded-runner/model.test.ts | 35 ++++++++++++++++ src/agents/pi-embedded-runner/model.ts | 43 ++++++++++++++++++++ src/agents/pi-embedded-runner/run/attempt.ts | 6 ++- 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/agents/pi-embedded-runner/google.ts b/src/agents/pi-embedded-runner/google.ts index 03383622bd5..5acdc64b096 100644 --- a/src/agents/pi-embedded-runner/google.ts +++ b/src/agents/pi-embedded-runner/google.ts @@ -59,7 +59,7 @@ function isValidAntigravitySignature(value: unknown): value is string { return ANTIGRAVITY_SIGNATURE_RE.test(trimmed); } -function sanitizeAntigravityThinkingBlocks(messages: AgentMessage[]): AgentMessage[] { +export function sanitizeAntigravityThinkingBlocks(messages: AgentMessage[]): AgentMessage[] { let touched = false; const out: AgentMessage[] = []; for (const msg of messages) { diff --git a/src/agents/pi-embedded-runner/model.test.ts b/src/agents/pi-embedded-runner/model.test.ts index 4a9bba8caf0..9603f1e1e98 100644 --- a/src/agents/pi-embedded-runner/model.test.ts +++ b/src/agents/pi-embedded-runner/model.test.ts @@ -207,6 +207,41 @@ describe("resolveModel", () => { }); }); + it("builds a google-antigravity forward-compat fallback for claude-opus-4-6-thinking", () => { + const templateModel = { + id: "claude-opus-4-5-thinking", + name: "Claude Opus 4.5 Thinking", + provider: "google-antigravity", + api: "google-gemini-cli", + baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com", + reasoning: true, + input: ["text", "image"] as const, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1000000, + maxTokens: 64000, + }; + + vi.mocked(discoverModels).mockReturnValue({ + find: vi.fn((provider: string, modelId: string) => { + if (provider === "google-antigravity" && modelId === "claude-opus-4-5-thinking") { + return templateModel; + } + return null; + }), + } as unknown as ReturnType); + + const result = resolveModel("google-antigravity", "claude-opus-4-6-thinking", "/tmp/agent"); + + expect(result.error).toBeUndefined(); + expect(result.model).toMatchObject({ + provider: "google-antigravity", + id: "claude-opus-4-6-thinking", + api: "google-gemini-cli", + baseUrl: "https://daily-cloudcode-pa.sandbox.googleapis.com", + reasoning: true, + }); + }); + it("keeps unknown-model errors for non-gpt-5 openai-codex ids", () => { const result = resolveModel("openai-codex", "gpt-4.1-mini", "/tmp/agent"); expect(result.model).toBeUndefined(); diff --git a/src/agents/pi-embedded-runner/model.ts b/src/agents/pi-embedded-runner/model.ts index 2f489ffdab5..4c01aa3dba4 100644 --- a/src/agents/pi-embedded-runner/model.ts +++ b/src/agents/pi-embedded-runner/model.ts @@ -114,6 +114,41 @@ function resolveAnthropicOpus46ForwardCompatModel( return undefined; } +// google-antigravity's model catalog in pi-ai can lag behind the actual platform. +// When a google-antigravity model ID contains "opus-4-6" (or "opus-4.6") but isn't +// in the registry yet, clone the opus-4-5 template so the correct api +// ("google-gemini-cli") and baseUrl are preserved. +const ANTIGRAVITY_OPUS_46_STEMS = ["claude-opus-4-6", "claude-opus-4.6"] as const; +const ANTIGRAVITY_OPUS_45_TEMPLATES = ["claude-opus-4-5-thinking", "claude-opus-4-5"] as const; + +function resolveAntigravityOpus46ForwardCompatModel( + provider: string, + modelId: string, + modelRegistry: ModelRegistry, +): Model | undefined { + if (normalizeProviderId(provider) !== "google-antigravity") { + return undefined; + } + const lower = modelId.trim().toLowerCase(); + const isOpus46 = ANTIGRAVITY_OPUS_46_STEMS.some( + (stem) => lower === stem || lower.startsWith(`${stem}-`), + ); + if (!isOpus46) { + return undefined; + } + for (const templateId of ANTIGRAVITY_OPUS_45_TEMPLATES) { + const template = modelRegistry.find("google-antigravity", templateId) as Model | null; + if (template) { + return normalizeModelCompat({ + ...template, + id: modelId.trim(), + name: modelId.trim(), + } as Model); + } + } + return undefined; +} + export function buildInlineProviderModels( providers: Record, ): InlineModelEntry[] { @@ -199,6 +234,14 @@ export function resolveModel( if (anthropicForwardCompat) { return { model: anthropicForwardCompat, authStorage, modelRegistry }; } + const antigravityForwardCompat = resolveAntigravityOpus46ForwardCompatModel( + provider, + modelId, + modelRegistry, + ); + if (antigravityForwardCompat) { + return { model: antigravityForwardCompat, authStorage, modelRegistry }; + } const providerCfg = providers[provider]; if (providerCfg || modelId.startsWith("mock-")) { const fallbackModel: Model = normalizeModelCompat({ diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index c010d5811d0..4735eab3db4 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -67,6 +67,7 @@ import { buildEmbeddedExtensionPaths } from "../extensions.js"; import { applyExtraParamsToAgent } from "../extra-params.js"; import { logToolSchemasForGoogle, + sanitizeAntigravityThinkingBlocks, sanitizeSessionHistory, sanitizeToolsForGoogle, } from "../google.js"; @@ -770,7 +771,10 @@ export async function runEmbeddedAttempt( sessionManager.resetLeaf(); } const sessionContext = sessionManager.buildSessionContext(); - activeSession.agent.replaceMessages(sessionContext.messages); + const sanitizedOrphan = transcriptPolicy.normalizeAntigravityThinkingBlocks + ? sanitizeAntigravityThinkingBlocks(sessionContext.messages) + : sessionContext.messages; + activeSession.agent.replaceMessages(sanitizedOrphan); log.warn( `Removed orphaned user message to prevent consecutive user turns. ` + `runId=${params.runId} sessionId=${params.sessionId}`,