mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
feat(google): add configurable thinking level for Gemini 3 models (#6585)
Signed-off-by: rabi <ramishra@redhat.com>
This commit is contained in:
@@ -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...");
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)]
|
||||
@@ -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,
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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(() => {
|
||||
@@ -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"
|
||||
/>
|
||||
<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"
|
||||
@@ -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 */
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user