diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index 5f5e581b8b16..5bf844316a61 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -707,6 +707,14 @@ pub async fn configure_provider_dialog() -> anyhow::Result { } }; + if model.to_lowercase().starts_with("gemini-3") { + let thinking_level: &str = cliclack::select("Select thinking level for Gemini 3:") + .item("low", "Low - Better latency, lighter reasoning", "") + .item("high", "High - Deeper reasoning, higher latency", "") + .interact()?; + config.set_gemini3_thinking_level(thinking_level)?; + } + // Test the configuration let spin = spinner(); spin.start("Checking your configuration..."); diff --git a/crates/goose/src/config/base.rs b/crates/goose/src/config/base.rs index 4cb55812c0e8..f03652f7921d 100644 --- a/crates/goose/src/config/base.rs +++ b/crates/goose/src/config/base.rs @@ -968,6 +968,7 @@ config_value!(GOOSE_PROVIDER, String); config_value!(GOOSE_MODEL, String); config_value!(GOOSE_PROMPT_EDITOR, Option); config_value!(GOOSE_MAX_ACTIVE_AGENTS, usize); +config_value!(GEMINI3_THINKING_LEVEL, String); /// Load init-config.yaml from workspace root if it exists. /// This function is shared between the config recovery and the init_config endpoint. diff --git a/crates/goose/src/providers/formats/google.rs b/crates/goose/src/providers/formats/google.rs index 296cf38b9dc9..eeef7f9253d0 100644 --- a/crates/goose/src/providers/formats/google.rs +++ b/crates/goose/src/providers/formats/google.rs @@ -533,6 +533,21 @@ struct GenerationConfig { temperature: Option, #[serde(skip_serializing_if = "Option::is_none")] max_output_tokens: Option, + #[serde(skip_serializing_if = "Option::is_none")] + thinking_config: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "lowercase")] +enum ThinkingLevel { + Low, + High, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct ThinkingConfig { + thinking_level: ThinkingLevel, } #[derive(Serialize)] @@ -546,6 +561,45 @@ struct GoogleRequest<'a> { generation_config: Option, } +fn get_thinking_config(model_config: &ModelConfig) -> Option { + if !model_config + .model_name + .to_lowercase() + .starts_with("gemini-3") + { + return None; + } + + let thinking_level_str = model_config + .request_params + .as_ref() + .and_then(|params| params.get("thinking_level")) + .and_then(|v| v.as_str()) + .map(|s| s.to_lowercase()) + .or_else(|| { + crate::config::Config::global() + .get_param::("gemini3_thinking_level") + .ok() + .map(|s| s.to_lowercase()) + }) + .unwrap_or_else(|| "low".to_string()); + + let thinking_level = match thinking_level_str.as_str() { + "high" => ThinkingLevel::High, + "low" => ThinkingLevel::Low, + invalid => { + tracing::warn!( + "Invalid thinking level '{}' for model '{}'. Valid levels: low, high. Using 'low'.", + invalid, + model_config.model_name, + ); + ThinkingLevel::Low + } + }; + + Some(ThinkingConfig { thinking_level }) +} + pub fn create_request( model_config: &ModelConfig, system: &str, @@ -560,15 +614,20 @@ pub fn create_request( }) }; - let generation_config = - if model_config.temperature.is_some() || model_config.max_tokens.is_some() { - Some(GenerationConfig { - temperature: model_config.temperature.map(|t| t as f64), - max_output_tokens: model_config.max_tokens, - }) - } else { - None - }; + let thinking_config = get_thinking_config(model_config); + + let generation_config = if model_config.temperature.is_some() + || model_config.max_tokens.is_some() + || thinking_config.is_some() + { + Some(GenerationConfig { + temperature: model_config.temperature.map(|t| t as f64), + max_output_tokens: model_config.max_tokens, + thinking_config, + }) + } else { + None + }; let request = GoogleRequest { system_instruction: SystemInstruction { @@ -1293,4 +1352,26 @@ data: [DONE]"#; assert_eq!(schema["properties"]["field"]["$ref"], "#/$defs/MyType"); assert!(schema.get("$defs").is_some()); } + + #[test] + fn test_get_thinking_config() { + use crate::model::ModelConfig; + + // Test 1: Gemini 3 model defaults to low thinking level + let config = ModelConfig::new("gemini-3-pro").unwrap(); + let result = get_thinking_config(&config); + assert!(result.is_some()); + let thinking_config = result.unwrap(); + assert!(matches!(thinking_config.thinking_level, ThinkingLevel::Low)); + + // Test 2: Case-insensitive model detection + let config = ModelConfig::new("Gemini-3-Flash").unwrap(); + let result = get_thinking_config(&config); + assert!(result.is_some()); + + // Test 3: Non-Gemini 3 model returns None + let config = ModelConfig::new("gpt-4o").unwrap(); + let result = get_thinking_config(&config); + assert!(result.is_none()); + } } diff --git a/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx b/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx index d005534f4654..920dacd77671 100644 --- a/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx +++ b/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx @@ -21,6 +21,11 @@ import { getPredefinedModelsFromEnv, shouldShowPredefinedModels } from '../prede import { ProviderType } from '../../../../api'; import { trackModelChanged } from '../../../../utils/analytics'; +const THINKING_LEVEL_OPTIONS = [ + { value: 'low', label: 'Low - Better latency, lighter reasoning' }, + { value: 'high', label: 'High - Deeper reasoning, higher latency' }, +]; + const PREFERRED_MODEL_PATTERNS = [ /claude-sonnet-4/i, /claude-4/i, @@ -101,6 +106,10 @@ export const SwitchModelModal = ({ const [loadingModels, setLoadingModels] = useState(false); const [userClearedModel, setUserClearedModel] = useState(false); const [providerErrors, setProviderErrors] = useState>({}); + const [thinkingLevel, setThinkingLevel] = useState('low'); + + const modelName = usePredefinedModels ? selectedPredefinedModel?.name : model; + const isGemini3Model = modelName?.toLowerCase().startsWith('gemini-3') ?? false; // Validate form data const validateForm = useCallback(() => { @@ -148,7 +157,18 @@ export const SwitchModelModal = ({ } else { const providerMetaData = await getProviderMetadata(provider || '', getProviders); const providerDisplayName = providerMetaData.display_name; - modelObj = { name: model, provider: provider, subtext: providerDisplayName } as Model; + modelObj = { + name: model, + provider: provider, + subtext: providerDisplayName, + } as Model; + } + + if (isGemini3Model) { + modelObj = { + ...modelObj, + request_params: { ...modelObj.request_params, thinking_level: thinkingLevel }, + }; } await changeModel(sessionId, modelObj); @@ -410,7 +430,7 @@ export const SwitchModelModal = ({ className="peer sr-only" />
{validationErrors.model}
)} + + {isGemini3Model && ( +
+ + o.value === thinkingLevel)} + onChange={(newValue: unknown) => { + const option = newValue as { value: string; label: string } | null; + setThinkingLevel(option?.value || 'low'); + }} + placeholder="Select thinking level" + /> +
+ )} )}