Skip to content

Commit b4dbc21

Browse files
committed
fix(#3996): preserve Gemini user thinking budgets outside title generation
Title generation requests must omit thinkingConfig so image-capable Gemini models can produce a plain text title. Keep the explicit no-thinking behavior for MCP sampling and ordinary requests unchanged, including Gemini 3 minimum reasoning settings and Gemini 2.5 zero-budget suppression.
1 parent 2dcd040 commit b4dbc21

2 files changed

Lines changed: 70 additions & 2 deletions

File tree

pkg/model/provider/gemini/client.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ func extractMimeType(dataURLPrefix string) string {
425425
return "image/jpeg" // Default fallback
426426
}
427427

428-
// buildConfig creates GenerateContentConfig from model config
428+
// BuildConfig creates GenerateContentConfig from model config.
429429
func (c *Client) buildConfig() *genai.GenerateContentConfig {
430430
config := &genai.GenerateContentConfig{}
431431
if c.ModelConfig.MaxTokens != nil {
@@ -453,7 +453,11 @@ func (c *Client) buildConfig() *genai.GenerateContentConfig {
453453
// Apply thinking configuration for Gemini models.
454454
// See https://ai.google.dev/gemini-api/docs/thinking
455455
if c.ModelOptions.NoThinking() {
456-
// NoThinking requested (e.g. title generation). For Gemini 3+ models
456+
if c.ModelOptions.GeneratingTitle() {
457+
return config
458+
}
459+
460+
// NoThinking requested (e.g. MCP sampling). For Gemini 3+ models
457461
// that always think, use the lowest level and bump MaxOutputTokens so
458462
// internal reasoning doesn't consume the entire budget. Gemini 2.5 and
459463
// older can fully disable thinking with ThinkingBudget=0.

pkg/model/provider/gemini/client_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,74 @@ import (
1010
"github.com/docker/docker-agent/pkg/chat"
1111
"github.com/docker/docker-agent/pkg/config/latest"
1212
"github.com/docker/docker-agent/pkg/model/provider/base"
13+
"github.com/docker/docker-agent/pkg/model/provider/options"
1314
"github.com/docker/docker-agent/pkg/modelsdev"
1415
"github.com/docker/docker-agent/pkg/tools"
1516
)
1617

18+
func TestBuildConfig_NoThinking(t *testing.T) {
19+
t.Parallel()
20+
21+
tests := []struct {
22+
name string
23+
model string
24+
opts []options.Opt
25+
wantThinking bool
26+
wantMinTokens bool
27+
}{
28+
{
29+
name: "title generation omits thinking config",
30+
model: "gemini-3-flash",
31+
opts: []options.Opt{options.WithGeneratingTitle(), options.WithNoThinking()},
32+
wantThinking: false,
33+
},
34+
{
35+
name: "MCP sampling disables Gemini 3 thinking",
36+
model: "gemini-3-flash",
37+
opts: []options.Opt{options.WithNoThinking()},
38+
wantThinking: true,
39+
wantMinTokens: true,
40+
},
41+
{
42+
name: "MCP sampling disables Gemini 2.5 thinking",
43+
model: "gemini-2.5-flash",
44+
opts: []options.Opt{options.WithNoThinking()},
45+
wantThinking: true,
46+
},
47+
}
48+
49+
for _, tt := range tests {
50+
t.Run(tt.name, func(t *testing.T) {
51+
t.Parallel()
52+
53+
client := &Client{Config: base.Config{
54+
ModelConfig: latest.ModelConfig{
55+
Provider: "google",
56+
Model: tt.model,
57+
ThinkingBudget: &latest.ThinkingBudget{Effort: "high"},
58+
},
59+
ModelOptions: options.Apply(tt.opts...),
60+
}}
61+
62+
config := client.buildConfig()
63+
if !tt.wantThinking {
64+
assert.Nil(t, config.ThinkingConfig)
65+
return
66+
}
67+
68+
require.NotNil(t, config.ThinkingConfig)
69+
assert.False(t, config.ThinkingConfig.IncludeThoughts)
70+
if tt.wantMinTokens {
71+
assert.Equal(t, genai.ThinkingLevelLow, config.ThinkingConfig.ThinkingLevel)
72+
assert.GreaterOrEqual(t, config.MaxOutputTokens, int32(200))
73+
return
74+
}
75+
require.NotNil(t, config.ThinkingConfig.ThinkingBudget)
76+
assert.Zero(t, *config.ThinkingConfig.ThinkingBudget)
77+
})
78+
}
79+
}
80+
1781
func TestBuildConfig_Gemini25_ThinkingBudget(t *testing.T) {
1882
t.Parallel()
1983

0 commit comments

Comments
 (0)