Omit unset thinking effort

Preserve an unset unified thinking effort through provider formatting and the model switcher. Providers now only serialize effort fields when the user or legacy config explicitly sets an effort, allowing upstream API defaults to apply.

Signed-off-by: jh-block <jhugo@block.xyz>
This commit is contained in:
jh-block
2026-05-20 12:49:32 +02:00
parent 64465da3d7
commit 5179823d25
6 changed files with 81 additions and 79 deletions
+1 -1
View File
@@ -1100,7 +1100,7 @@ config_value!(CURSOR_AGENT_COMMAND, String, "cursor-agent");
config_value!(CODEX_COMMAND, String, "codex");
config_value!(CODEX_ENABLE_SKILLS, String, "true");
config_value!(CODEX_SKIP_GIT_CHECK, String, "false");
config_value!(CHATGPT_CODEX_REASONING_EFFORT, String, "medium");
config_value!(CHATGPT_CODEX_REASONING_EFFORT, String);
config_value!(GOOSE_SEARCH_PATHS, Vec<String>);
config_value!(GOOSE_MODE, GooseMode);
+33 -15
View File
@@ -209,32 +209,34 @@ fn build_input_items(messages: &[Message]) -> Result<Vec<Value>> {
Ok(items)
}
fn get_reasoning_effort(model_name: &str) -> String {
let config = crate::config::Config::global();
let effort = config
.get_chatgpt_codex_reasoning_effort()
.map(String::from)
.unwrap_or_else(|_| "medium".to_string());
fn configured_reasoning_effort(model_name: &str, config: &crate::config::Config) -> Option<String> {
let effort = String::from(config.get_chatgpt_codex_reasoning_effort().ok()?);
let valid_levels = reasoning_levels_for_model(model_name);
if valid_levels.contains(&effort.as_str()) {
effort
Some(effort)
} else {
tracing::warn!(
"Invalid CHATGPT_CODEX_REASONING_EFFORT '{}' for model '{}', using 'medium'",
"Invalid CHATGPT_CODEX_REASONING_EFFORT '{}' for model '{}', omitting reasoning effort",
effort,
model_name
);
"medium".to_string()
None
}
}
fn reasoning_effort_for_config(model_config: &ModelConfig) -> Option<String> {
reasoning_effort_for_config_with_config(model_config, crate::config::Config::global())
}
fn reasoning_effort_for_config_with_config(
model_config: &ModelConfig,
config: &crate::config::Config,
) -> Option<String> {
use crate::model::ThinkingEffort;
model_config
.thinking_effort()
.map(|effort| {
match model_config.thinking_effort() {
Some(effort) => {
let valid_levels = reasoning_levels_for_model(&model_config.model_name);
let preferred_levels: &[&str] = match effort {
ThinkingEffort::Off => return None,
@@ -248,8 +250,9 @@ fn reasoning_effort_for_config(model_config: &ModelConfig) -> Option<String> {
.iter()
.find(|level| valid_levels.contains(level))
.map(|level| (*level).to_string())
})
.unwrap_or_else(|| Some(get_reasoning_effort(&model_config.model_name)))
}
None => configured_reasoning_effort(&model_config.model_name, config),
}
}
fn create_codex_request(
@@ -1242,6 +1245,21 @@ mod tests {
assert!(payload.get("reasoning_effort").is_none());
}
#[test]
fn test_create_codex_request_omits_reasoning_when_effort_unset() {
let _guard = env_lock::lock_env([
("GOOSE_THINKING_EFFORT", None::<&str>),
("CHATGPT_CODEX_REASONING_EFFORT", None::<&str>),
]);
let config_file = tempfile::NamedTempFile::new().unwrap();
let secrets_file = tempfile::NamedTempFile::new().unwrap();
let empty_config =
crate::config::Config::new_with_file_secrets(config_file.path(), secrets_file.path())
.unwrap();
assert!(configured_reasoning_effort("gpt-5.2-codex", &empty_config).is_none());
}
#[test_case(
JwtClaims {
chatgpt_account_id: Some("account-1".to_string()),
+2 -5
View File
@@ -79,10 +79,7 @@ impl CodexProvider {
effort: Option<crate::model::ThinkingEffort>,
) -> Option<String> {
use crate::model::ThinkingEffort;
match effort
.or_else(Self::legacy_reasoning_effort)
.unwrap_or(ThinkingEffort::High)
{
match effort.or_else(Self::legacy_reasoning_effort)? {
ThinkingEffort::Off => Some("none".to_string()),
ThinkingEffort::Low => Some("low".to_string()),
ThinkingEffort::Medium => Some("medium".to_string()),
@@ -1259,7 +1256,7 @@ mod tests {
);
assert_eq!(
CodexProvider::map_thinking_effort("gpt-5.2-codex", None),
Some("high".to_string())
None
);
}
+26 -45
View File
@@ -503,13 +503,21 @@ pub fn thinking_effort(model_config: &ModelConfig) -> ThinkingEffort {
.unwrap_or(ThinkingEffort::High)
}
fn adaptive_effort_value(model_config: &ModelConfig) -> &'static str {
match model_config.thinking_effort() {
Some(ThinkingEffort::Low) => "low",
Some(ThinkingEffort::Medium) => "medium",
Some(ThinkingEffort::Max) if is_claude_opus_47(&model_config.model_name) => "xhigh",
Some(ThinkingEffort::Max) => "max",
Some(ThinkingEffort::Off | ThinkingEffort::High) | None => "high",
pub fn adaptive_effort_value(model_config: &ModelConfig) -> Option<&'static str> {
adaptive_effort_value_for_model(&model_config.model_name, model_config.thinking_effort())
}
fn adaptive_effort_value_for_model(
model_name: &str,
effort: Option<ThinkingEffort>,
) -> Option<&'static str> {
match effort? {
ThinkingEffort::Off => None,
ThinkingEffort::Low => Some("low"),
ThinkingEffort::Medium => Some("medium"),
ThinkingEffort::High => Some("high"),
ThinkingEffort::Max if is_claude_opus_47(model_name) => Some("xhigh"),
ThinkingEffort::Max => Some("max"),
}
}
@@ -562,8 +570,9 @@ fn apply_thinking_config(
"thinking".to_string(),
json!({"type": "adaptive", "display": "summarized"}),
);
let effort = adaptive_effort_value(model_config);
obj.insert("output_config".to_string(), json!({"effort": effort}));
if let Some(effort) = adaptive_effort_value(model_config) {
obj.insert("output_config".to_string(), json!({"effort": effort}));
}
}
ThinkingType::Enabled => {
let budget_tokens = thinking_budget_tokens(model_config);
@@ -587,8 +596,9 @@ fn apply_thinking_config(
"thinking".to_string(),
json!({"type": "adaptive", "display": "summarized"}),
);
let effort = adaptive_effort_value(model_config);
obj.insert("output_config".to_string(), json!({"effort": effort}));
if let Some(effort) = adaptive_effort_value(model_config) {
obj.insert("output_config".to_string(), json!({"effort": effort}));
}
} else {
let budget_tokens = thinking_budget_tokens(model_config);
obj.insert("max_tokens".to_string(), json!(max_tokens + budget_tokens));
@@ -1245,40 +1255,11 @@ mod tests {
}
#[test]
fn test_create_request_preserved_opus_47_context_defaults_to_high_effort() -> Result<()> {
let _guard = env_lock::lock_env([
("GOOSE_THINKING_EFFORT", None::<&str>),
("CLAUDE_THINKING_TYPE", None::<&str>),
("CLAUDE_THINKING_ENABLED", None::<&str>),
("ANTHROPIC_THINKING_BUDGET", None::<&str>),
("CLAUDE_THINKING_BUDGET", None::<&str>),
("ANTHROPIC_PRESERVE_THINKING_CONTEXT", None::<&str>),
("ANTHROPIC_PRESERVE_UNSIGNED_THINKING", None::<&str>),
]);
let mut config = cfg("claude-opus-4-7");
config.max_tokens = Some(4096);
let messages = vec![
Message::assistant().with_content(MessageContent::thinking("internal", "")),
Message::user().with_text("Continue"),
];
let payload = create_request_with_options(
&config,
"system",
&messages,
&[],
AnthropicFormatOptions {
preserve_unsigned_thinking: true,
preserve_thinking_context: true,
},
)?;
assert_eq!(payload["thinking"]["type"], "adaptive");
assert_eq!(payload["output_config"]["effort"], "high");
assert_eq!(payload["max_tokens"], 4096);
Ok(())
fn test_adaptive_effort_omits_unset_default() {
assert_eq!(
adaptive_effort_value_for_model("claude-opus-4-7", None),
None
);
}
#[test]
@@ -1,7 +1,7 @@
use crate::conversation::message::{Message, MessageContent};
use crate::model::ModelConfig;
use crate::providers::formats::anthropic::{
thinking_budget_tokens, thinking_effort, thinking_type, ThinkingType,
adaptive_effort_value, thinking_budget_tokens, thinking_type, ThinkingType,
};
use crate::providers::utils::{
convert_image, detect_image_path, extract_reasoning_effort, is_openai_responses_model,
@@ -237,10 +237,9 @@ fn apply_claude_thinking_config(payload: &mut Value, model_config: &ModelConfig)
match thinking_type(model_config) {
ThinkingType::Adaptive => {
obj.insert("thinking".to_string(), json!({ "type": "adaptive" }));
obj.insert(
"output_config".to_string(),
json!({ "effort": thinking_effort(model_config).to_string() }),
);
if let Some(effort) = adaptive_effort_value(model_config) {
obj.insert("output_config".to_string(), json!({ "effort": effort }));
}
obj.insert(
"max_completion_tokens".to_string(),
json!(model_config.max_output_tokens()),
@@ -413,12 +413,16 @@ export const SwitchModelModal = ({
};
if (showThinkingControl) {
const effort = thinkingEffort ?? modelObj.request_params?.thinking_effort ?? 'off';
modelObj = {
...modelObj,
request_params: { ...modelObj.request_params, thinking_effort: effort },
};
upsert('GOOSE_THINKING_EFFORT', effort, false).catch(console.warn);
const effort = (thinkingEffort ?? modelObj.request_params?.thinking_effort) as
| ThinkingEffort
| undefined;
if (effort) {
modelObj = {
...modelObj,
request_params: { ...modelObj.request_params, thinking_effort: effort },
};
upsert('GOOSE_THINKING_EFFORT', effort, false).catch(console.warn);
}
}
const success = await changeModel(sessionId, modelObj);
@@ -704,6 +708,9 @@ export const SwitchModelModal = ({
}
};
const selectedThinkingEffort =
thinkingEffort ?? (selectedPredefinedModel?.request_params?.thinking_effort as ThinkingEffort);
const thinkingEffortControl = showThinkingControl && (
<div className="mt-2">
<label className="text-sm text-textSubtle mb-1 block">
@@ -711,10 +718,10 @@ export const SwitchModelModal = ({
</label>
<Select
options={THINKING_EFFORT_OPTIONS}
value={THINKING_EFFORT_OPTIONS.find((o) => o.value === (thinkingEffort ?? 'off'))}
value={THINKING_EFFORT_OPTIONS.find((o) => o.value === selectedThinkingEffort) ?? null}
onChange={(newValue: unknown) => {
const option = newValue as { value: ThinkingEffort; label: string } | null;
setThinkingEffort(option?.value || 'off');
setThinkingEffort(option?.value ?? null);
}}
placeholder={intl.formatMessage(i18n.selectEffortLevel)}
/>