diff --git a/open-sse/translator/concerns/thinkingUnified.js b/open-sse/translator/concerns/thinkingUnified.js index b8540468..0c98b075 100644 --- a/open-sse/translator/concerns/thinkingUnified.js +++ b/open-sse/translator/concerns/thinkingUnified.js @@ -148,18 +148,57 @@ function toKimiReasoningEffort(cfg) { return null; } +const GEMINI_LEVEL_OUTPUT_FLOOR = { + minimal: 4096, + low: 8192, + medium: 16384, + high: 65535, +}; + +function geminiBudgetOutputFloor(budget) { + if (budget === -1) return 32768; + if (!Number.isFinite(budget)) return 32768; + if (budget <= 1024) return 8192; + if (budget <= 8192) return 16384; + if (budget <= 24576) return 32768; + return 65535; +} + +function geminiLevelOutputFloor(level) { + return GEMINI_LEVEL_OUTPUT_FLOOR[level] || GEMINI_LEVEL_OUTPUT_FLOOR.high; +} + // Gemini nests thinkingConfig under generationConfig. gemini-cli / antigravity wrap // the whole request in a { request: { generationConfig } } envelope — target the // envelope's generationConfig when present, else the top-level one. +function getGeminiGenerationConfig(body) { + if (body.request && typeof body.request === "object") { + if (!body.request.generationConfig || typeof body.request.generationConfig !== "object") { + body.request.generationConfig = {}; + } + return body.request.generationConfig; + } + if (!body.generationConfig || typeof body.generationConfig !== "object") { + body.generationConfig = {}; + } + return body.generationConfig; +} + function setGeminiThinking(body, tc) { - const gc = body.request?.generationConfig - ? body.request.generationConfig - : (body.generationConfig && typeof body.generationConfig === "object" - ? body.generationConfig - : (body.generationConfig = {})); + const gc = getGeminiGenerationConfig(body); gc.thinkingConfig = tc; } +function ensureGeminiOutputFloor(body, floor, caps) { + const cap = Number.isFinite(caps?.maxOutput) ? caps.maxOutput : floor; + const target = Math.min(floor, cap); + const gc = getGeminiGenerationConfig(body); + const current = Number(gc.maxOutputTokens); + if (!Number.isFinite(current) || current < target) { + gc.maxOutputTokens = target; + } +} + // Strip every known thinking field from a body (used before re-applying / when unsupported). function stripAll(body) { delete body.thinking; @@ -203,12 +242,14 @@ function applyFormat(fmt, body, cfg, caps) { case "gemini-level": { const level = none ? "minimal" : toGeminiThinkingLevel(eff); setGeminiThinking(body, { thinkingLevel: level, includeThoughts: level !== "minimal" }); + ensureGeminiOutputFloor(body, geminiLevelOutputFloor(level), caps); break; } case "gemini-budget": { if (none && canDisable) { setGeminiThinking(body, { thinkingBudget: 0, includeThoughts: false }); break; } const budget = toBudget(eff, caps.thinkingRange); setGeminiThinking(body, { thinkingBudget: budget ?? -1, includeThoughts: true }); + ensureGeminiOutputFloor(body, geminiBudgetOutputFloor(budget ?? -1), caps); break; } case "zai": { diff --git a/open-sse/translator/request/openai-to-gemini.js b/open-sse/translator/request/openai-to-gemini.js index 9423b293..9e029e2c 100644 --- a/open-sse/translator/request/openai-to-gemini.js +++ b/open-sse/translator/request/openai-to-gemini.js @@ -281,18 +281,17 @@ function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null, isAntigra // Antigravity specific fields if (isAntigravity) { envelope.requestType = "agent"; - - // Add toolConfig for Antigravity - if (geminiCLI.tools?.length > 0) { - envelope.request.toolConfig = { - functionCallingConfig: { mode: "VALIDATED" } - }; - } } else { // Keep safetySettings for Gemini CLI envelope.request.safetySettings = geminiCLI.safetySettings; } + if (geminiCLI.tools?.length > 0) { + envelope.request.toolConfig = { + functionCallingConfig: { mode: "VALIDATED" } + }; + } + return envelope; } diff --git a/tests/translator/golden-request.test.js b/tests/translator/golden-request.test.js index a6491543..21cf49f7 100644 --- a/tests/translator/golden-request.test.js +++ b/tests/translator/golden-request.test.js @@ -54,6 +54,45 @@ describe("GOLDEN request: OpenAI → Gemini", () => { const out = translateRequest(FORMATS.OPENAI, FORMATS.GEMINI, "gemini-3-pro", baseBody(), true, { apiKey: "k" }, "gemini"); expect(clean(out)).toMatchSnapshot(); }); + + it("Gemini CLI tool requests include validated toolConfig and enough output for high thinking", () => { + const body = { + messages: [{ role: "user", content: "Call add with 7 and 35." }], + tools: [ + { + type: "function", + function: { + name: "add", + description: "Add two numbers", + parameters: { + type: "object", + properties: { + a: { type: "number" }, + b: { type: "number" }, + }, + required: ["a", "b"], + }, + }, + }, + ], + reasoning_effort: "high", + max_tokens: 128, + }; + const out = translateRequest( + FORMATS.OPENAI, + FORMATS.GEMINI_CLI, + "gemini-3.1-pro-preview", + body, + true, + { accessToken: "t", projectId: "p" }, + "gemini-cli" + ); + + expect(out.request.toolConfig).toEqual({ functionCallingConfig: { mode: "VALIDATED" } }); + expect(out.request.safetySettings).toBeDefined(); + expect(out.request.generationConfig.thinkingConfig).toEqual({ thinkingLevel: "high", includeThoughts: true }); + expect(out.request.generationConfig.maxOutputTokens).toBe(65535); + }); }); describe("GOLDEN request: OpenAI → Kiro", () => { diff --git a/tests/translator/thinking-unified.test.js b/tests/translator/thinking-unified.test.js index d547e947..3132ce85 100644 --- a/tests/translator/thinking-unified.test.js +++ b/tests/translator/thinking-unified.test.js @@ -78,11 +78,27 @@ describe("applyThinking per provider format", () => { const out = apply("gemini", "gemini-3-pro", { reasoning_effort: "auto" }, "gemini"); expect(out.generationConfig.thinkingConfig.thinkingLevel).toBe("high"); }); + it("gemini-3 high thinking raises too-small maxOutputTokens", () => { + const out = apply("gemini-cli", "gemini-3.1-pro-preview", { + request: { generationConfig: { maxOutputTokens: 128 } }, + reasoning_effort: "high", + }, "gemini-cli"); + expect(out.request.generationConfig.thinkingConfig).toEqual({ thinkingLevel: "high", includeThoughts: true }); + expect(out.request.generationConfig.maxOutputTokens).toBe(65535); + }); it("gemini-2.5 → thinkingBudget", () => { const out = apply("gemini", "gemini-2.5-flash", { reasoning_effort: "high" }, "gemini"); expect(out.generationConfig.thinkingConfig.thinkingBudget).toBe(24576); expect(out.generationConfig.thinkingConfig.thinkingLevel).toBeUndefined(); }); + it("gemini-2.5 budget thinking keeps enough room for answer tokens", () => { + const out = apply("gemini-cli", "gemini-2.5-pro", { + request: { generationConfig: { maxOutputTokens: 1024 } }, + reasoning_effort: "high", + }, "gemini-cli"); + expect(out.request.generationConfig.thinkingConfig).toEqual({ thinkingBudget: 24576, includeThoughts: true }); + expect(out.request.generationConfig.maxOutputTokens).toBe(32768); + }); it("GLM off → enable_thinking:false (not thinking.disabled)", () => { const out = apply("openai", "glm-4.6", { reasoning_effort: "none" }, "glm"); expect(out.enable_thinking).toBe(false);