fix(#1316): resolve thinking leaks, consecutive roles, and missing thoughtSignatures for Antigravity translator

This commit is contained in:
diegosouzapw
2026-04-16 10:28:59 -03:00
parent b140181257
commit ca944f280f
5 changed files with 92 additions and 25 deletions
+18 -4
View File
@@ -150,13 +150,27 @@ export class AntigravityExecutor extends BaseExecutor {
// Antigravity rejects synthetic thought text, but Gemini 3+ requires any
// returned thoughtSignature metadata to survive model tool-call turns.
const parts =
c.parts?.filter((p) => !p.thought && (hasFunctionCall || !p.thoughtSignature)) || [];
c.parts?.filter((p) => {
// Drop empty text parts
if (typeof p.text === "string" && p.text === "") return false;
// Drop empty functionCalls
if (p.functionCall && !p.functionCall.name) return false;
return !p.thought && (hasFunctionCall || !p.thoughtSignature);
}) || [];
return { ...c, role, parts };
}) || [];
const contents = normalizedContents.filter((c) =>
Array.isArray(c.parts) ? c.parts.length > 0 : true
);
// Merge consecutive same-role entries and filter out empty sequences
const contents = [];
for (const c of normalizedContents) {
if (!Array.isArray(c.parts) || c.parts.length === 0) continue;
if (contents.length > 0 && contents[contents.length - 1].role === c.role) {
contents[contents.length - 1].parts.push(...c.parts);
} else {
contents.push(c);
}
}
const transformedRequest = {
...body.request,
+2 -1
View File
@@ -159,7 +159,8 @@ export function applyThinkingBudget(body, config = null) {
// Early exit: strip ALL reasoning/thinking params for models that don't support them.
// Sending thinking params to unsupported models (e.g. AG claude-sonnet-4-6) causes 400 errors.
const modelStr = typeof body.model === "string" ? body.model : "";
if (modelStr && !supportsReasoning(modelStr)) {
const isClaude = modelStr.toLowerCase().includes("claude");
if (modelStr && (!supportsReasoning(modelStr) || (!isClaude && modelStr.includes("gemini")))) {
return stripThinkingConfig(body);
}
@@ -152,19 +152,20 @@ export function claudeToGeminiRequest(model, body, stream) {
// Map Claude roles to Gemini roles
const geminiRole = msg.role === "assistant" ? "model" : "user";
// Gemini 3+ expects the signature on the first functionCall part in a tool-call
// Gemini 3+ expects the signature on all functionCall parts in a tool-call
// batch. If the assistant turn had no explicit thinking block, inject a fallback
// signature into that first functionCall. (#927)
// signature into all functionCalls.
if (geminiRole === "model") {
const hasFunctionCall = parts.some((p) => p.functionCall);
const hasSignature = parts.some((p) => p.thoughtSignature);
if (hasFunctionCall && !hasSignature) {
const fcIndex = parts.findIndex((p) => p.functionCall);
if (fcIndex >= 0) {
parts[fcIndex] = {
...parts[fcIndex],
thoughtSignature: DEFAULT_THINKING_GEMINI_SIGNATURE,
};
for (let i = 0; i < parts.length; i++) {
if (parts[i].functionCall) {
parts[i] = {
...parts[i],
thoughtSignature: DEFAULT_THINKING_GEMINI_SIGNATURE,
};
}
}
}
}
@@ -213,22 +213,17 @@ function openaiToGeminiBase(model, body, stream, toolNameOptions: GeminiToolName
.find((signature) => typeof signature === "string" && signature.length > 0);
const shouldUseEmbeddedSignature = !parts.some((p) => p.thoughtSignature);
let embeddedSignatureUsed = false;
for (const tc of msg.tool_calls) {
if (tc.type !== "function") continue;
const args = tryParseJSON(tc.function?.arguments || "{}");
const signatureForToolCall = getGeminiThoughtSignature(tc.id);
const embeddedThoughtSignature =
shouldUseEmbeddedSignature && !embeddedSignatureUsed
? firstPersistedSignature ||
signatureForToolCall ||
DEFAULT_THINKING_GEMINI_SIGNATURE
: undefined;
const embeddedThoughtSignature = shouldUseEmbeddedSignature
? firstPersistedSignature || signatureForToolCall || DEFAULT_THINKING_GEMINI_SIGNATURE
: undefined;
// Gemini expects the signature on the functionCall part itself. For
// parallel calls, only the first functionCall in the batch carries it.
// Gemini expects the signature on the functionCall part itself.
parts.push({
...(embeddedThoughtSignature ? { thoughtSignature: embeddedThoughtSignature } : {}),
functionCall: {
@@ -238,9 +233,6 @@ function openaiToGeminiBase(model, body, stream, toolNameOptions: GeminiToolName
},
});
if (embeddedThoughtSignature) {
embeddedSignatureUsed = true;
}
toolCallIds.push(tc.id);
}
+59
View File
@@ -0,0 +1,59 @@
"use strict";
var __importDefault =
(this && this.__importDefault) ||
function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.APP_NAME = void 0;
exports.getLegacyDotDataDir = getLegacyDotDataDir;
exports.getDefaultDataDir = getDefaultDataDir;
exports.resolveDataDir = resolveDataDir;
exports.isSamePath = isSamePath;
const path_1 = __importDefault(require("path"));
const os_1 = __importDefault(require("os"));
exports.APP_NAME = "omniroute";
function safeHomeDir() {
try {
return os_1.default.homedir();
} catch {
return process.cwd();
}
}
function normalizeConfiguredPath(dir) {
if (typeof dir !== "string") return null;
const trimmed = dir.trim();
if (!trimmed) return null;
return path_1.default.resolve(trimmed);
}
function getLegacyDotDataDir() {
return path_1.default.join(safeHomeDir(), `.${exports.APP_NAME}`);
}
function getDefaultDataDir() {
const homeDir = safeHomeDir();
if (process.platform === "win32") {
const appData = process.env.APPDATA || path_1.default.join(homeDir, "AppData", "Roaming");
return path_1.default.join(appData, exports.APP_NAME);
}
// Support XDG on Linux/macOS when explicitly configured.
const xdgConfigHome = normalizeConfiguredPath(process.env.XDG_CONFIG_HOME);
if (xdgConfigHome) {
return path_1.default.join(xdgConfigHome, exports.APP_NAME);
}
return getLegacyDotDataDir();
}
function resolveDataDir({ isCloud = false } = {}) {
if (isCloud) return "/tmp";
const configured = normalizeConfiguredPath(process.env.DATA_DIR);
if (configured) return configured;
return getDefaultDataDir();
}
function isSamePath(a, b) {
if (!a || !b) return false;
const normalizedA = path_1.default.resolve(a);
const normalizedB = path_1.default.resolve(b);
if (process.platform === "win32") {
return normalizedA.toLowerCase() === normalizedB.toLowerCase();
}
return normalizedA === normalizedB;
}