fix(models): clamp reasoning effort to supported levels (closes #422)

This commit is contained in:
James Peter
2025-11-22 11:44:10 +10:00
parent e0881b937c
commit 5dcf7ae991
5 changed files with 128 additions and 23 deletions
@@ -17,6 +17,7 @@ use code_core::protocol::SandboxPolicy;
use code_core::slash_commands::get_enabled_agents;
use code_core::{AuthManager, ModelClient, Prompt, ResponseEvent, TextFormat};
use code_core::error::CodexErr;
use code_common::model_presets::clamp_reasoning_effort_for_model;
use code_protocol::models::{ContentItem, ReasoningItemContent, ResponseItem};
use code_core::protocol::TokenUsage;
use futures::StreamExt;
@@ -922,6 +923,11 @@ fn run_auto_loop(
if matches!(config.model_reasoning_effort, ReasoningEffort::None) {
config.model_reasoning_effort = ReasoningEffort::High;
}
let requested_effort: code_protocol::config_types::ReasoningEffort =
config.model_reasoning_effort.into();
let clamped_effort =
clamp_reasoning_effort_for_model(&config.model, requested_effort);
config.model_reasoning_effort = ReasoningEffort::from(clamped_effort);
let allowed_verbosity = supported_text_verbosity_for_model(&config.model);
config.model_text_verbosity = allowed_verbosity
.iter()
+61 -5
View File
@@ -268,14 +268,57 @@ pub fn all_model_presets() -> &'static Vec<ModelPreset> {
&PRESETS
}
pub fn allowed_text_verbosity_for_model(model: &str) -> &'static [TextVerbosityConfig] {
fn find_preset_for_model(model: &str) -> Option<&'static ModelPreset> {
let model_lower = model.to_ascii_lowercase();
PRESETS
PRESETS.iter().find(|preset| {
preset.model.eq_ignore_ascii_case(&model_lower)
|| preset.id.eq_ignore_ascii_case(&model_lower)
|| preset.display_name.eq_ignore_ascii_case(&model_lower)
})
}
fn reasoning_effort_rank(effort: ReasoningEffort) -> u8 {
match effort {
ReasoningEffort::Minimal => 0,
ReasoningEffort::Low => 1,
ReasoningEffort::Medium => 2,
ReasoningEffort::High => 3,
ReasoningEffort::XHigh => 4,
}
}
pub fn clamp_reasoning_effort_for_model(
model: &str,
requested: ReasoningEffort,
) -> ReasoningEffort {
let Some(preset) = find_preset_for_model(model) else {
return requested;
};
if preset
.supported_reasoning_efforts
.iter()
.find(|preset| {
preset.model.eq_ignore_ascii_case(&model_lower)
|| preset.id.eq_ignore_ascii_case(&model_lower)
.any(|opt| opt.effort == requested)
{
return requested;
}
let requested_rank = reasoning_effort_rank(requested);
preset
.supported_reasoning_efforts
.iter()
.min_by_key(|opt| {
let rank = reasoning_effort_rank(opt.effort);
(requested_rank.abs_diff(rank), u8::MAX - rank)
})
.map(|opt| opt.effort)
.unwrap_or(requested)
}
pub fn allowed_text_verbosity_for_model(model: &str) -> &'static [TextVerbosityConfig] {
find_preset_for_model(model)
.map(|preset| preset.supported_text_verbosity)
.unwrap_or(ALL_TEXT_VERBOSITY)
}
@@ -296,4 +339,17 @@ mod tests {
.iter()
.all(|preset| preset.id != "gpt-5.1-codex-max"));
}
#[test]
fn clamp_reasoning_effort_downgrades_to_supported_level() {
let clamped = clamp_reasoning_effort_for_model(
"gpt-5.1-codex",
ReasoningEffort::XHigh,
);
assert_eq!(clamped, ReasoningEffort::High);
let clamped_minimal =
clamp_reasoning_effort_for_model("gpt-5.1-codex-mini", ReasoningEffort::Minimal);
assert_eq!(clamped_minimal, ReasoningEffort::Medium);
}
}
+14
View File
@@ -1432,6 +1432,20 @@ impl From<code_protocol::config_types::ReasoningEffort> for ReasoningEffort {
}
}
impl From<ReasoningEffort> for code_protocol::config_types::ReasoningEffort {
fn from(v: ReasoningEffort) -> Self {
match v {
ReasoningEffort::Minimal | ReasoningEffort::None => {
code_protocol::config_types::ReasoningEffort::Minimal
}
ReasoningEffort::Low => code_protocol::config_types::ReasoningEffort::Low,
ReasoningEffort::Medium => code_protocol::config_types::ReasoningEffort::Medium,
ReasoningEffort::High => code_protocol::config_types::ReasoningEffort::High,
ReasoningEffort::XHigh => code_protocol::config_types::ReasoningEffort::XHigh,
}
}
}
impl From<code_protocol::config_types::ReasoningSummary> for ReasoningSummary {
fn from(v: code_protocol::config_types::ReasoningSummary) -> Self {
match v {
+7 -3
View File
@@ -23,7 +23,7 @@ use code_app_server_protocol::ClientRequest;
use code_protocol::ConversationId;
use code_protocol::protocol::SessionSource;
use code_common::model_presets::{builtin_model_presets, ModelPreset};
use code_common::model_presets::{builtin_model_presets, clamp_reasoning_effort_for_model, ModelPreset};
use code_core::AuthManager;
use code_core::ConversationManager;
use code_core::config_types::{ClientTools, McpServerConfig, McpServerTransportConfig, ReasoningEffort};
@@ -1592,6 +1592,10 @@ fn resolve_model_selection(model_id: &acp::ModelId, config: &Config) -> Option<M
}
fn apply_model_selection(config: &mut Config, model: &str, effort: ReasoningEffort) -> bool {
let requested_effort: code_protocol::config_types::ReasoningEffort = effort.into();
let clamped_effort: ReasoningEffort =
clamp_reasoning_effort_for_model(model, requested_effort).into();
let mut updated = false;
if !config.model.eq_ignore_ascii_case(model) {
config.model = model.to_string();
@@ -1600,8 +1604,8 @@ fn apply_model_selection(config: &mut Config, model: &str, effort: ReasoningEffo
updated = true;
}
if config.model_reasoning_effort != effort {
config.model_reasoning_effort = effort;
if config.model_reasoning_effort != clamped_effort {
config.model_reasoning_effort = clamped_effort;
updated = true;
}
+40 -15
View File
@@ -28,8 +28,9 @@ use crate::spinner;
use crate::thread_spawner;
use code_common::elapsed::format_duration;
use code_common::model_presets::ModelPreset;
use code_common::model_presets::builtin_model_presets;
use code_common::model_presets::clamp_reasoning_effort_for_model;
use code_common::model_presets::ModelPreset;
use code_core::ConversationManager;
use code_core::agent_defaults::{agent_model_spec, enabled_agent_model_specs};
use code_core::smoke_test_agent_blocking;
@@ -18655,6 +18656,12 @@ Have we met every part of this goal and is there no further work to do?"#
preset.default_reasoning_effort.into()
}
fn clamp_reasoning_for_model(model: &str, requested: ReasoningEffort) -> ReasoningEffort {
let protocol_effort: code_protocol::config_types::ReasoningEffort = requested.into();
let clamped = clamp_reasoning_effort_for_model(model, protocol_effort);
ReasoningEffort::from(clamped)
}
fn find_model_preset(&self, input: &str, presets: &[ModelPreset]) -> Option<ModelPreset> {
if presets.is_empty() {
return None;
@@ -18848,11 +18855,12 @@ Have we met every part of this goal and is there no further work to do?"#
updated = true;
}
if let Some(new_effort) = effort {
if self.config.model_reasoning_effort != new_effort {
self.config.model_reasoning_effort = new_effort;
updated = true;
}
let requested_effort = effort.unwrap_or(self.config.model_reasoning_effort);
let clamped_effort = Self::clamp_reasoning_for_model(trimmed, requested_effort);
if self.config.model_reasoning_effort != clamped_effort {
self.config.model_reasoning_effort = clamped_effort;
updated = true;
}
if updated {
@@ -18924,14 +18932,16 @@ Have we met every part of this goal and is there no further work to do?"#
self.config.review_use_chat_model = false;
let clamped_effort = Self::clamp_reasoning_for_model(trimmed, effort);
let mut updated = false;
if !self.config.review_model.eq_ignore_ascii_case(trimmed) {
self.config.review_model = trimmed.to_string();
updated = true;
}
if self.config.review_model_reasoning_effort != effort {
self.config.review_model_reasoning_effort = effort;
if self.config.review_model_reasoning_effort != clamped_effort {
self.config.review_model_reasoning_effort = clamped_effort;
updated = true;
}
@@ -19077,13 +19087,15 @@ Have we met every part of this goal and is there no further work to do?"#
self.config.planning_use_chat_model = false;
let clamped_effort = Self::clamp_reasoning_for_model(trimmed, effort);
let mut updated = false;
if !self.config.planning_model.eq_ignore_ascii_case(trimmed) {
self.config.planning_model = trimmed.to_string();
updated = true;
}
if self.config.planning_model_reasoning_effort != effort {
self.config.planning_model_reasoning_effort = effort;
if self.config.planning_model_reasoning_effort != clamped_effort {
self.config.planning_model_reasoning_effort = clamped_effort;
updated = true;
}
@@ -19231,14 +19243,16 @@ Have we met every part of this goal and is there no further work to do?"#
self.config.auto_drive_use_chat_model = false;
let clamped_effort = Self::clamp_reasoning_for_model(trimmed, effort);
let mut updated = false;
if !self.config.auto_drive.model.eq_ignore_ascii_case(trimmed) {
self.config.auto_drive.model = trimmed.to_string();
updated = true;
}
if self.config.auto_drive.model_reasoning_effort != effort {
self.config.auto_drive.model_reasoning_effort = effort;
if self.config.auto_drive.model_reasoning_effort != clamped_effort {
self.config.auto_drive.model_reasoning_effort = clamped_effort;
updated = true;
}
@@ -19489,14 +19503,25 @@ Have we met every part of this goal and is there no further work to do?"#
}
pub(crate) fn set_reasoning_effort(&mut self, new_effort: ReasoningEffort) {
let clamped_effort = Self::clamp_reasoning_for_model(&self.config.model, new_effort);
if clamped_effort != new_effort {
let requested = Self::format_reasoning_effort(new_effort);
let applied = Self::format_reasoning_effort(clamped_effort);
self.bottom_pane.flash_footer_notice(format!(
"{} does not support {} reasoning; using {} instead.",
self.config.model, requested, applied
));
}
// Update the config
self.config.model_reasoning_effort = new_effort;
self.config.model_reasoning_effort = clamped_effort;
// Send ConfigureSession op to update the backend
let op = Op::ConfigureSession {
provider: self.config.model_provider.clone(),
model: self.config.model.clone(),
model_reasoning_effort: new_effort,
model_reasoning_effort: clamped_effort,
model_reasoning_summary: self.config.model_reasoning_summary,
model_text_verbosity: self.config.model_text_verbosity,
user_instructions: self.config.user_instructions.clone(),
@@ -19513,7 +19538,7 @@ Have we met every part of this goal and is there no further work to do?"#
// Add status message to history (replaceable system notice)
let placement = self.ui_placement_for_now();
let state = history_cell::new_reasoning_output(&new_effort);
let state = history_cell::new_reasoning_output(&self.config.model_reasoning_effort);
let cell = crate::history_cell::PlainHistoryCell::from_state(state.clone());
self.push_system_cell(
Box::new(cell),