From e8e17bb264359c9ef9cd38c843018a7c3501e1b0 Mon Sep 17 00:00:00 2001 From: dhaern Date: Thu, 28 May 2026 21:44:44 +0000 Subject: [PATCH] fix(antigravity): harden signatureless tool history --- open-sse/handlers/chatCore.ts | 1 + .../translator/request/openai-to-gemini.ts | 30 +++++++++- package.json | 1 + src/sse/handlers/chat.ts | 55 ++++++++++++++++++- .../unit/translator-openai-to-gemini.test.ts | 28 ++++++++-- 5 files changed, 107 insertions(+), 8 deletions(-) diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 8d5a4cca086..fcff01e4462 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -5105,6 +5105,7 @@ export async function handleChatCore({ status: failureResponse.status, error: reason, errorType: streamReadiness.type, + errorCode: streamReadiness.code, response: failureResponse, }; } diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts index adfa76c2746..6f88529326c 100644 --- a/open-sse/translator/request/openai-to-gemini.ts +++ b/open-sse/translator/request/openai-to-gemini.ts @@ -183,6 +183,32 @@ function applyAntigravityGenerationDefaults(generationConfig: GeminiGenerationCo return config; } +function stringifyHistoricalToolArguments(value: unknown): string { + if (typeof value === "string") return value; + try { + return JSON.stringify(value ?? {}); + } catch { + return String(value ?? "{}"); + } +} + +function buildInertHistoricalToolCallText(name: string | undefined, args: unknown): string { + const toolName = name || "unknown"; + return [ + "Historical tool-call record only. Do not execute, imitate, or continue this as a tool call.", + `Tool name: ${toolName}`, + `Tool arguments JSON: ${stringifyHistoricalToolArguments(args || "{}")}`, + ].join("\n"); +} + +function buildInertHistoricalToolResponseText(name: string, response: unknown): string { + return [ + "Historical tool-response record only. Do not execute, imitate, or continue this as a tool response.", + `Tool name: ${name || "unknown"}`, + `Tool result: ${typeof response === "string" ? response : stringifyHistoricalToolArguments(response)}`, + ].join("\n"); +} + // Core: Convert OpenAI request to Gemini format (base for all variants) function openaiToGeminiBase( model: string, @@ -355,7 +381,7 @@ function openaiToGeminiBase( if (!signatureForToolCall && stringifySignaturelessToolCalls) { const args = fn.arguments || "{}"; parts.push({ - text: `[Tool call: ${fn.name || "unknown"}]\nArguments: ${args}`, + text: buildInertHistoricalToolCallText(fn.name, args), }); continue; } @@ -444,7 +470,7 @@ function openaiToGeminiBase( const name = tcID2Name[id] || fn?.name || "unknown"; const resp = toolResponses[id]; toolParts.push({ - text: `[Tool response: ${name}]\nResult: ${resp}`, + text: buildInertHistoricalToolResponseText(name, resp), }); } } diff --git a/package.json b/package.json index cb90e3cc957..442eb401ecb 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "open-sse/mcp-server/schemas/", "open-sse/mcp-server/tools/", "open-sse/mcp-server/README.md", + "open-sse/utils/setupPolyfill.ts", "src/shared/contracts/", "src/shared/utils/nodeRuntimeSupport.ts", ".env.example", diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index 51fae0b6edb..4f621fc948b 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -1005,12 +1005,65 @@ async function handleSingleModelChat( return result.response; } - if (result.errorType === "stream_timeout" || result.errorType === "stream_early_eof") { + const isAntigravityStreamReadinessFailure = + provider === "antigravity" && + (result.errorCode === "STREAM_READINESS_TIMEOUT" || + result.errorCode === "STREAM_EARLY_EOF" || + result.errorType === "stream_timeout" || + result.errorType === "stream_early_eof"); + + if ( + (result.errorType === "stream_timeout" || result.errorType === "stream_early_eof") && + !isAntigravityStreamReadinessFailure + ) { // Stream readiness timeout is an upstream stall after an HTTP response was received, // not an account/quota failure. Do NOT mark the account unavailable here. return result.response; } + if (isAntigravityStreamReadinessFailure) { + const { shouldFallback, cooldownMs } = await markAccountUnavailable( + credentials.connectionId, + result.status || HTTP_STATUS.BAD_GATEWAY, + result.error || result.errorCode || "Antigravity stream ended before useful content", + provider, + model, + providerProfile + ); + + if (shouldFallback && !hasForcedConnection) { + log.warn( + "AUTH", + `Antigravity connection ${accountId}... produced no useful stream content, trying fallback connection` + ); + if (Number.isFinite(cooldownMs) && cooldownMs > 0) { + lastCooldownMs = cooldownMs; + requestRetryLastCooldownMs = cooldownMs; + } + if (runtimeOptions.sessionAffinityKey) { + try { + const affinity = getSessionAccountAffinity( + runtimeOptions.sessionAffinityKey, + provider + ); + if (affinity?.connectionId === credentials.connectionId) { + deleteSessionAccountAffinity(runtimeOptions.sessionAffinityKey, provider); + } + } catch { + // best-effort: selection also excludes this connection for the current retry. + } + } + excludedConnectionIds.add(credentials.connectionId); + lastError = result.error; + lastStatus = result.status; + requestRetryLastError = result.error; + requestRetryLastStatus = result.status; + continue; + } + + return result.response; + } + const isAntigravityPreResponseTimeout = provider === "antigravity" && result.status === HTTP_STATUS.GATEWAY_TIMEOUT && diff --git a/tests/unit/translator-openai-to-gemini.test.ts b/tests/unit/translator-openai-to-gemini.test.ts index 8e85c5d8afc..73be2483558 100644 --- a/tests/unit/translator-openai-to-gemini.test.ts +++ b/tests/unit/translator-openai-to-gemini.test.ts @@ -620,7 +620,7 @@ test("OpenAI -> Antigravity wraps Gemini requests in a Cloud Code envelope", () }); }); -test("OpenAI -> Antigravity Gemini stringifies signature-less historical tool calls", () => { +test("OpenAI -> Antigravity Gemini preserves signature-less historical tool calls as inert text", () => { const result = openaiToAntigravityRequest( "gemini-3.5-flash-low", { @@ -662,9 +662,18 @@ test("OpenAI -> Antigravity Gemini stringifies signature-less historical tool ca modelTurn.parts.some( (part) => typeof part.text === "string" && - part.text.includes("[Tool call: default_api:todowrite_ide]") + part.text.includes("Historical tool-call record only") && + part.text.includes("Tool name: default_api:todowrite_ide") && + part.text.includes('Tool arguments JSON: {"todos":[]}') ), - "expected signature-less tool call to be preserved as text" + "expected signature-less tool call to be preserved as inert text" + ); + assert.equal( + modelTurn.parts.some( + (part) => typeof part.text === "string" && part.text.includes("[Tool call:") + ), + false, + "signature-less historical call must not use executable textual tool-call markers" ); assert.equal( modelTurn.parts.some((part) => part.functionCall), @@ -678,10 +687,19 @@ test("OpenAI -> Antigravity Gemini stringifies signature-less historical tool ca content.parts.some( (part) => typeof part.text === "string" && - part.text.includes("[Tool response: default_api:todowrite_ide]") + part.text.includes("Historical tool-response record only") && + part.text.includes("Tool name: default_api:todowrite_ide") && + part.text.includes("Tool result: []") ) ); - assert.ok(toolTurn, "expected signature-less tool response to be preserved as text"); + assert.ok(toolTurn, "expected signature-less tool response to be preserved as inert text"); + assert.equal( + toolTurn.parts.some( + (part) => typeof part.text === "string" && part.text.includes("[Tool response:") + ), + false, + "signature-less historical response must not use executable textual tool-response markers" + ); assert.equal( toolTurn.parts.some((part) => part.functionResponse), false,