fix(gemini): backfill thoughtSignature and suppress stream done sentinel
Backfill DEFAULT_THINKING_AG_SIGNATURE onto functionCall parts missing it (client history replay) and on Claude tool_use blocks, fixing 400 INVALID_ARGUMENT from Gemini-family APIs. Suppress the OpenAI-style data: [DONE] sentinel for antigravity/gemini/vertex to avoid parser crashes. Fixes #2193. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { HTTP_STATUS } from "../config/runtimeConfig.js";
|
|||||||
import { resolveSessionId } from "../utils/sessionManager.js";
|
import { resolveSessionId } from "../utils/sessionManager.js";
|
||||||
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
import { proxyAwareFetch } from "../utils/proxyFetch.js";
|
||||||
import { cleanJSONSchemaForAntigravity } from "../translator/formats/gemini.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}
|
// Sanitize function name: Gemini requires [a-zA-Z_][a-zA-Z0-9_.:\-]{0,63}
|
||||||
function sanitizeFunctionName(name) {
|
function sanitizeFunctionName(name) {
|
||||||
@@ -177,8 +178,19 @@ export class AntigravityExecutor extends BaseExecutor {
|
|||||||
if (p.thoughtSignature && !p.functionCall && !p.text) return false;
|
if (p.thoughtSignature && !p.functionCall && !p.text) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
if (role !== c.role || parts?.length !== c.parts?.length) {
|
// Gemini 3+ rejects functionCall parts without thoughtSignature. Clients (Claude Code, IDE)
|
||||||
return { ...c, role, parts };
|
// 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;
|
return c;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wrap Claude format in Cloud Code envelope for Antigravity
|
// 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 projectId = credentials?.projectId || generateProjectId();
|
||||||
|
|
||||||
const envelope = {
|
const envelope = {
|
||||||
@@ -343,6 +343,7 @@ function wrapInCloudCodeEnvelopeForClaude(model, claudeRequest, credentials = nu
|
|||||||
parts.push({ text: block.text });
|
parts.push({ text: block.text });
|
||||||
} else if (block.type === CLAUDE_BLOCK.TOOL_USE) {
|
} else if (block.type === CLAUDE_BLOCK.TOOL_USE) {
|
||||||
parts.push({
|
parts.push({
|
||||||
|
thoughtSignature: signature,
|
||||||
functionCall: {
|
functionCall: {
|
||||||
id: block.id,
|
id: block.id,
|
||||||
name: sanitizeGeminiFunctionName(block.name),
|
name: sanitizeGeminiFunctionName(block.name),
|
||||||
|
|||||||
@@ -350,7 +350,9 @@ export function createSSEStream(options = {}) {
|
|||||||
// Some clients (e.g. OpenClaw) expect the OpenAI-style sentinel:
|
// Some clients (e.g. OpenClaw) expect the OpenAI-style sentinel:
|
||||||
// data: [DONE]\n\n
|
// data: [DONE]\n\n
|
||||||
// Without it they can hang until timeout and trigger failover.
|
// 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";
|
const doneOutput = "data: [DONE]\n\n";
|
||||||
reqLogger?.appendConvertedChunk?.(doneOutput);
|
reqLogger?.appendConvertedChunk?.(doneOutput);
|
||||||
controller.enqueue(sharedEncoder.encode(doneOutput));
|
controller.enqueue(sharedEncoder.encode(doneOutput));
|
||||||
|
|||||||
Reference in New Issue
Block a user