Skip to content

Commit 8156995

Browse files
committed
fix(gemini): make thinkingConfig model-aware for 2.5 vs 3.x
Gemini 2.5 uses `thinkingBudget` (and 2.5 Pro cannot disable thinking), while Gemini 3.x uses `thinkingLevel` ("low"/"high") and cannot be disabled at all. Translate the plugin's generic `thinkingLevel` per family instead of sending it through verbatim.
1 parent 239e340 commit 8156995

2 files changed

Lines changed: 75 additions & 8 deletions

File tree

lua/codecompanion/adapters/http/gemini.lua

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,19 +118,46 @@ return {
118118
return true
119119
end,
120120

121+
---Translate the plugin's generic `thinkingLevel` into a model-appropriate
122+
---`thinkingConfig`. Gemini 2.5 uses `thinkingBudget` (and Pro cannot be
123+
---disabled); Gemini 3.x uses `thinkingLevel` ("low" or "high") and cannot
124+
---be disabled at all. See https://ai.google.dev/gemini-api/docs/thinking.
121125
---@param self CodeCompanion.HTTPAdapter
122126
---@param params table
123127
---@param messages table
124128
---@return table
125129
form_parameters = function(self, params, messages)
126-
if self.temp.thinkingLevel then
130+
local level = self.temp and self.temp.thinkingLevel
131+
if not level then
132+
return params
133+
end
134+
135+
local model = self.schema.model.default
136+
if type(model) == "function" then
137+
model = model(self)
138+
end
139+
140+
local thinking_config
141+
if type(model) == "string" and model:match("^gemini%-3") then
142+
-- Gemini 3.x: thinkingLevel is "low" or "high"; thinking cannot be disabled.
143+
local level_map = { high = "high", medium = "high", low = "low" }
144+
local mapped = level_map[level]
145+
if mapped then
146+
thinking_config = { thinkingLevel = mapped }
147+
end
148+
elseif type(model) == "string" and model:match("^gemini%-2%.5") then
149+
-- Gemini 2.5: thinkingBudget in tokens. 2.5 Pro cannot disable thinking.
127150
local budgets = { none = 0, low = 1024, medium = 8192, high = 24576 }
128-
local budget = budgets[self.temp.thinkingLevel]
129-
if budget ~= nil then
130-
params.generationConfig = params.generationConfig or {}
131-
params.generationConfig.thinkingConfig = { thinkingBudget = budget }
151+
local budget = budgets[level]
152+
if budget ~= nil and not (budget == 0 and model:match("pro")) then
153+
thinking_config = { thinkingBudget = budget }
132154
end
133155
end
156+
157+
if thinking_config then
158+
params.generationConfig = params.generationConfig or {}
159+
params.generationConfig.thinkingConfig = thinking_config
160+
end
134161
return params
135162
end,
136163

tests/adapters/http/test_gemini.lua

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -463,15 +463,16 @@ T["Gemini adapter"]["No Streaming"]["can output for the inline assistant"] = fun
463463
h.expect_starts_with("Elegant, dynamic.", adapter.handlers.inline_output(adapter, json).output)
464464
end
465465

466-
T["Gemini adapter"]["form_parameters sets thinkingConfig as object for reasoning models"] = function()
467-
local levels = {
466+
T["Gemini adapter"]["form_parameters maps thinkingLevel to thinkingBudget for Gemini 2.5 Flash"] = function()
467+
adapter.schema.model.default = "gemini-2.5-flash"
468+
local cases = {
468469
{ level = "none", budget = 0 },
469470
{ level = "low", budget = 1024 },
470471
{ level = "medium", budget = 8192 },
471472
{ level = "high", budget = 24576 },
472473
}
473474

474-
for _, tc in ipairs(levels) do
475+
for _, tc in ipairs(cases) do
475476
adapter.temp = { thinkingLevel = tc.level }
476477
local params = adapter.handlers.form_parameters(adapter, {}, {})
477478
h.eq(
@@ -482,6 +483,45 @@ T["Gemini adapter"]["form_parameters sets thinkingConfig as object for reasoning
482483
end
483484
end
484485

486+
T["Gemini adapter"]["form_parameters refuses to disable thinking on Gemini 2.5 Pro"] = function()
487+
adapter.schema.model.default = "gemini-2.5-pro"
488+
adapter.temp = { thinkingLevel = "none" }
489+
local params = adapter.handlers.form_parameters(adapter, {}, {})
490+
-- 2.5 Pro cannot disable thinking; the field must be omitted entirely.
491+
h.eq(nil, params.generationConfig)
492+
493+
adapter.temp = { thinkingLevel = "high" }
494+
params = adapter.handlers.form_parameters(adapter, {}, {})
495+
h.eq({ thinkingBudget = 24576 }, params.generationConfig.thinkingConfig)
496+
end
497+
498+
T["Gemini adapter"]["form_parameters maps thinkingLevel for Gemini 3.x reasoning models"] = function()
499+
adapter.schema.model.default = "gemini-3.1-pro-preview"
500+
local cases = {
501+
{ level = "high", expected = "high" },
502+
{ level = "medium", expected = "high" },
503+
{ level = "low", expected = "low" },
504+
}
505+
506+
for _, tc in ipairs(cases) do
507+
adapter.temp = { thinkingLevel = tc.level }
508+
local params = adapter.handlers.form_parameters(adapter, {}, {})
509+
h.eq(
510+
{ thinkingLevel = tc.expected },
511+
params.generationConfig.thinkingConfig,
512+
string.format("thinkingLevel '%s' should map to '%s'", tc.level, tc.expected)
513+
)
514+
end
515+
end
516+
517+
T["Gemini adapter"]["form_parameters omits thinkingConfig when disabling on Gemini 3.x"] = function()
518+
adapter.schema.model.default = "gemini-3.1-pro-preview"
519+
adapter.temp = { thinkingLevel = "none" }
520+
-- Gemini 3.x does not support disabling thinking; the field is omitted.
521+
local params = adapter.handlers.form_parameters(adapter, {}, {})
522+
h.eq(nil, params.generationConfig)
523+
end
524+
485525
T["Gemini adapter"]["form_parameters does not set thinkingConfig when thinkingLevel is absent"] = function()
486526
adapter.temp = {}
487527
local params = adapter.handlers.form_parameters(adapter, {}, {})

0 commit comments

Comments
 (0)