[codex] Support model-defined reasoning efforts (#26444)

## Summary
- accept non-empty model-defined reasoning effort values while
preserving built-in effort behavior
- propagate the non-Copy effort type through core, app-server, TUI,
telemetry, and persistence call sites
- preserve string wire encoding and expose an open-string schema for
clients
- update model selection and shortcut behavior for model-advertised
effort values

## Root cause
`ReasoningEffort` gained a string-backed custom variant, so it could no
longer implement `Copy` or rely on derived closed-enum serialization.
Existing consumers still moved effort values from shared references and
assumed a fixed built-in value set.

## Validation
- `just fmt`
- Local tests and compilation were not run per request; relying on CI.
This commit is contained in:
Ahmed Ibrahim authored and GitHub committed 2026-06-04 13:36:24 -07:00
1 parent 4a70e0ac1b
commit 8ac304c299
97 files changed
+685 -581

No files matched your search

+6 -10
View File
@@ -712,21 +712,17 @@ impl App {
.add_info_message("Reset local memories.".to_string(), /*hint*/ None);
}
pub(super) fn reasoning_label(reasoning_effort: Option<ReasoningEffortConfig>) -> &'static str {
pub(super) fn reasoning_label(reasoning_effort: Option<&ReasoningEffortConfig>) -> String {
match reasoning_effort {
Some(ReasoningEffortConfig::Minimal) => "minimal",
Some(ReasoningEffortConfig::Low) => "low",
Some(ReasoningEffortConfig::Medium) => "medium",
Some(ReasoningEffortConfig::High) => "high",
Some(ReasoningEffortConfig::XHigh) => "xhigh",
None | Some(ReasoningEffortConfig::None) => "default",
None | Some(ReasoningEffortConfig::None) => "default".to_string(),
Some(reasoning_effort) => reasoning_effort.as_str().to_string(),
}
}
pub(super) fn reasoning_label_for(
model: &str,
reasoning_effort: Option<ReasoningEffortConfig>,
) -> Option<&'static str> {
reasoning_effort: Option<&ReasoningEffortConfig>,
) -> Option<String> {
(!model.starts_with("codex-auto-")).then(|| Self::reasoning_label(reasoning_effort))
}
@@ -737,7 +733,7 @@ impl App {
pub(super) fn on_update_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) {
// TODO(aibrahim): Remove this and don't use config as a state object.
// Instead, explicitly pass the stored collaboration mode's effort into new sessions.
self.config.model_reasoning_effort = effort;
self.config.model_reasoning_effort = effort.clone();
self.chat_widget.set_reasoning_effort(effort);
}
+10 -6
View File
@@ -750,7 +750,7 @@ impl App {
self.chat_widget.on_connectors_loaded(result, is_final);
}
AppEvent::UpdateReasoningEffort(effort) => {
self.on_update_reasoning_effort(effort);
self.on_update_reasoning_effort(effort.clone());
self.sync_active_thread_reasoning_setting(app_server, effort)
.await;
}
@@ -1300,19 +1300,23 @@ impl App {
AppEvent::PersistModelSelection { model, effort } => {
match crate::config_update::write_config_batch(
app_server.request_handle(),
crate::config_update::build_model_selection_edits(model.as_str(), effort),
crate::config_update::build_model_selection_edits(
model.as_str(),
effort.as_ref(),
),
)
.await
{
Ok(_) => {
let effort_label = effort
.map(|selected_effort| selected_effort.to_string())
.as_ref()
.map(std::string::ToString::to_string)
.unwrap_or_else(|| "default".to_string());
tracing::info!("Selected model: {model}, Selected effort: {effort_label}");
let mut message = format!("Model changed to {model}");
if let Some(label) = Self::reasoning_label_for(&model, effort) {
if let Some(label) = Self::reasoning_label_for(&model, effort.as_ref()) {
message.push(' ');
message.push_str(label);
message.push_str(&label);
}
self.chat_widget.add_info_message(message, /*hint*/ None);
}
@@ -1607,7 +1611,7 @@ impl App {
self.chat_widget.set_rate_limit_switch_prompt_hidden(hidden);
}
AppEvent::UpdatePlanModeReasoningEffort(effort) => {
self.config.plan_mode_reasoning_effort = effort;
self.config.plan_mode_reasoning_effort = effort.clone();
self.chat_widget.set_plan_mode_reasoning_effort(effort);
self.sync_active_thread_plan_mode_reasoning_setting(app_server)
.await;
+5 -3
View File
@@ -152,9 +152,11 @@ pub(super) fn apply_accepted_model_migration(
});
config.model = Some(target_model.clone());
config.model_reasoning_effort = Some(target_default_effort);
config.model_reasoning_effort = Some(target_default_effort.clone());
app_event_tx.send(AppEvent::UpdateModel(target_model.clone()));
app_event_tx.send(AppEvent::UpdateReasoningEffort(Some(target_default_effort)));
app_event_tx.send(AppEvent::UpdateReasoningEffort(Some(
target_default_effort.clone(),
)));
app_event_tx.send(AppEvent::PersistModelSelection {
model: target_model,
effort: Some(target_default_effort),
@@ -290,7 +292,7 @@ pub(super) async fn handle_model_migration_prompt_if_needed(
app_event_tx,
model.to_string(),
target_model.clone(),
target_preset.default_reasoning_effort,
target_preset.default_reasoning_effort.clone(),
);
}
ModelMigrationOutcome::Rejected => {
+2 -2
View File
@@ -5241,7 +5241,7 @@ async fn override_turn_context_sends_thread_settings_update() {
.expect("thread/start should succeed");
let thread_id = started.session.thread_id;
let initial_model = started.session.model.clone();
let initial_effort = started.session.reasoning_effort;
let initial_effort = started.session.reasoning_effort.clone();
app.enqueue_primary_thread_session(started.session, started.turns)
.await
.expect("primary thread should be registered");
@@ -5466,7 +5466,7 @@ async fn inactive_thread_settings_notification_updates_cached_collaboration_mode
model: "gpt-plan".to_string(),
model_provider: "openai".to_string(),
service_tier: None,
effort: collaboration_mode.settings.reasoning_effort,
effort: collaboration_mode.settings.reasoning_effort.clone(),
summary: None,
collaboration_mode: collaboration_mode.clone(),
personality: Some(Personality::Pragmatic),
+1 -1
View File
@@ -606,7 +606,7 @@ impl App {
permissions_override,
config.permissions.user_visible_workspace_roots(),
model.to_string(),
*effort,
effort.clone(),
*summary,
service_tier.clone(),
collaboration_mode.clone(),
+3 -3
View File
@@ -124,7 +124,7 @@ impl App {
.as_ref()
.map(|profile| profile.id.clone()),
model: model.clone(),
effort: effort.unwrap_or_default(),
effort: effort.clone().unwrap_or_default(),
summary: *summary,
service_tier: service_tier.clone(),
collaboration_mode: collaboration_mode.clone(),
@@ -172,7 +172,7 @@ impl App {
fn apply_thread_settings_to_session(session: &mut ThreadSessionState, settings: &ThreadSettings) {
if settings.collaboration_mode.mode == ModeKind::Default {
session.model = settings.model.clone();
session.reasoning_effort = settings.effort;
session.reasoning_effort = settings.effort.clone();
}
session.model_provider_id = settings.model_provider.clone();
session.service_tier = settings.service_tier.clone();
@@ -190,7 +190,7 @@ fn apply_thread_settings_to_session(session: &mut ThreadSessionState, settings:
.settings
.model
.clone_from(&settings.model);
collaboration_mode.settings.reasoning_effort = settings.effort;
collaboration_mode.settings.reasoning_effort = settings.effort.clone();
session.collaboration_mode = Some(Box::new(collaboration_mode));
}