From c56cc558904c571d694acebc5fc9d02b90056dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20He=CC=81ritier?= Date: Fri, 21 Aug 2026 00:12:08 +0200 Subject: [PATCH] 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. --- pkg/model/provider/gemini/client.go | 8 ++- pkg/model/provider/gemini/client_test.go | 64 ++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/pkg/model/provider/gemini/client.go b/pkg/model/provider/gemini/client.go index d14194371a..d486615c9d 100644 --- a/pkg/model/provider/gemini/client.go +++ b/pkg/model/provider/gemini/client.go @@ -425,7 +425,7 @@ func extractMimeType(dataURLPrefix string) string { return "image/jpeg" // Default fallback } -// buildConfig creates GenerateContentConfig from model config +// BuildConfig creates GenerateContentConfig from model config. func (c *Client) buildConfig() *genai.GenerateContentConfig { config := &genai.GenerateContentConfig{} if c.ModelConfig.MaxTokens != nil { @@ -453,7 +453,11 @@ func (c *Client) buildConfig() *genai.GenerateContentConfig { // Apply thinking configuration for Gemini models. // See https://ai.google.dev/gemini-api/docs/thinking if c.ModelOptions.NoThinking() { - // NoThinking requested (e.g. title generation). For Gemini 3+ models + if c.ModelOptions.GeneratingTitle() { + return config + } + + // NoThinking requested (e.g. MCP sampling). For Gemini 3+ models // that always think, use the lowest level and bump MaxOutputTokens so // internal reasoning doesn't consume the entire budget. Gemini 2.5 and // older can fully disable thinking with ThinkingBudget=0. diff --git a/pkg/model/provider/gemini/client_test.go b/pkg/model/provider/gemini/client_test.go index 9b344ddc0d..e68adc7fe7 100644 --- a/pkg/model/provider/gemini/client_test.go +++ b/pkg/model/provider/gemini/client_test.go @@ -10,10 +10,74 @@ import ( "github.com/docker/docker-agent/pkg/chat" "github.com/docker/docker-agent/pkg/config/latest" "github.com/docker/docker-agent/pkg/model/provider/base" + "github.com/docker/docker-agent/pkg/model/provider/options" "github.com/docker/docker-agent/pkg/modelsdev" "github.com/docker/docker-agent/pkg/tools" ) +func TestBuildConfig_NoThinking(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + model string + opts []options.Opt + wantThinking bool + wantMinTokens bool + }{ + { + name: "title generation omits thinking config", + model: "gemini-3-flash", + opts: []options.Opt{options.WithGeneratingTitle(), options.WithNoThinking()}, + wantThinking: false, + }, + { + name: "MCP sampling disables Gemini 3 thinking", + model: "gemini-3-flash", + opts: []options.Opt{options.WithNoThinking()}, + wantThinking: true, + wantMinTokens: true, + }, + { + name: "MCP sampling disables Gemini 2.5 thinking", + model: "gemini-2.5-flash", + opts: []options.Opt{options.WithNoThinking()}, + wantThinking: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + client := &Client{Config: base.Config{ + ModelConfig: latest.ModelConfig{ + Provider: "google", + Model: tt.model, + ThinkingBudget: &latest.ThinkingBudget{Effort: "high"}, + }, + ModelOptions: options.Apply(tt.opts...), + }} + + config := client.buildConfig() + if !tt.wantThinking { + assert.Nil(t, config.ThinkingConfig) + return + } + + require.NotNil(t, config.ThinkingConfig) + assert.False(t, config.ThinkingConfig.IncludeThoughts) + if tt.wantMinTokens { + assert.Equal(t, genai.ThinkingLevelLow, config.ThinkingConfig.ThinkingLevel) + assert.GreaterOrEqual(t, config.MaxOutputTokens, int32(200)) + return + } + require.NotNil(t, config.ThinkingConfig.ThinkingBudget) + assert.Zero(t, *config.ThinkingConfig.ThinkingBudget) + }) + } +} + func TestBuildConfig_Gemini25_ThinkingBudget(t *testing.T) { t.Parallel()