From ca944f280f5c75e0579e203e41b1fc98e07936cc Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 16 Apr 2026 10:28:59 -0300 Subject: [PATCH] fix(#1316): resolve thinking leaks, consecutive roles, and missing thoughtSignatures for Antigravity translator --- open-sse/executors/antigravity.ts | 22 +++++-- open-sse/services/thinkingBudget.ts | 3 +- .../translator/request/claude-to-gemini.ts | 17 +++--- .../translator/request/openai-to-gemini.ts | 16 ++--- src/lib/dataPaths.js | 59 +++++++++++++++++++ 5 files changed, 92 insertions(+), 25 deletions(-) create mode 100644 src/lib/dataPaths.js diff --git a/open-sse/executors/antigravity.ts b/open-sse/executors/antigravity.ts index d56403b98d..f8d5223854 100644 --- a/open-sse/executors/antigravity.ts +++ b/open-sse/executors/antigravity.ts @@ -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, diff --git a/open-sse/services/thinkingBudget.ts b/open-sse/services/thinkingBudget.ts index ed000e0e13..ae03ae76db 100644 --- a/open-sse/services/thinkingBudget.ts +++ b/open-sse/services/thinkingBudget.ts @@ -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); } diff --git a/open-sse/translator/request/claude-to-gemini.ts b/open-sse/translator/request/claude-to-gemini.ts index 07e6538841..02713b3977 100644 --- a/open-sse/translator/request/claude-to-gemini.ts +++ b/open-sse/translator/request/claude-to-gemini.ts @@ -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, + }; + } } } } diff --git a/open-sse/translator/request/openai-to-gemini.ts b/open-sse/translator/request/openai-to-gemini.ts index e375193109..ddf31bea24 100644 --- a/open-sse/translator/request/openai-to-gemini.ts +++ b/open-sse/translator/request/openai-to-gemini.ts @@ -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); } diff --git a/src/lib/dataPaths.js b/src/lib/dataPaths.js new file mode 100644 index 0000000000..ad9d20a6bf --- /dev/null +++ b/src/lib/dataPaths.js @@ -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; +}