mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -1273,7 +1273,8 @@ fn config_request_overrides_from_config(
|
||||
"model_reasoning_effort",
|
||||
config
|
||||
.model_reasoning_effort
|
||||
.map(|effort| effort.to_string()),
|
||||
.as_ref()
|
||||
.map(std::string::ToString::to_string),
|
||||
);
|
||||
insert(
|
||||
"model_reasoning_summary",
|
||||
@@ -1586,7 +1587,7 @@ async fn thread_session_state_from_thread_start_response(
|
||||
response.cwd.clone(),
|
||||
response.runtime_workspace_roots.clone(),
|
||||
response.instruction_sources.clone(),
|
||||
response.reasoning_effort,
|
||||
response.reasoning_effort.clone(),
|
||||
config,
|
||||
)
|
||||
.await
|
||||
@@ -1627,7 +1628,7 @@ async fn thread_session_state_from_thread_resume_response(
|
||||
response.cwd.clone(),
|
||||
response.runtime_workspace_roots.clone(),
|
||||
response.instruction_sources.clone(),
|
||||
response.reasoning_effort,
|
||||
response.reasoning_effort.clone(),
|
||||
config,
|
||||
)
|
||||
.await
|
||||
@@ -1659,7 +1660,7 @@ async fn thread_session_state_from_thread_fork_response(
|
||||
response.cwd.clone(),
|
||||
response.runtime_workspace_roots.clone(),
|
||||
response.instruction_sources.clone(),
|
||||
response.reasoning_effort,
|
||||
response.reasoning_effort.clone(),
|
||||
config,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -166,7 +166,7 @@ impl ChatWidget {
|
||||
mut collaboration_mode: CollaborationModeMask,
|
||||
) {
|
||||
if collaboration_mode.mode == Some(ModeKind::Plan)
|
||||
&& let Some(effort) = self.config.plan_mode_reasoning_effort
|
||||
&& let Some(effort) = self.config.plan_mode_reasoning_effort.clone()
|
||||
{
|
||||
collaboration_mode.reasoning_effort = Some(Some(effort));
|
||||
}
|
||||
|
||||
@@ -100,11 +100,11 @@ impl ChatWidget {
|
||||
let model = preset.model.clone();
|
||||
let should_prompt_plan_mode_scope = self.should_prompt_plan_mode_reasoning_scope(
|
||||
model.as_str(),
|
||||
Some(preset.default_reasoning_effort),
|
||||
Some(preset.default_reasoning_effort.clone()),
|
||||
);
|
||||
let actions = Self::model_selection_actions(
|
||||
model.clone(),
|
||||
Some(preset.default_reasoning_effort),
|
||||
Some(preset.default_reasoning_effort.clone()),
|
||||
should_prompt_plan_mode_scope,
|
||||
);
|
||||
SelectionItem {
|
||||
@@ -222,16 +222,16 @@ impl ChatWidget {
|
||||
if should_prompt_plan_mode_scope {
|
||||
tx.send(AppEvent::OpenPlanReasoningScopePrompt {
|
||||
model: model_for_action.clone(),
|
||||
effort: effort_for_action,
|
||||
effort: effort_for_action.clone(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
tx.send(AppEvent::UpdateModel(model_for_action.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(effort_for_action));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(effort_for_action.clone()));
|
||||
tx.send(AppEvent::PersistModelSelection {
|
||||
model: model_for_action.clone(),
|
||||
effort: effort_for_action,
|
||||
effort: effort_for_action.clone(),
|
||||
});
|
||||
})]
|
||||
}
|
||||
@@ -261,30 +261,34 @@ impl ChatWidget {
|
||||
model: String,
|
||||
effort: Option<ReasoningEffortConfig>,
|
||||
) {
|
||||
let reasoning_phrase = match effort {
|
||||
let reasoning_phrase = match effort.as_ref() {
|
||||
Some(ReasoningEffortConfig::None) => "no reasoning".to_string(),
|
||||
Some(selected_effort) => {
|
||||
format!(
|
||||
"{} reasoning",
|
||||
Self::reasoning_effort_label(selected_effort).to_lowercase()
|
||||
Self::reasoning_effort_sentence_label(selected_effort)
|
||||
)
|
||||
}
|
||||
None => "the selected reasoning".to_string(),
|
||||
};
|
||||
let plan_only_description = format!("Always use {reasoning_phrase} in Plan mode.");
|
||||
let plan_reasoning_source = if let Some(plan_override) =
|
||||
self.config.plan_mode_reasoning_effort
|
||||
self.config.plan_mode_reasoning_effort.as_ref()
|
||||
{
|
||||
format!(
|
||||
"user-chosen Plan override ({})",
|
||||
Self::reasoning_effort_label(plan_override).to_lowercase()
|
||||
Self::reasoning_effort_sentence_label(plan_override)
|
||||
)
|
||||
} else if let Some(plan_mask) = collaboration_modes::plan_mask(self.model_catalog.as_ref())
|
||||
{
|
||||
match plan_mask.reasoning_effort.flatten() {
|
||||
match plan_mask
|
||||
.reasoning_effort
|
||||
.as_ref()
|
||||
.and_then(|effort| effort.as_ref())
|
||||
{
|
||||
Some(plan_effort) => format!(
|
||||
"built-in Plan default ({})",
|
||||
Self::reasoning_effort_label(plan_effort).to_lowercase()
|
||||
Self::reasoning_effort_sentence_label(plan_effort)
|
||||
),
|
||||
None => "built-in Plan default (no reasoning)".to_string(),
|
||||
}
|
||||
@@ -298,20 +302,21 @@ impl ChatWidget {
|
||||
|
||||
let plan_only_actions: Vec<SelectionAction> = vec![Box::new({
|
||||
let model = model.clone();
|
||||
let effort = effort.clone();
|
||||
move |tx| {
|
||||
tx.send(AppEvent::UpdateModel(model.clone()));
|
||||
tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort));
|
||||
tx.send(AppEvent::PersistPlanModeReasoningEffort(effort));
|
||||
tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort.clone()));
|
||||
tx.send(AppEvent::PersistPlanModeReasoningEffort(effort.clone()));
|
||||
}
|
||||
})];
|
||||
let all_modes_actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::UpdateModel(model.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(effort));
|
||||
tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort));
|
||||
tx.send(AppEvent::PersistPlanModeReasoningEffort(effort));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(effort.clone()));
|
||||
tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort.clone()));
|
||||
tx.send(AppEvent::PersistPlanModeReasoningEffort(effort.clone()));
|
||||
tx.send(AppEvent::PersistModelSelection {
|
||||
model: model.clone(),
|
||||
effort,
|
||||
effort: effort.clone(),
|
||||
});
|
||||
})];
|
||||
|
||||
@@ -344,7 +349,7 @@ impl ChatWidget {
|
||||
|
||||
/// Open a popup to choose the reasoning effort (stage 2) for the given model.
|
||||
pub(crate) fn open_reasoning_popup(&mut self, preset: ModelPreset) {
|
||||
let default_effort: ReasoningEffortConfig = preset.default_reasoning_effort;
|
||||
let default_effort = preset.default_reasoning_effort;
|
||||
let supported = preset.supported_reasoning_efforts;
|
||||
let in_plan_mode =
|
||||
self.collaboration_modes_enabled() && self.active_mode_kind() == ModeKind::Plan;
|
||||
@@ -362,7 +367,7 @@ impl ChatWidget {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let warning_text = warn_effort.map(|effort| {
|
||||
let warning_text = warn_effort.as_ref().map(|effort| {
|
||||
let effort_label = Self::reasoning_effort_label(effort);
|
||||
format!("⚠ {effort_label} reasoning effort can quickly consume Plus plan rate limits.")
|
||||
});
|
||||
@@ -370,30 +375,25 @@ impl ChatWidget {
|
||||
|| preset.model.starts_with("gpt-5.1-codex-max")
|
||||
|| preset.model.starts_with("gpt-5.2");
|
||||
|
||||
struct EffortChoice {
|
||||
stored: Option<ReasoningEffortConfig>,
|
||||
display: ReasoningEffortConfig,
|
||||
}
|
||||
let mut choices: Vec<EffortChoice> = Vec::new();
|
||||
for effort in ReasoningEffortConfig::iter() {
|
||||
if supported.iter().any(|option| option.effort == effort) {
|
||||
choices.push(EffortChoice {
|
||||
stored: Some(effort),
|
||||
display: effort,
|
||||
});
|
||||
}
|
||||
}
|
||||
let mut choices: Vec<ReasoningEffortConfig> = ReasoningEffortConfig::known_values()
|
||||
.filter(|effort| supported.iter().any(|option| option.effort == *effort))
|
||||
.collect();
|
||||
choices.extend(
|
||||
supported
|
||||
.iter()
|
||||
.filter(|option| option.effort.known_rank().is_none())
|
||||
.map(|option| option.effort.clone()),
|
||||
);
|
||||
if choices.is_empty() {
|
||||
choices.push(EffortChoice {
|
||||
stored: Some(default_effort),
|
||||
display: default_effort,
|
||||
});
|
||||
choices.push(default_effort.clone());
|
||||
}
|
||||
|
||||
if choices.len() == 1 {
|
||||
let selected_effort = choices.first().and_then(|c| c.stored);
|
||||
let selected_effort = choices.first().cloned();
|
||||
let selected_model = preset.model;
|
||||
if self.should_prompt_plan_mode_reasoning_scope(&selected_model, selected_effort) {
|
||||
if self
|
||||
.should_prompt_plan_mode_reasoning_scope(&selected_model, selected_effort.clone())
|
||||
{
|
||||
self.app_event_tx
|
||||
.send(AppEvent::OpenPlanReasoningScopePrompt {
|
||||
model: selected_model,
|
||||
@@ -405,12 +405,10 @@ impl ChatWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
let default_choice: Option<ReasoningEffortConfig> = choices
|
||||
.iter()
|
||||
.any(|choice| choice.stored == Some(default_effort))
|
||||
.then_some(Some(default_effort))
|
||||
.flatten()
|
||||
.or_else(|| choices.iter().find_map(|choice| choice.stored))
|
||||
let default_choice = choices
|
||||
.contains(&default_effort)
|
||||
.then(|| default_effort.clone())
|
||||
.or_else(|| choices.first().cloned())
|
||||
.or(Some(default_effort));
|
||||
|
||||
let model_slug = preset.model.to_string();
|
||||
@@ -419,40 +417,33 @@ impl ChatWidget {
|
||||
if in_plan_mode {
|
||||
self.config
|
||||
.plan_mode_reasoning_effort
|
||||
.or(self.effective_reasoning_effort())
|
||||
.clone()
|
||||
.or_else(|| self.effective_reasoning_effort())
|
||||
} else {
|
||||
self.effective_reasoning_effort()
|
||||
}
|
||||
} else {
|
||||
default_choice
|
||||
default_choice.clone()
|
||||
};
|
||||
let selection_choice = highlight_choice.or(default_choice);
|
||||
let selection_choice = highlight_choice.clone().or_else(|| default_choice.clone());
|
||||
let initial_selected_idx = choices
|
||||
.iter()
|
||||
.position(|choice| choice.stored == selection_choice)
|
||||
.or_else(|| {
|
||||
selection_choice
|
||||
.and_then(|effort| choices.iter().position(|choice| choice.display == effort))
|
||||
});
|
||||
.position(|choice| Some(choice) == selection_choice.as_ref());
|
||||
let mut items: Vec<SelectionItem> = Vec::new();
|
||||
for choice in choices.iter() {
|
||||
let effort = choice.display;
|
||||
let mut effort_label = Self::reasoning_effort_label(effort).to_string();
|
||||
if choice.stored == default_choice {
|
||||
let effort = choice.clone();
|
||||
let mut effort_label = Self::reasoning_effort_label(&effort);
|
||||
if Some(choice) == default_choice.as_ref() {
|
||||
effort_label.push_str(" (default)");
|
||||
}
|
||||
|
||||
let description = choice
|
||||
.stored
|
||||
.and_then(|effort| {
|
||||
supported
|
||||
.iter()
|
||||
.find(|option| option.effort == effort)
|
||||
.map(|option| option.description.to_string())
|
||||
})
|
||||
let description = supported
|
||||
.iter()
|
||||
.find(|option| option.effort == effort)
|
||||
.map(|option| option.description.to_string())
|
||||
.filter(|text| !text.is_empty());
|
||||
|
||||
let show_warning = warn_for_model && warn_effort == Some(effort);
|
||||
let show_warning = warn_for_model && warn_effort.as_ref() == Some(&effort);
|
||||
let selected_description = if show_warning {
|
||||
warning_text.as_ref().map(|warning_message| {
|
||||
description.as_ref().map_or_else(
|
||||
@@ -465,21 +456,23 @@ impl ChatWidget {
|
||||
};
|
||||
|
||||
let model_for_action = model_slug.clone();
|
||||
let choice_effort = choice.stored;
|
||||
let should_prompt_plan_mode_scope =
|
||||
self.should_prompt_plan_mode_reasoning_scope(model_slug.as_str(), choice_effort);
|
||||
let choice_effort = Some(effort);
|
||||
let should_prompt_plan_mode_scope = self.should_prompt_plan_mode_reasoning_scope(
|
||||
model_slug.as_str(),
|
||||
choice_effort.clone(),
|
||||
);
|
||||
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||
if should_prompt_plan_mode_scope {
|
||||
tx.send(AppEvent::OpenPlanReasoningScopePrompt {
|
||||
model: model_for_action.clone(),
|
||||
effort: choice_effort,
|
||||
effort: choice_effort.clone(),
|
||||
});
|
||||
} else {
|
||||
tx.send(AppEvent::UpdateModel(model_for_action.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(choice_effort));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(choice_effort.clone()));
|
||||
tx.send(AppEvent::PersistModelSelection {
|
||||
model: model_for_action.clone(),
|
||||
effort: choice_effort,
|
||||
effort: choice_effort.clone(),
|
||||
});
|
||||
}
|
||||
})];
|
||||
@@ -488,7 +481,7 @@ impl ChatWidget {
|
||||
name: effort_label,
|
||||
description,
|
||||
selected_description,
|
||||
is_current: is_current_model && choice.stored == highlight_choice,
|
||||
is_current: is_current_model && Some(choice) == highlight_choice.as_ref(),
|
||||
actions,
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
@@ -509,14 +502,22 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn reasoning_effort_label(effort: ReasoningEffortConfig) -> &'static str {
|
||||
pub(super) fn reasoning_effort_label(effort: &ReasoningEffortConfig) -> String {
|
||||
match effort {
|
||||
ReasoningEffortConfig::None => "None",
|
||||
ReasoningEffortConfig::Minimal => "Minimal",
|
||||
ReasoningEffortConfig::Low => "Low",
|
||||
ReasoningEffortConfig::Medium => "Medium",
|
||||
ReasoningEffortConfig::High => "High",
|
||||
ReasoningEffortConfig::XHigh => "Extra high",
|
||||
ReasoningEffortConfig::None => "None".to_string(),
|
||||
ReasoningEffortConfig::Minimal => "Minimal".to_string(),
|
||||
ReasoningEffortConfig::Low => "Low".to_string(),
|
||||
ReasoningEffortConfig::Medium => "Medium".to_string(),
|
||||
ReasoningEffortConfig::High => "High".to_string(),
|
||||
ReasoningEffortConfig::XHigh => "Extra high".to_string(),
|
||||
ReasoningEffortConfig::Custom(value) => value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn reasoning_effort_sentence_label(effort: &ReasoningEffortConfig) -> String {
|
||||
match effort {
|
||||
ReasoningEffortConfig::Custom(value) => value.clone(),
|
||||
effort => Self::reasoning_effort_label(effort).to_lowercase(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,7 +532,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn apply_model_and_effort(&self, model: String, effort: Option<ReasoningEffortConfig>) {
|
||||
self.apply_model_and_effort_without_persist(model.clone(), effort);
|
||||
self.apply_model_and_effort_without_persist(model.clone(), effort.clone());
|
||||
self.app_event_tx
|
||||
.send(AppEvent::PersistModelSelection { model, effort });
|
||||
}
|
||||
|
||||
@@ -343,14 +343,16 @@ impl ChatWidget {
|
||||
/*active_permission_profile*/ None,
|
||||
/*windows_sandbox_level*/ None,
|
||||
Some(switch_model_for_events.clone()),
|
||||
Some(Some(default_effort)),
|
||||
Some(Some(default_effort.clone())),
|
||||
/*summary*/ None,
|
||||
/*service_tier*/ None,
|
||||
/*collaboration_mode*/ None,
|
||||
/*personality*/ None,
|
||||
)));
|
||||
tx.send(AppEvent::UpdateModel(switch_model_for_events.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(Some(default_effort)));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(Some(
|
||||
default_effort.clone(),
|
||||
)));
|
||||
})];
|
||||
|
||||
let keep_actions: Vec<SelectionAction> = Vec::new();
|
||||
|
||||
@@ -8,15 +8,14 @@
|
||||
//! The shortcut state machine is deliberately narrow: it only handles key
|
||||
//! presses when no modal or popup owns input, it anchors unset reasoning to the
|
||||
//! current model preset's default, and it walks only efforts advertised by the
|
||||
//! active model. Unsupported current efforts are not normalized eagerly; the
|
||||
//! next shortcut moves to the nearest supported effort in the requested
|
||||
//! direction.
|
||||
//! active model. Unsupported known efforts move to the nearest advertised known
|
||||
//! effort in the requested direction. Unknown efforts anchor to the model
|
||||
//! default before stepping through the advertised order.
|
||||
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use crossterm::event::KeyEvent;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use super::ChatWidget;
|
||||
use crate::app_event::AppEvent;
|
||||
@@ -30,8 +29,8 @@ pub(super) enum ReasoningShortcutDirection {
|
||||
}
|
||||
|
||||
impl ReasoningShortcutDirection {
|
||||
fn bound_message(self, effort: ReasoningEffortConfig) -> String {
|
||||
let label = ChatWidget::reasoning_effort_label(effort).to_lowercase();
|
||||
fn bound_message(self, effort: &ReasoningEffortConfig) -> String {
|
||||
let label = ChatWidget::reasoning_effort_sentence_label(effort);
|
||||
match self {
|
||||
Self::Lower => format!("Reasoning is already at the lowest level ({label})."),
|
||||
Self::Raise => format!("Reasoning is already at the highest level ({label})."),
|
||||
@@ -90,12 +89,23 @@ impl ChatWidget {
|
||||
};
|
||||
|
||||
let choices = reasoning_choices(&preset);
|
||||
let current_effort = self
|
||||
let configured_effort = self
|
||||
.effective_reasoning_effort()
|
||||
.unwrap_or(preset.default_reasoning_effort);
|
||||
let Some(next_effort) = next_reasoning_effort(&choices, Some(current_effort), direction)
|
||||
.unwrap_or_else(|| preset.default_reasoning_effort.clone());
|
||||
let current_effort = if choices.contains(&configured_effort) {
|
||||
configured_effort
|
||||
} else if choices.contains(&preset.default_reasoning_effort) {
|
||||
preset.default_reasoning_effort
|
||||
} else {
|
||||
choices
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or(preset.default_reasoning_effort)
|
||||
};
|
||||
let Some(next_effort) =
|
||||
next_reasoning_effort(&choices, Some(current_effort.clone()), direction)
|
||||
else {
|
||||
self.add_info_message(direction.bound_message(current_effort), /*hint*/ None);
|
||||
self.add_info_message(direction.bound_message(¤t_effort), /*hint*/ None);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -120,18 +130,23 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn reasoning_choices(preset: &ModelPreset) -> Vec<ReasoningEffortConfig> {
|
||||
let mut choices = Vec::new();
|
||||
for effort in ReasoningEffortConfig::iter() {
|
||||
if preset
|
||||
let mut choices: Vec<ReasoningEffortConfig> = ReasoningEffortConfig::known_values()
|
||||
.filter(|effort| {
|
||||
preset
|
||||
.supported_reasoning_efforts
|
||||
.iter()
|
||||
.any(|option| option.effort == *effort)
|
||||
})
|
||||
.collect();
|
||||
choices.extend(
|
||||
preset
|
||||
.supported_reasoning_efforts
|
||||
.iter()
|
||||
.any(|option| option.effort == effort)
|
||||
{
|
||||
choices.push(effort);
|
||||
}
|
||||
}
|
||||
.filter(|option| option.effort.known_rank().is_none())
|
||||
.map(|option| option.effort.clone()),
|
||||
);
|
||||
if choices.is_empty() {
|
||||
choices.push(preset.default_reasoning_effort);
|
||||
choices.push(preset.default_reasoning_effort.clone());
|
||||
}
|
||||
choices
|
||||
}
|
||||
@@ -142,32 +157,51 @@ fn next_reasoning_effort(
|
||||
direction: ReasoningShortcutDirection,
|
||||
) -> Option<ReasoningEffortConfig> {
|
||||
let current_effort = current_effort?;
|
||||
if choices.is_empty() {
|
||||
return None;
|
||||
if let Some(current_index) = choices.iter().position(|choice| choice == ¤t_effort) {
|
||||
return match direction {
|
||||
ReasoningShortcutDirection::Lower => current_index
|
||||
.checked_sub(1)
|
||||
.and_then(|index| choices.get(index))
|
||||
.cloned(),
|
||||
ReasoningShortcutDirection::Raise => choices.get(current_index + 1).cloned(),
|
||||
};
|
||||
}
|
||||
|
||||
let current_rank = effort_rank(current_effort);
|
||||
match direction {
|
||||
let current_rank = current_effort.known_rank()?;
|
||||
let ranked_choice = match direction {
|
||||
ReasoningShortcutDirection::Lower => choices
|
||||
.iter()
|
||||
.rev()
|
||||
.copied()
|
||||
.find(|choice| effort_rank(*choice) < current_rank),
|
||||
.filter_map(|choice| choice.known_rank().map(|rank| (rank, choice)))
|
||||
.filter(|(rank, _)| *rank < current_rank)
|
||||
.max_by_key(|(rank, _)| *rank)
|
||||
.map(|(_, choice)| choice.clone()),
|
||||
ReasoningShortcutDirection::Raise => choices
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|choice| effort_rank(*choice) > current_rank),
|
||||
.filter_map(|choice| choice.known_rank().map(|rank| (rank, choice)))
|
||||
.filter(|(rank, _)| *rank > current_rank)
|
||||
.min_by_key(|(rank, _)| *rank)
|
||||
.map(|(_, choice)| choice.clone()),
|
||||
};
|
||||
if let Some(ranked_choice) = ranked_choice {
|
||||
return Some(ranked_choice);
|
||||
}
|
||||
}
|
||||
|
||||
fn effort_rank(effort: ReasoningEffortConfig) -> i32 {
|
||||
match effort {
|
||||
ReasoningEffortConfig::None => 0,
|
||||
ReasoningEffortConfig::Minimal => 1,
|
||||
ReasoningEffortConfig::Low => 2,
|
||||
ReasoningEffortConfig::Medium => 3,
|
||||
ReasoningEffortConfig::High => 4,
|
||||
ReasoningEffortConfig::XHigh => 5,
|
||||
let nearest_known_index = choices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, choice)| {
|
||||
choice
|
||||
.known_rank()
|
||||
.map(|rank| (rank.abs_diff(current_rank), index))
|
||||
})
|
||||
.min_by_key(|(distance, _)| *distance)
|
||||
.map(|(_, index)| index)?;
|
||||
match direction {
|
||||
ReasoningShortcutDirection::Lower => nearest_known_index
|
||||
.checked_sub(1)
|
||||
.and_then(|index| choices.get(index))
|
||||
.cloned(),
|
||||
ReasoningShortcutDirection::Raise => choices.get(nearest_known_index + 1).cloned(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,6 +269,21 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_reasoning_effort_reaches_custom_level_from_nearest_known_anchor() {
|
||||
let custom_effort = ReasoningEffortConfig::Custom("max".to_string());
|
||||
let choices = vec![ReasoningEffortConfig::Medium, custom_effort.clone()];
|
||||
|
||||
assert_eq!(
|
||||
next_reasoning_effort(
|
||||
&choices,
|
||||
Some(ReasoningEffortConfig::High),
|
||||
ReasoningShortcutDirection::Raise,
|
||||
),
|
||||
Some(custom_effort)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_reasoning_effort_clamps_at_bounds() {
|
||||
let choices = vec![
|
||||
|
||||
@@ -79,7 +79,7 @@ impl ChatWidget {
|
||||
let default_model = session.model.clone();
|
||||
self.current_collaboration_mode = self.current_collaboration_mode.with_updates(
|
||||
Some(default_model.clone()),
|
||||
Some(session.reasoning_effort),
|
||||
Some(session.reasoning_effort.clone()),
|
||||
/*developer_instructions*/ None,
|
||||
);
|
||||
match session.collaboration_mode.as_deref() {
|
||||
@@ -93,7 +93,7 @@ impl ChatWidget {
|
||||
Some(&default_model),
|
||||
);
|
||||
if let Some(mask) = self.active_collaboration_mask.as_mut() {
|
||||
mask.reasoning_effort = Some(session.reasoning_effort);
|
||||
mask.reasoning_effort = Some(session.reasoning_effort.clone());
|
||||
}
|
||||
self.update_collaboration_mode_indicator();
|
||||
self.refresh_plan_mode_nudge();
|
||||
|
||||
@@ -159,7 +159,7 @@ impl ChatWidget {
|
||||
/// so the footer reflects it without waiting for the next mode switch.
|
||||
/// Passing `None` resets to the Plan-mode preset default.
|
||||
pub(crate) fn set_plan_mode_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) {
|
||||
self.config.plan_mode_reasoning_effort = effort;
|
||||
self.config.plan_mode_reasoning_effort = effort.clone();
|
||||
if self.collaboration_modes_enabled()
|
||||
&& let Some(mask) = self.active_collaboration_mask.as_mut()
|
||||
&& mask.mode == Some(ModeKind::Plan)
|
||||
@@ -182,7 +182,7 @@ impl ChatWidget {
|
||||
pub(crate) fn set_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) {
|
||||
self.current_collaboration_mode = self.current_collaboration_mode.with_updates(
|
||||
/*model*/ None,
|
||||
Some(effort),
|
||||
Some(effort.clone()),
|
||||
/*developer_instructions*/ None,
|
||||
);
|
||||
if self.collaboration_modes_enabled()
|
||||
@@ -471,7 +471,7 @@ impl ChatWidget {
|
||||
let current_effort = self.current_collaboration_mode.reasoning_effort();
|
||||
self.active_collaboration_mask
|
||||
.as_ref()
|
||||
.and_then(|mask| mask.reasoning_effort)
|
||||
.and_then(|mask| mask.reasoning_effort.clone())
|
||||
.unwrap_or(current_effort)
|
||||
}
|
||||
|
||||
@@ -590,7 +590,7 @@ impl ChatWidget {
|
||||
name: mode_kind.display_name().to_string(),
|
||||
mode: Some(mode_kind),
|
||||
model: Some(settings.model.clone()),
|
||||
reasoning_effort: Some(settings.reasoning_effort),
|
||||
reasoning_effort: Some(settings.reasoning_effort.clone()),
|
||||
developer_instructions: Some(settings.developer_instructions),
|
||||
});
|
||||
self.update_collaboration_mode_indicator();
|
||||
@@ -712,7 +712,7 @@ impl ChatWidget {
|
||||
let previous_model = self.current_model().to_string();
|
||||
let previous_effort = self.effective_reasoning_effort();
|
||||
if mask.mode == Some(ModeKind::Plan)
|
||||
&& let Some(effort) = self.config.plan_mode_reasoning_effort
|
||||
&& let Some(effort) = self.config.plan_mode_reasoning_effort.clone()
|
||||
{
|
||||
mask.reasoning_effort = Some(Some(effort));
|
||||
}
|
||||
@@ -732,13 +732,9 @@ impl ChatWidget {
|
||||
{
|
||||
let mut message = format!("Model changed to {next_model}");
|
||||
if !next_model.starts_with("codex-auto-") {
|
||||
let reasoning_label = match next_effort {
|
||||
Some(ReasoningEffortConfig::Minimal) => "minimal",
|
||||
Some(ReasoningEffortConfig::Low) => "low",
|
||||
Some(ReasoningEffortConfig::Medium) => "medium",
|
||||
Some(ReasoningEffortConfig::High) => "high",
|
||||
Some(ReasoningEffortConfig::XHigh) => "xhigh",
|
||||
let reasoning_label = match next_effort.as_ref() {
|
||||
None | Some(ReasoningEffortConfig::None) => "default",
|
||||
Some(effort) => effort.as_str(),
|
||||
};
|
||||
message.push(' ');
|
||||
message.push_str(reasoning_label);
|
||||
|
||||
+1
@@ -8,5 +8,6 @@ expression: popup
|
||||
2. Medium (default) Balances speed and reasoning depth for everyday tasks
|
||||
› 3. High (current) Greater reasoning depth for complex problems
|
||||
4. Extra high Extra high reasoning depth for complex problems
|
||||
5. max Maximum available reasoning
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
|
||||
@@ -212,7 +212,7 @@ impl ChatWidget {
|
||||
});
|
||||
let reasoning_effort_override = Some(
|
||||
self.effective_reasoning_effort()
|
||||
.or(self.config.model_reasoning_effort)
|
||||
.or_else(|| self.config.model_reasoning_effort.clone())
|
||||
.or(model_default_reasoning_effort),
|
||||
);
|
||||
let rate_limit_snapshots: Vec<RateLimitSnapshotDisplay> = self
|
||||
@@ -382,15 +382,11 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
pub(super) fn status_line_reasoning_effort_label(
|
||||
effort: Option<ReasoningEffortConfig>,
|
||||
) -> &'static str {
|
||||
effort: Option<&ReasoningEffortConfig>,
|
||||
) -> String {
|
||||
match 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(effort) => effort.as_str().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,7 +562,7 @@ impl ChatWidget {
|
||||
match item {
|
||||
StatusLineItem::ModelName => Some(self.model_display_name().to_string()),
|
||||
StatusLineItem::ModelWithReasoning => Some(self.model_with_reasoning_display_name()),
|
||||
StatusLineItem::Reasoning => Some(self.reasoning_display_name().to_string()),
|
||||
StatusLineItem::Reasoning => Some(self.reasoning_display_name()),
|
||||
StatusLineItem::CurrentDir => {
|
||||
Some(format_directory_display(
|
||||
self.status_line_cwd(),
|
||||
@@ -762,15 +762,16 @@ impl ChatWidget {
|
||||
/*max_chars*/ 32,
|
||||
)),
|
||||
TerminalTitleItem::Reasoning => Some(Self::truncate_terminal_title_part(
|
||||
self.reasoning_display_name().to_string(),
|
||||
self.reasoning_display_name(),
|
||||
/*max_chars*/ 32,
|
||||
)),
|
||||
TerminalTitleItem::TaskProgress => self.terminal_title_task_progress(),
|
||||
}
|
||||
}
|
||||
|
||||
fn reasoning_display_name(&self) -> &'static str {
|
||||
Self::status_line_reasoning_effort_label(self.effective_reasoning_effort())
|
||||
fn reasoning_display_name(&self) -> String {
|
||||
let effort = self.effective_reasoning_effort();
|
||||
Self::status_line_reasoning_effort_label(effort.as_ref())
|
||||
}
|
||||
|
||||
fn model_with_reasoning_display_name(&self) -> String {
|
||||
|
||||
@@ -2405,13 +2405,54 @@ async fn model_reasoning_selection_popup_snapshot() {
|
||||
set_chatgpt_auth(&mut chat);
|
||||
chat.set_reasoning_effort(Some(ReasoningEffortConfig::High));
|
||||
|
||||
let preset = get_available_model(&chat, "gpt-5.4");
|
||||
let mut preset = get_available_model(&chat, "gpt-5.4");
|
||||
preset
|
||||
.supported_reasoning_efforts
|
||||
.push(ReasoningEffortPreset {
|
||||
effort: ReasoningEffortConfig::Custom("max".to_string()),
|
||||
description: "Maximum available reasoning".to_string(),
|
||||
});
|
||||
chat.open_reasoning_popup(preset);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert_chatwidget_snapshot!("model_reasoning_selection_popup", popup);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn model_reasoning_selection_popup_applies_custom_effort() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
let custom_effort = ReasoningEffortConfig::Custom("max".to_string());
|
||||
chat.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh));
|
||||
|
||||
let mut preset = get_available_model(&chat, "gpt-5.4");
|
||||
preset
|
||||
.supported_reasoning_efforts
|
||||
.push(ReasoningEffortPreset {
|
||||
effort: custom_effort.clone(),
|
||||
description: "Maximum available reasoning".to_string(),
|
||||
});
|
||||
chat.open_reasoning_popup(preset);
|
||||
while rx.try_recv().is_ok() {}
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
let selected_effort_events = std::iter::from_fn(|| rx.try_recv().ok())
|
||||
.filter_map(|event| match event {
|
||||
AppEvent::UpdateReasoningEffort(effort) => Some((None, effort)),
|
||||
AppEvent::PersistModelSelection { model, effort } => Some((Some(model), effort)),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
selected_effort_events,
|
||||
vec![
|
||||
(None, Some(custom_effort.clone())),
|
||||
(Some("gpt-5.4".to_string()), Some(custom_effort)),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn model_reasoning_selection_popup_extra_high_warning_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
|
||||
|
||||
@@ -149,7 +149,7 @@ pub(crate) fn new_session_info(
|
||||
// Header box rendered as history (so it appears at the very top)
|
||||
let header = SessionHeaderHistoryCell::new(
|
||||
session.model.clone(),
|
||||
session.reasoning_effort,
|
||||
session.reasoning_effort.clone(),
|
||||
show_fast_status,
|
||||
config.cwd.to_path_buf(),
|
||||
CODEX_CLI_VERSION,
|
||||
@@ -317,15 +317,10 @@ impl SessionHeaderHistoryCell {
|
||||
formatted
|
||||
}
|
||||
|
||||
fn reasoning_label(&self) -> Option<&'static str> {
|
||||
self.reasoning_effort.map(|effort| match effort {
|
||||
ReasoningEffortConfig::Minimal => "minimal",
|
||||
ReasoningEffortConfig::Low => "low",
|
||||
ReasoningEffortConfig::Medium => "medium",
|
||||
ReasoningEffortConfig::High => "high",
|
||||
ReasoningEffortConfig::XHigh => "xhigh",
|
||||
ReasoningEffortConfig::None => "none",
|
||||
})
|
||||
fn reasoning_label(&self) -> Option<&str> {
|
||||
self.reasoning_effort
|
||||
.as_ref()
|
||||
.map(ReasoningEffortConfig::as_str)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +363,7 @@ impl HistoryCell for SessionHeaderHistoryCell {
|
||||
];
|
||||
if let Some(reasoning) = reasoning_label {
|
||||
spans.push(Span::from(" "));
|
||||
spans.push(Span::from(reasoning));
|
||||
spans.push(Span::from(reasoning.to_owned()));
|
||||
}
|
||||
if self.show_fast_status {
|
||||
spans.push(" ".into());
|
||||
|
||||
@@ -182,7 +182,7 @@ pub(crate) fn spawn_request_summary(item: &ThreadItem) -> Option<SpawnRequestSum
|
||||
..
|
||||
} => Some(SpawnRequestSummary {
|
||||
model: model.clone(),
|
||||
reasoning_effort: *reasoning_effort,
|
||||
reasoning_effort: reasoning_effort.clone(),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ impl StatusHistoryCell {
|
||||
];
|
||||
if config.model_provider.wire_api == WireApi::Responses {
|
||||
let effort_value = reasoning_effort_override
|
||||
.unwrap_or(config.model_reasoning_effort)
|
||||
.unwrap_or_else(|| config.model_reasoning_effort.clone())
|
||||
.map(|effort| effort.to_string())
|
||||
.unwrap_or_else(|| "none".to_string());
|
||||
config_entries.push(("reasoning effort", effort_value));
|
||||
|
||||
Reference in New Issue
Block a user