fix(gemini-cli): raise output floor for thinking and add validated toolConfig (#2486)

Gemini CLI requests with small max_tokens spend the whole output budget on
thoughts after reasoning_effort maps to thinkingConfig, returning blank
content or finish=length. Raise maxOutputTokens floors per thinking level/
budget (clamped to caps.maxOutput). Also emit toolConfig
functionCallingConfig.mode=VALIDATED for Gemini CLI tool requests to avoid
MALFORMED_FUNCTION_CALL.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
DOMANHDUC
2026-07-10 11:27:41 +07:00
committed by decolua
parent 0d4d4bc261
commit 7610f28f42
4 changed files with 107 additions and 12 deletions

View File

@@ -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": {

View File

@@ -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;
}

View File

@@ -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", () => {

View File

@@ -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);