feat(google): add ThinkingLevel support for Gemini 3+ models

Xavier Capaldi created

Gemini 3+ uses thinking_level (LOW/MEDIUM/HIGH/MINIMAL) instead of
thinking_budget. The two are mutually exclusive per Google's API.
Maps to genai.ThinkingConfig.ThinkingLevel already available in v1.45.0.

Change summary

providers/google/google.go              | 11 +++++++++++
providers/google/provider_options.go    | 17 +++++++++++++++--
providertests/google_test.go            | 20 ++++++++++++++++++++
providertests/provider_registry_test.go |  6 ++++++
4 files changed, 52 insertions(+), 2 deletions(-)

Detailed changes

providers/google/google.go 🔗

@@ -242,6 +242,14 @@ func (g languageModel) prepareParams(call fantasy.Call) (*genai.GenerateContentC
 			})
 			providerOptions.ThinkingConfig.ThinkingBudget = fantasy.Opt(int64(128))
 		}
+
+		if providerOptions.ThinkingConfig.ThinkingLevel != nil &&
+			providerOptions.ThinkingConfig.ThinkingBudget != nil {
+			return nil, nil, nil, &fantasy.Error{
+				Title:   "invalid argument",
+				Message: "thinking_level and thinking_budget are mutually exclusive",
+			}
+		}
 	}
 
 	isGemmaModel := strings.HasPrefix(strings.ToLower(g.modelID), "gemma-")
@@ -298,6 +306,9 @@ func (g languageModel) prepareParams(call fantasy.Call) (*genai.GenerateContentC
 			tmp := int32(*providerOptions.ThinkingConfig.ThinkingBudget) //nolint: gosec
 			config.ThinkingConfig.ThinkingBudget = &tmp
 		}
+		if providerOptions.ThinkingConfig.ThinkingLevel != nil {
+			config.ThinkingConfig.ThinkingLevel = genai.ThinkingLevel(*providerOptions.ThinkingConfig.ThinkingLevel)
+		}
 	}
 	for _, safetySetting := range providerOptions.SafetySettings {
 		config.SafetySettings = append(config.SafetySettings, &genai.SafetySetting{

providers/google/provider_options.go 🔗

@@ -31,10 +31,23 @@ func init() {
 	})
 }
 
+// ThinkingLevel controls the amount of thinking a model does.
+// Use this for Gemini 3+ models instead of ThinkingBudget.
+// Mutually exclusive with ThinkingBudget.
+type ThinkingLevel = string
+
+const (
+	ThinkingLevelLow     ThinkingLevel = "LOW"
+	ThinkingLevelMedium  ThinkingLevel = "MEDIUM"
+	ThinkingLevelHigh    ThinkingLevel = "HIGH"
+	ThinkingLevelMinimal ThinkingLevel = "MINIMAL"
+)
+
 // ThinkingConfig represents thinking configuration for the Google provider.
 type ThinkingConfig struct {
-	ThinkingBudget  *int64 `json:"thinking_budget"`
-	IncludeThoughts *bool  `json:"include_thoughts"`
+	ThinkingBudget  *int64  `json:"thinking_budget,omitempty"`
+	IncludeThoughts *bool   `json:"include_thoughts,omitempty"`
+	ThinkingLevel   *string `json:"thinking_level,omitempty"`
 }
 
 // ReasoningMetadata represents reasoning metadata for the Google provider.

providertests/google_test.go 🔗

@@ -56,6 +56,26 @@ func TestGoogleThinking(t *testing.T) {
 	testThinking(t, pairs, testGoogleThinking)
 }
 
+func TestGoogleThinkingLevel(t *testing.T) {
+	opts := fantasy.ProviderOptions{
+		google.Name: &google.ProviderOptions{
+			ThinkingConfig: &google.ThinkingConfig{
+				ThinkingLevel:   fantasy.Opt(google.ThinkingLevelHigh),
+				IncludeThoughts: fantasy.Opt(true),
+			},
+		},
+	}
+
+	var pairs []builderPair
+	for _, m := range geminiTestModels {
+		if !m.reasoning {
+			continue
+		}
+		pairs = append(pairs, builderPair{m.name, geminiBuilder(m.model), opts, nil})
+	}
+	testThinking(t, pairs, testGoogleThinking)
+}
+
 func TestGoogleObjectGeneration(t *testing.T) {
 	var pairs []builderPair
 	for _, m := range geminiTestModels {

providertests/provider_registry_test.go 🔗

@@ -181,6 +181,9 @@ func TestProviderRegistry_Serialization_GoogleOptions(t *testing.T) {
 			google.Name: &google.ProviderOptions{
 				CachedContent: "cached-123",
 				Threshold:     "BLOCK_ONLY_HIGH",
+				ThinkingConfig: &google.ThinkingConfig{
+					ThinkingLevel: fantasy.Opt(google.ThinkingLevelHigh),
+				},
 			},
 		},
 	}
@@ -197,6 +200,9 @@ func TestProviderRegistry_Serialization_GoogleOptions(t *testing.T) {
 	require.True(t, ok)
 	require.Equal(t, "cached-123", opt.CachedContent)
 	require.Equal(t, "BLOCK_ONLY_HIGH", opt.Threshold)
+	require.NotNil(t, opt.ThinkingConfig)
+	require.NotNil(t, opt.ThinkingConfig.ThinkingLevel)
+	require.Equal(t, google.ThinkingLevelHigh, *opt.ThinkingConfig.ThinkingLevel)
 }
 
 func TestProviderRegistry_Serialization_OpenRouterOptions(t *testing.T) {