diff --git a/open-sse/executors/antigravity.js b/open-sse/executors/antigravity.js index cfd63482..5249a3ec 100644 --- a/open-sse/executors/antigravity.js +++ b/open-sse/executors/antigravity.js @@ -6,6 +6,7 @@ import { HTTP_STATUS } from "../config/runtimeConfig.js"; import { resolveSessionId } from "../utils/sessionManager.js"; import { proxyAwareFetch } from "../utils/proxyFetch.js"; import { cleanJSONSchemaForAntigravity } from "../translator/formats/gemini.js"; +import { DEFAULT_THINKING_AG_SIGNATURE } from "../config/defaultThinkingSignature.js"; // Sanitize function name: Gemini requires [a-zA-Z_][a-zA-Z0-9_.:\-]{0,63} function sanitizeFunctionName(name) { @@ -177,8 +178,19 @@ export class AntigravityExecutor extends BaseExecutor { if (p.thoughtSignature && !p.functionCall && !p.text) return false; return true; }); - if (role !== c.role || parts?.length !== c.parts?.length) { - return { ...c, role, parts }; + // Gemini 3+ rejects functionCall parts without thoughtSignature. Clients (Claude Code, IDE) + // don't persist thoughtSignature in their history, so backfill the default signature on any + // functionCall part that arrives without one. + const needsBackfill = parts?.some(p => p.functionCall && !p.thoughtSignature) ?? false; + if (role !== c.role || parts?.length !== c.parts?.length || needsBackfill) { + return { + ...c, role, + parts: needsBackfill + ? parts.map(p => (p.functionCall && !p.thoughtSignature) + ? { ...p, thoughtSignature: DEFAULT_THINKING_AG_SIGNATURE } + : p) + : parts, + }; } return c; }); diff --git a/open-sse/translator/request/openai-to-gemini.js b/open-sse/translator/request/openai-to-gemini.js index fe181631..6fa44f60 100644 --- a/open-sse/translator/request/openai-to-gemini.js +++ b/open-sse/translator/request/openai-to-gemini.js @@ -299,7 +299,7 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra } // Wrap Claude format in Cloud Code envelope for Antigravity -function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = null) { +function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = null, signature = DEFAULT_THINKING_AG_SIGNATURE) { const projectId = credentials?.projectId || generateProjectId(); const envelope = { @@ -343,6 +343,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu parts.push({ text: block.text }); } else if (block.type === CLAUDE_BLOCK.TOOL_USE) { parts.push({ + thoughtSignature: signature, functionCall: { id: block.id, name: sanitizeGeminiFunctionName(block.name), diff --git a/open-sse/utils/stream.js b/open-sse/utils/stream.js index 387891aa..082b8033 100644 --- a/open-sse/utils/stream.js +++ b/open-sse/utils/stream.js @@ -350,7 +350,9 @@ export function createSSEStream(options = {}) { // Some clients (e.g. OpenClaw) expect the OpenAI-style sentinel: // data: [DONE]\n\n // Without it they can hang until timeout and trigger failover. - if (!streamDoneSent) { + // Gemini-family clients (Antigravity, Vertex, Gemini) reject this sentinel with 400 syntax errors. + const isGeminiFamily = provider === "antigravity" || provider === "gemini" || provider === "vertex"; + if (!streamDoneSent && !isGeminiFamily) { const doneOutput = "data: [DONE]\n\n"; reqLogger?.appendConvertedChunk?.(doneOutput); controller.enqueue(sharedEncoder.encode(doneOutput));