Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crates/goose-cli/src/commands/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,14 @@ pub async fn configure_provider_dialog() -> anyhow::Result<bool> {
}
};

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...");
Expand Down
1 change: 1 addition & 0 deletions crates/goose/src/config/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,7 @@ config_value!(GOOSE_PROVIDER, String);
config_value!(GOOSE_MODEL, String);
config_value!(GOOSE_PROMPT_EDITOR, Option<String>);
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.
Expand Down
99 changes: 90 additions & 9 deletions crates/goose/src/providers/formats/google.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,21 @@ struct GenerationConfig {
temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
max_output_tokens: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
thinking_config: Option<ThinkingConfig>,
}

#[derive(Serialize)]
#[serde(rename_all = "lowercase")]
enum ThinkingLevel {
Low,
High,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ThinkingConfig {
thinking_level: ThinkingLevel,
}

#[derive(Serialize)]
Expand All @@ -546,6 +561,45 @@ struct GoogleRequest<'a> {
generation_config: Option<GenerationConfig>,
}

fn get_thinking_config(model_config: &ModelConfig) -> Option<ThinkingConfig> {
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::<String>("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,
Expand All @@ -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,
Comment thread
rabi marked this conversation as resolved.
})
} else {
None
};

let request = GoogleRequest {
system_instruction: SystemInstruction {
Expand Down Expand Up @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -101,6 +106,10 @@ export const SwitchModelModal = ({
const [loadingModels, setLoadingModels] = useState<boolean>(false);
const [userClearedModel, setUserClearedModel] = useState(false);
const [providerErrors, setProviderErrors] = useState<Record<string, string>>({});
const [thinkingLevel, setThinkingLevel] = useState<string>('low');

const modelName = usePredefinedModels ? selectedPredefinedModel?.name : model;
const isGemini3Model = modelName?.toLowerCase().startsWith('gemini-3') ?? false;

// Validate form data
const validateForm = useCallback(() => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -410,7 +430,7 @@ export const SwitchModelModal = ({
className="peer sr-only"
/>
<div
className="h-4 w-4 rounded-full border border-border-default
className="h-4 w-4 rounded-full border border-border-default
peer-checked:border-[6px] peer-checked:border-black dark:peer-checked:border-white
peer-checked:bg-white dark:peer-checked:bg-black
transition-all duration-200 ease-in-out group-hover:border-border-default"
Expand All @@ -424,6 +444,24 @@ export const SwitchModelModal = ({
{attemptedSubmit && validationErrors.model && (
<div className="text-red-500 text-sm mt-1">{validationErrors.model}</div>
)}

{isGemini3Model && (
<div className="mt-2">
<label className="text-sm text-textSubtle mb-1 block">
Thinking Level
<span className="text-xs text-textMuted ml-2">(Gemini 3 models only)</span>
</label>
<Select
options={THINKING_LEVEL_OPTIONS}
value={THINKING_LEVEL_OPTIONS.find((o) => o.value === thinkingLevel)}
onChange={(newValue: unknown) => {
const option = newValue as { value: string; label: string } | null;
setThinkingLevel(option?.value || 'low');
}}
placeholder="Select thinking level"
/>
</div>
)}
</div>
) : (
/* Manual Provider/Model Selection */
Expand Down Expand Up @@ -522,6 +560,24 @@ export const SwitchModelModal = ({
)}
</div>
)}

{isGemini3Model && (
<div className="mt-2">
<label className="text-sm text-textSubtle mb-1 block">
Thinking Level
<span className="text-xs text-textMuted ml-2">(Gemini 3 models only)</span>
</label>
<Select
options={THINKING_LEVEL_OPTIONS}
value={THINKING_LEVEL_OPTIONS.find((o) => o.value === thinkingLevel)}
onChange={(newValue: unknown) => {
const option = newValue as { value: string; label: string } | null;
setThinkingLevel(option?.value || 'low');
}}
placeholder="Select thinking level"
/>
</div>
)}
</>
)}
</div>
Expand Down