fix(#1316): resolve thinking leaks, consecutive roles, and missing thoughtSignatures for Antigravity translator
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user