[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
2026-06-04 13:36:24 -07:00
committed by GitHub
parent 4a70e0ac1b
commit 8ac304c299
97 changed files with 685 additions and 581 deletions
+2 -9
View File
@@ -1971,15 +1971,8 @@
"type": "string"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningItemContent": {
@@ -2777,15 +2777,8 @@
"type": "string"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningSummary": {
@@ -13525,15 +13525,8 @@
"type": "object"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningEffortOption": {
@@ -10047,15 +10047,8 @@
"type": "object"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningEffortOption": {
@@ -667,15 +667,8 @@
"type": "string"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningSummary": {
@@ -462,15 +462,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"TextElement": {
@@ -462,15 +462,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"TextElement": {
@@ -185,15 +185,8 @@
"type": "object"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningEffortOption": {
@@ -606,15 +606,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"TextElement": {
@@ -718,15 +718,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SandboxPolicy": {
@@ -632,15 +632,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SessionSource": {
@@ -632,15 +632,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SessionSource": {
@@ -632,15 +632,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SessionSource": {
@@ -718,15 +718,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SandboxPolicy": {
@@ -632,15 +632,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SessionSource": {
@@ -124,15 +124,8 @@
"type": "string"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningSummary": {
@@ -718,15 +718,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SandboxPolicy": {
@@ -632,15 +632,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SessionSource": {
@@ -632,15 +632,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SessionSource": {
@@ -606,15 +606,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"TextElement": {
@@ -154,15 +154,8 @@
"type": "string"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningSummary": {
@@ -606,15 +606,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"TextElement": {
@@ -606,15 +606,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"TextElement": {
@@ -5,4 +5,4 @@
/**
* See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
*/
export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
export type ReasoningEffort = string;
@@ -621,7 +621,7 @@ impl ThreadHistoryBuilder {
receiver_thread_ids: Vec::new(),
prompt: Some(payload.prompt.clone()),
model: Some(payload.model.clone()),
reasoning_effort: Some(payload.reasoning_effort),
reasoning_effort: Some(payload.reasoning_effort.clone()),
agents_states: HashMap::new(),
};
self.upsert_item_in_current_turn(item);
@@ -656,7 +656,7 @@ impl ThreadHistoryBuilder {
receiver_thread_ids,
prompt: Some(payload.prompt.clone()),
model: Some(payload.model.clone()),
reasoning_effort: Some(payload.reasoning_effort),
reasoning_effort: Some(payload.reasoning_effort.clone()),
agents_states,
});
}
+1 -1
View File
@@ -188,7 +188,7 @@ Example with notification opt-out:
- `fs/watch` — subscribe this connection to filesystem change notifications for an absolute file or directory path and caller-provided `watchId`; returns the canonicalized `path`.
- `fs/unwatch` — stop sending notifications for a prior `fs/watch`; returns `{}`.
- `fs/changed` — notification emitted when watched paths change, including the `watchId` and `changedPaths`.
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, `serviceTiers`, optional `defaultServiceTier`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with model-advertised string reasoning effort options, `additionalSpeedTiers`, `serviceTiers`, optional `defaultServiceTier`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
- `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider.
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. Pass `threadId` when showing feature state for an existing loaded thread so `enabled` is computed from that thread's refreshed config, including project-local config for the thread's cwd; if omitted, the server uses its default config resolution context. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
- `permissionProfile/list` — beta; list available permission profile ids with optional display `description` text, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.<id>]` entries to be included in the current catalog view.
+2 -2
View File
@@ -62,10 +62,10 @@ fn reasoning_efforts_from_preset(
efforts: Vec<ReasoningEffortPreset>,
) -> Vec<ReasoningEffortOption> {
efforts
.iter()
.into_iter()
.map(|preset| ReasoningEffortOption {
reasoning_effort: preset.effort,
description: preset.description.to_string(),
description: preset.description,
})
.collect()
}
@@ -158,7 +158,7 @@ fn merge_persisted_resume_metadata(
typesafe_overrides.model = persisted_metadata.model.clone();
typesafe_overrides.model_provider = Some(persisted_metadata.model_provider.clone());
if let Some(reasoning_effort) = persisted_metadata.reasoning_effort {
if let Some(reasoning_effort) = persisted_metadata.reasoning_effort.as_ref() {
request_overrides.get_or_insert_with(HashMap::new).insert(
"model_reasoning_effort".to_string(),
serde_json::Value::String(reasoning_effort.to_string()),
@@ -203,7 +203,7 @@ pub(crate) fn thread_settings_from_config_snapshot(
model: config_snapshot.model.clone(),
model_provider: config_snapshot.model_provider_id.clone(),
service_tier: config_snapshot.service_tier.clone(),
effort: config_snapshot.reasoning_effort,
effort: config_snapshot.reasoning_effort.clone(),
summary: config_snapshot.reasoning_summary,
collaboration_mode: config_snapshot.collaboration_mode.clone(),
personality: config_snapshot.personality,
@@ -629,7 +629,7 @@ impl TurnRequestProcessor {
profile_workspace_roots: profile_workspace_roots.clone(),
windows_sandbox_level: None,
model: model.clone(),
effort,
effort: effort.clone(),
summary,
service_tier: service_tier.clone(),
collaboration_mode: collaboration_mode.clone(),
@@ -18,7 +18,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
slug: preset.id.clone(),
display_name: preset.display_name.clone(),
description: Some(preset.description.clone()),
default_reasoning_level: Some(preset.default_reasoning_effort),
default_reasoning_level: Some(preset.default_reasoning_effort.clone()),
supported_reasoning_levels: preset.supported_reasoning_efforts.clone(),
shell_type: ConfigShellToolType::ShellCommand,
visibility: if preset.show_in_picker {
@@ -48,11 +48,11 @@ fn model_from_preset(preset: &ModelPreset) -> Model {
.supported_reasoning_efforts
.iter()
.map(|preset| ReasoningEffortOption {
reasoning_effort: preset.effort,
reasoning_effort: preset.effort.clone(),
description: preset.description.clone(),
})
.collect(),
default_reasoning_effort: preset.default_reasoning_effort,
default_reasoning_effort: preset.default_reasoning_effort.clone(),
input_modalities: preset.input_modalities.clone(),
// `write_models_cache()` round-trips through a simplified ModelInfo fixture that does not
// preserve personality placeholders in base instructions, so app-server list results from
@@ -164,10 +164,10 @@ async fn list_models_uses_chatgpt_remote_catalog_as_source_of_truth() -> Result<
"slug": "chatgpt-remote-only",
"display_name": "ChatGPT Remote Only",
"description": "Remote-only model for app-server model/list coverage",
"default_reasoning_level": "medium",
"default_reasoning_level": "max",
"supported_reasoning_levels": [
{"effort": "low", "description": "low"},
{"effort": "medium", "description": "medium"}
{"effort": "max", "description": "Maximum"}
],
"shell_type": "shell_command",
"visibility": "list",
+2 -9
View File
@@ -2503,15 +2503,8 @@
"type": "string"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningSummary": {
+4 -4
View File
@@ -721,7 +721,7 @@ impl ModelClient {
) -> Option<Reasoning> {
if model_info.supports_reasoning_summaries {
Some(Reasoning {
effort: effort.or(model_info.default_reasoning_level),
effort: effort.or_else(|| model_info.default_reasoning_level.clone()),
summary: if summary == ReasoningSummaryConfig::None {
None
} else {
@@ -1267,7 +1267,7 @@ impl ModelClientSession {
&client_setup.api_provider,
prompt,
model_info,
effort,
effort.clone(),
summary,
service_tier.clone(),
)?;
@@ -1376,7 +1376,7 @@ impl ModelClientSession {
&client_setup.api_provider,
prompt,
model_info,
effort,
effort.clone(),
summary,
service_tier.clone(),
)?;
@@ -1595,7 +1595,7 @@ impl ModelClientSession {
prompt,
model_info,
session_telemetry,
effort,
effort.clone(),
summary,
service_tier.clone(),
turn_metadata_header,
+1 -1
View File
@@ -573,7 +573,7 @@ async fn drain_to_completed(
prompt,
&turn_context.model_info,
&turn_context.session_telemetry,
turn_context.reasoning_effort,
turn_context.reasoning_effort.clone(),
turn_context.reasoning_summary,
turn_context.config.service_tier.clone(),
turn_metadata_header,
+1 -1
View File
@@ -242,7 +242,7 @@ async fn run_remote_compact_task_inner_impl(
&prompt,
&turn_context.model_info,
CompactConversationRequestSettings {
effort: turn_context.reasoning_effort,
effort: turn_context.reasoning_effort.clone(),
summary: turn_context.reasoning_summary,
service_tier: if sess.services.auth_manager.auth_mode() == Some(AuthMode::ApiKey) {
None
+1 -1
View File
@@ -339,7 +339,7 @@ async fn run_remote_compaction_request_v2(
prompt,
&turn_context.model_info,
&turn_context.session_telemetry,
turn_context.reasoning_effort,
turn_context.reasoning_effort.clone(),
turn_context.reasoning_summary,
turn_context.config.service_tier.clone(),
turn_metadata_header,
+1 -1
View File
@@ -225,7 +225,7 @@ impl ConfigDocument {
);
mutated |= self.write_optional_value(
&["model_reasoning_effort"],
effort.map(|effort| value(effort.to_string())),
effort.as_ref().map(|effort| value(effort.to_string())),
);
mutated
}),
+4 -3
View File
@@ -702,7 +702,7 @@ pub(super) async fn run_guardian_review_session(
.supported_reasoning_efforts
.iter()
.any(|effort| effort.effort == codex_protocol::openai_models::ReasoningEffort::Low),
Some(preset.default_reasoning_effort),
Some(preset.default_reasoning_effort.clone()),
);
(review_model_id.to_string(), reasoning_effort)
} else {
@@ -712,7 +712,8 @@ pub(super) async fn run_guardian_review_session(
.iter()
.any(|preset| preset.effort == codex_protocol::openai_models::ReasoningEffort::Low),
turn.reasoning_effort
.or(turn.model_info.default_reasoning_level),
.clone()
.or_else(|| turn.model_info.default_reasoning_level.clone()),
);
(
model_override
@@ -725,7 +726,7 @@ pub(super) async fn run_guardian_review_session(
turn.config.as_ref(),
live_network_config.clone(),
guardian_model.as_str(),
guardian_reasoning_effort,
guardian_reasoning_effort.clone(),
);
let guardian_config = match guardian_config {
Ok(config) => config,
+6 -5
View File
@@ -167,7 +167,7 @@ impl GuardianReviewSessionReuseKey {
model_context_window: spawn_config.model_context_window,
model_auto_compact_token_limit: spawn_config.model_auto_compact_token_limit,
model_auto_compact_token_limit_scope: spawn_config.model_auto_compact_token_limit_scope,
model_reasoning_effort: spawn_config.model_reasoning_effort,
model_reasoning_effort: spawn_config.model_reasoning_effort.clone(),
model_reasoning_summary: spawn_config.model_reasoning_summary,
permissions: spawn_config.permissions.clone(),
developer_instructions: spawn_config.developer_instructions.clone(),
@@ -658,7 +658,8 @@ async fn run_review_on_session(
let guardian_reasoning_effort = if model_info.supports_reasoning_summaries {
params
.reasoning_effort
.or(model_info.default_reasoning_level)
.clone()
.or_else(|| model_info.default_reasoning_level.clone())
} else {
None
};
@@ -742,7 +743,7 @@ async fn run_review_on_session(
mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings {
model: params.model.clone(),
reasoning_effort: params.reasoning_effort,
reasoning_effort: params.reasoning_effort.clone(),
developer_instructions: None,
},
}),
@@ -1107,7 +1108,7 @@ mod tests {
async fn test_review_params() -> GuardianReviewSessionParams {
let (session, turn) = crate::session::tests::make_session_and_context().await;
let model = turn.model_info.slug.clone();
let reasoning_effort = turn.reasoning_effort;
let reasoning_effort = turn.reasoning_effort.clone();
let reasoning_summary = turn.reasoning_summary;
let personality = turn.personality;
#[allow(deprecated)]
@@ -1116,7 +1117,7 @@ mod tests {
turn.config.as_ref(),
/*live_network_config*/ None,
model.as_str(),
reasoning_effort,
reasoning_effort.clone(),
)
.expect("guardian config");
+1 -1
View File
@@ -129,7 +129,7 @@ fn save_config_resolved_fields(
) -> anyhow::Result<()> {
lock_config.web_search = Some(config.web_search_mode.value());
lock_config.model_provider = Some(config.model_provider_id.clone());
lock_config.plan_mode_reasoning_effort = config.plan_mode_reasoning_effort;
lock_config.plan_mode_reasoning_effort = config.plan_mode_reasoning_effort.clone();
lock_config.model_verbosity = config.model_verbosity;
lock_config.include_permissions_instructions = Some(config.include_permissions_instructions);
lock_config.include_apps_instructions = Some(config.include_apps_instructions);
+1 -1
View File
@@ -579,7 +579,7 @@ impl Codex {
mode: ModeKind::Default,
settings: Settings {
model: model.clone(),
reasoning_effort: config.model_reasoning_effort,
reasoning_effort: config.model_reasoning_effort.clone(),
developer_instructions: None,
},
};
+1 -1
View File
@@ -74,7 +74,7 @@ pub(super) async fn spawn_review_thread(
let auth_manager_for_context = auth_manager.clone();
let provider_for_context = provider.clone();
let session_telemetry_for_context = session_telemetry.clone();
let reasoning_effort = per_turn_config.model_reasoning_effort;
let reasoning_effort = per_turn_config.model_reasoning_effort.clone();
let reasoning_summary = per_turn_config
.model_reasoning_summary
.unwrap_or(model_info.default_reasoning_summary);
@@ -74,7 +74,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
effort: turn_context.reasoning_effort.clone(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
let rollout_items = vec![RolloutItem::TurnContext(previous_context_item)];
@@ -113,7 +113,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
effort: turn_context.reasoning_effort.clone(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
let turn_id = previous_context_item
@@ -963,7 +963,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
effort: turn_context.reasoning_effort.clone(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
let previous_turn_id = previous_context_item
@@ -1042,7 +1042,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
effort: turn_context.reasoning_effort.clone(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
}))
.expect("serialize expected reference context item")
@@ -1071,7 +1071,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
effort: turn_context.reasoning_effort.clone(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
let previous_turn_id = previous_context_item
@@ -1190,7 +1190,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
effort: turn_context.reasoning_effort.clone(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
@@ -1308,7 +1308,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
effort: turn_context.reasoning_effort.clone(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
let previous_turn_id = previous_context_item
@@ -1467,7 +1467,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
effort: turn_context.reasoning_effort.clone(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
let previous_turn_id = previous_context_item
+10 -10
View File
@@ -2559,7 +2559,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active),
effort: turn_context.reasoning_effort,
effort: turn_context.reasoning_effort.clone(),
summary: codex_protocol::config_types::ReasoningSummary::Auto,
};
let turn_id = previous_context_item
@@ -3146,7 +3146,7 @@ async fn set_rate_limits_retains_previous_credits() {
let model = get_model_offline_for_tests(config.model.as_deref());
let model_info =
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config());
let reasoning_effort = config.model_reasoning_effort;
let reasoning_effort = config.model_reasoning_effort.clone();
let collaboration_mode = CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
@@ -3254,7 +3254,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
let model = get_model_offline_for_tests(config.model.as_deref());
let model_info =
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config());
let reasoning_effort = config.model_reasoning_effort;
let reasoning_effort = config.model_reasoning_effort.clone();
let collaboration_mode = CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
@@ -3785,7 +3785,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
let model = get_model_offline_for_tests(config.model.as_deref());
let model_info =
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config());
let reasoning_effort = config.model_reasoning_effort;
let reasoning_effort = config.model_reasoning_effort.clone();
let collaboration_mode = CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
@@ -4614,7 +4614,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
mode: ModeKind::Default,
settings: Settings {
model,
reasoning_effort: config.model_reasoning_effort,
reasoning_effort: config.model_reasoning_effort.clone(),
developer_instructions: None,
},
};
@@ -4716,7 +4716,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
let model = get_model_offline_for_tests(config.model.as_deref());
let model_info =
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config());
let reasoning_effort = config.model_reasoning_effort;
let reasoning_effort = config.model_reasoning_effort.clone();
let collaboration_mode = CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
@@ -4959,7 +4959,7 @@ async fn make_session_with_config_and_rx(
mode: ModeKind::Default,
settings: Settings {
model,
reasoning_effort: config.model_reasoning_effort,
reasoning_effort: config.model_reasoning_effort.clone(),
developer_instructions: None,
},
};
@@ -5064,7 +5064,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
mode: ModeKind::Default,
settings: Settings {
model,
reasoning_effort: config.model_reasoning_effort,
reasoning_effort: config.model_reasoning_effort.clone(),
developer_instructions: None,
},
};
@@ -5999,7 +5999,7 @@ async fn user_turn_updates_approvals_reviewer() {
mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings {
model: turn_context.model_info.slug.clone(),
reasoning_effort: config.model_reasoning_effort,
reasoning_effort: config.model_reasoning_effort.clone(),
developer_instructions: None,
},
}),
@@ -6806,7 +6806,7 @@ where
let model = get_model_offline_for_tests(config.model.as_deref());
let model_info =
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config());
let reasoning_effort = config.model_reasoning_effort;
let reasoning_effort = config.model_reasoning_effort.clone();
let collaboration_mode = CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
+2 -2
View File
@@ -698,7 +698,7 @@ async fn track_turn_resolved_config_analytics(
permission_profile: turn_context.permission_profile(),
#[allow(deprecated)]
permission_profile_cwd: turn_context.cwd.to_path_buf(),
reasoning_effort: turn_context.reasoning_effort,
reasoning_effort: turn_context.reasoning_effort.clone(),
reasoning_summary: Some(turn_context.reasoning_summary),
service_tier: turn_context
.config
@@ -1799,7 +1799,7 @@ async fn try_run_sampling_request(
prompt,
&turn_context.model_info,
&turn_context.session_telemetry,
turn_context.reasoning_effort,
turn_context.reasoning_effort.clone(),
turn_context.reasoning_summary,
turn_context.config.service_tier.clone(),
turn_metadata_header,
+12 -10
View File
@@ -136,7 +136,8 @@ impl TurnContext {
pub(crate) fn effective_reasoning_effort(&self) -> Option<ReasoningEffortConfig> {
if self.model_info.supports_reasoning_summaries {
self.reasoning_effort
.or(self.model_info.default_reasoning_level)
.clone()
.or_else(|| self.model_info.default_reasoning_level.clone())
} else {
None
}
@@ -196,28 +197,29 @@ impl TurnContext {
let supported_reasoning_levels = model_info
.supported_reasoning_levels
.iter()
.map(|preset| preset.effort)
.map(|preset| preset.effort.clone())
.collect::<Vec<_>>();
let reasoning_effort = if let Some(current_reasoning_effort) = self.reasoning_effort {
let reasoning_effort = if let Some(current_reasoning_effort) = self.reasoning_effort.clone()
{
if supported_reasoning_levels.contains(&current_reasoning_effort) {
Some(current_reasoning_effort)
} else {
supported_reasoning_levels
.get(supported_reasoning_levels.len().saturating_sub(1) / 2)
.copied()
.or(model_info.default_reasoning_level)
.cloned()
.or_else(|| model_info.default_reasoning_level.clone())
}
} else {
supported_reasoning_levels
.get(supported_reasoning_levels.len().saturating_sub(1) / 2)
.copied()
.or(model_info.default_reasoning_level)
.cloned()
.or_else(|| model_info.default_reasoning_level.clone())
};
config.model_reasoning_effort = reasoning_effort;
config.model_reasoning_effort = reasoning_effort.clone();
let collaboration_mode = self.collaboration_mode.with_updates(
Some(model.clone()),
Some(reasoning_effort),
Some(reasoning_effort.clone()),
/*developer_instructions*/ None,
);
let features = self.features.clone();
@@ -363,7 +365,7 @@ impl TurnContext {
collaboration_mode: Some(self.collaboration_mode.clone()),
multi_agent_version: Some(self.multi_agent_version),
realtime_active: Some(self.realtime_active),
effort: self.reasoning_effort,
effort: self.reasoning_effort.clone(),
summary: ReasoningSummaryConfig::Auto,
}
}
+1 -1
View File
@@ -271,7 +271,7 @@ async fn schedule_startup_prewarm_inner(
&startup_prompt,
&startup_turn_context.model_info,
&startup_turn_context.session_telemetry,
startup_turn_context.reasoning_effort,
startup_turn_context.reasoning_effort.clone(),
startup_turn_context.reasoning_summary,
startup_turn_context.config.service_tier.clone(),
startup_turn_metadata_header.as_deref(),
@@ -83,7 +83,7 @@ async fn handle_spawn_agent(
sender_thread_id: session.thread_id,
prompt: prompt.clone(),
model: args.model.clone().unwrap_or_default(),
reasoning_effort: args.reasoning_effort.unwrap_or_default(),
reasoning_effort: args.reasoning_effort.clone().unwrap_or_default(),
}
.into(),
)
@@ -94,14 +94,18 @@ async fn handle_spawn_agent(
config.service_tier = Some(service_tier.clone());
}
if args.fork_context {
reject_full_fork_spawn_overrides(role_name, args.model.as_deref(), args.reasoning_effort)?;
reject_full_fork_spawn_overrides(
role_name,
args.model.as_deref(),
args.reasoning_effort.clone(),
)?;
} else {
apply_requested_spawn_agent_model_overrides(
&session,
turn.as_ref(),
&mut config,
args.model.as_deref(),
args.reasoning_effort,
args.reasoning_effort.clone(),
)
.await?;
apply_role_to_config(&mut config, role_name)
@@ -174,7 +178,7 @@ async fn handle_spawn_agent(
.unwrap_or_else(|| args.model.clone().unwrap_or_default());
let effective_reasoning_effort = agent_snapshot
.as_ref()
.and_then(|snapshot| snapshot.reasoning_effort)
.and_then(|snapshot| snapshot.reasoning_effort.clone())
.unwrap_or(args.reasoning_effort.unwrap_or_default());
let nickname = new_agent_nickname.clone();
session
@@ -224,7 +224,8 @@ fn build_agent_shared_config(turn: &TurnContext) -> Result<Config, FunctionCallE
config.model_provider = turn.provider.info().clone();
config.model_reasoning_effort = turn
.reasoning_effort
.or(turn.model_info.default_reasoning_level);
.clone()
.or_else(|| turn.model_info.default_reasoning_level.clone());
config.model_reasoning_summary = Some(turn.reasoning_summary);
config.developer_instructions = turn.developer_instructions.clone();
config.compact_prompt = turn.compact_prompt.clone();
@@ -304,7 +305,7 @@ pub(crate) async fn apply_requested_spawn_agent_model_overrides(
validate_spawn_agent_reasoning_effort(
&selected_model_name,
&selected_model_info.supported_reasoning_levels,
reasoning_effort,
&reasoning_effort,
)?;
config.model_reasoning_effort = Some(reasoning_effort);
} else {
@@ -318,7 +319,7 @@ pub(crate) async fn apply_requested_spawn_agent_model_overrides(
validate_spawn_agent_reasoning_effort(
&turn.model_info.slug,
&turn.model_info.supported_reasoning_levels,
reasoning_effort,
&reasoning_effort,
)?;
config.model_reasoning_effort = Some(reasoning_effort);
}
@@ -404,11 +405,11 @@ fn find_spawn_agent_model_name(
fn validate_spawn_agent_reasoning_effort(
model: &str,
supported_reasoning_levels: &[ReasoningEffortPreset],
requested_reasoning_effort: ReasoningEffort,
requested_reasoning_effort: &ReasoningEffort,
) -> Result<(), FunctionCallError> {
if supported_reasoning_levels
.iter()
.any(|preset| preset.effort == requested_reasoning_effort)
.any(|preset| &preset.effort == requested_reasoning_effort)
{
return Ok(());
}
@@ -17,6 +17,7 @@ const SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION: &str =
const SPAWN_AGENT_SERVICE_TIER_OVERRIDE_DESCRIPTION: &str =
"Service tier override for the new agent. Omit unless explicitly requested.";
const MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT_DESCRIPTION: usize = 5;
const MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION: usize = 64;
#[derive(Debug, Clone, Default)]
pub struct SpawnAgentToolOptions {
@@ -762,13 +763,20 @@ fn spawn_agent_models_description(models: &[ModelPreset]) -> String {
let model_descriptions = visible_models
.into_iter()
.map(|model| {
let default_reasoning_effort = model.default_reasoning_effort;
let default_reasoning_effort = &model.default_reasoning_effort;
let efforts = model
.supported_reasoning_efforts
.iter()
.map(|preset| {
let effort = preset.effort;
if effort == default_reasoning_effort {
let effort = preset.effort.as_str();
let effort = match effort
.char_indices()
.nth(MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION)
{
Some((index, _)) => &effort[..index],
None => effort,
};
if &preset.effort == default_reasoning_effort {
format!("{effort} (default)")
} else {
effort.to_string()
@@ -186,6 +186,27 @@ fn spawn_agent_tool_caps_visible_model_summaries() {
assert!(!description.contains("`sixth-model`"));
}
#[test]
fn spawn_agent_tool_caps_reasoning_effort_value_length() {
let mut model = model_preset("visible", /*show_in_picker*/ true);
let custom_effort = ReasoningEffort::Custom(
"é".repeat(MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION + 1),
);
model.default_reasoning_effort = custom_effort.clone();
model.supported_reasoning_efforts = vec![ReasoningEffortPreset {
effort: custom_effort,
description: "Model-defined".to_string(),
}];
assert_eq!(
spawn_agent_models_description(&[model]),
format!(
"Available model overrides (optional; inherited parent model is preferred):\n- `visible-model`: visible description Reasoning efforts: {} (default). Service tiers: priority.",
"é".repeat(MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION)
)
);
}
#[test]
fn spawn_agent_tool_hides_service_tier_with_spawn_metadata() {
let tool = create_spawn_agent_tool_v2(SpawnAgentToolOptions {
@@ -4374,7 +4374,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
expected.base_instructions = Some(base_instructions.text);
expected.model = Some(turn.model_info.slug.clone());
expected.model_provider = turn.provider.info().clone();
expected.model_reasoning_effort = turn.reasoning_effort;
expected.model_reasoning_effort = turn.reasoning_effort.clone();
expected.model_reasoning_summary = Some(turn.reasoning_summary);
expected.developer_instructions = turn.developer_instructions.clone();
expected.compact_prompt = turn.compact_prompt.clone();
@@ -4431,7 +4431,7 @@ async fn build_agent_resume_config_clears_base_instructions() {
expected.base_instructions = None;
expected.model = Some(turn.model_info.slug.clone());
expected.model_provider = turn.provider.info().clone();
expected.model_reasoning_effort = turn.reasoning_effort;
expected.model_reasoning_effort = turn.reasoning_effort.clone();
expected.model_reasoning_summary = Some(turn.reasoning_summary);
expected.developer_instructions = turn.developer_instructions.clone();
expected.compact_prompt = turn.compact_prompt.clone();
@@ -75,7 +75,7 @@ async fn handle_spawn_agent(
sender_thread_id: session.thread_id,
prompt: prompt.clone(),
model: args.model.clone().unwrap_or_default(),
reasoning_effort: args.reasoning_effort.unwrap_or_default(),
reasoning_effort: args.reasoning_effort.clone().unwrap_or_default(),
}
.into(),
)
@@ -86,14 +86,18 @@ async fn handle_spawn_agent(
config.service_tier = Some(service_tier.clone());
}
if matches!(fork_mode, Some(SpawnAgentForkMode::FullHistory)) {
reject_full_fork_spawn_overrides(role_name, args.model.as_deref(), args.reasoning_effort)?;
reject_full_fork_spawn_overrides(
role_name,
args.model.as_deref(),
args.reasoning_effort.clone(),
)?;
} else {
apply_requested_spawn_agent_model_overrides(
&session,
turn.as_ref(),
&mut config,
args.model.as_deref(),
args.reasoning_effort,
args.reasoning_effort.clone(),
)
.await?;
apply_role_to_config(&mut config, role_name)
@@ -188,7 +192,7 @@ async fn handle_spawn_agent(
.unwrap_or_else(|| args.model.clone().unwrap_or_default());
let effective_reasoning_effort = agent_snapshot
.as_ref()
.and_then(|snapshot| snapshot.reasoning_effort)
.and_then(|snapshot| snapshot.reasoning_effort.clone())
.unwrap_or(args.reasoning_effort.unwrap_or_default());
let nickname = new_agent_nickname.clone();
session
+3 -3
View File
@@ -74,7 +74,7 @@ async fn responses_stream_includes_subagent_header_on_review() {
let mut config = load_default_config_for_test(&codex_home).await;
config.model_provider_id = provider.name.clone();
config.model_provider = provider.clone();
let effort = config.model_reasoning_effort;
let effort = config.model_reasoning_effort.clone();
let summary = config.model_reasoning_summary;
let model = codex_core::test_support::get_model_offline(config.model.as_deref());
config.model = Some(model.clone());
@@ -202,7 +202,7 @@ async fn responses_stream_includes_subagent_header_on_other() {
let mut config = load_default_config_for_test(&codex_home).await;
config.model_provider_id = provider.name.clone();
config.model_provider = provider.clone();
let effort = config.model_reasoning_effort;
let effort = config.model_reasoning_effort.clone();
let summary = config.model_reasoning_summary;
let model = codex_core::test_support::get_model_offline(config.model.as_deref());
config.model = Some(model.clone());
@@ -318,7 +318,7 @@ async fn responses_respects_model_info_overrides_from_config() {
config.model_provider = provider.clone();
config.model_supports_reasoning_summaries = Some(true);
config.model_reasoning_summary = Some(ReasoningSummary::Detailed);
let effort = config.model_reasoning_effort;
let effort = config.model_reasoning_effort.clone();
let summary = config.model_reasoning_summary;
let model = config.model.clone().expect("model configured");
let config = Arc::new(config);
+2 -2
View File
@@ -876,7 +876,7 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth
let mut config = load_default_config_for_test(&codex_home).await;
config.model_provider_id = provider.name.clone();
config.model_provider = provider.clone();
let effort = config.model_reasoning_effort;
let effort = config.model_reasoning_effort.clone();
let summary = config.model_reasoning_summary;
let model = codex_core::test_support::get_model_offline(config.model.as_deref());
config.model = Some(model.clone());
@@ -2332,7 +2332,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
let mut config = load_default_config_for_test(&codex_home).await;
config.model_provider_id = provider.name.clone();
config.model_provider = provider.clone();
let effort = config.model_reasoning_effort;
let effort = config.model_reasoning_effort.clone();
let summary = config.model_reasoning_summary;
let model = codex_core::test_support::get_model_offline(config.model.as_deref());
config.model = Some(model.clone());
+12 -12
View File
@@ -343,7 +343,7 @@ async fn responses_websocket_request_prewarm_reuses_connection() {
&prompt,
&harness.model_info,
&harness.session_telemetry,
harness.effort,
harness.effort.clone(),
harness.summary,
/*service_tier*/ None,
/*turn_metadata_header*/ None,
@@ -397,7 +397,7 @@ async fn responses_websocket_request_prewarm_traces_logical_request() {
&prompt,
&harness.model_info,
&harness.session_telemetry,
harness.effort,
harness.effort.clone(),
harness.summary,
/*service_tier*/ None,
/*turn_metadata_header*/ None,
@@ -442,7 +442,7 @@ async fn responses_websocket_request_prewarm_traces_logical_request() {
&prompt,
&harness.model_info,
&harness.session_telemetry,
harness.effort,
harness.effort.clone(),
harness.summary,
/*service_tier*/ None,
/*turn_metadata_header*/ None,
@@ -534,7 +534,7 @@ async fn responses_websocket_preconnect_is_reused_even_with_header_changes() {
&prompt,
&harness.model_info,
&harness.session_telemetry,
harness.effort,
harness.effort.clone(),
harness.summary,
/*service_tier*/ None,
/*turn_metadata_header*/ None,
@@ -573,7 +573,7 @@ async fn responses_websocket_request_prewarm_is_reused_even_with_header_changes(
&prompt,
&harness.model_info,
&harness.session_telemetry,
harness.effort,
harness.effort.clone(),
harness.summary,
/*service_tier*/ None,
/*turn_metadata_header*/ None,
@@ -585,7 +585,7 @@ async fn responses_websocket_request_prewarm_is_reused_even_with_header_changes(
&prompt,
&harness.model_info,
&harness.session_telemetry,
harness.effort,
harness.effort.clone(),
harness.summary,
/*service_tier*/ None,
/*turn_metadata_header*/ None,
@@ -639,7 +639,7 @@ async fn responses_websocket_prewarm_uses_v2_when_provider_supports_websockets()
&prompt,
&harness.model_info,
&harness.session_telemetry,
harness.effort,
harness.effort.clone(),
harness.summary,
/*service_tier*/ None,
/*turn_metadata_header*/ None,
@@ -988,7 +988,7 @@ async fn responses_websocket_emits_reasoning_included_event() {
&prompt,
&harness.model_info,
&harness.session_telemetry,
harness.effort,
harness.effort.clone(),
harness.summary,
/*service_tier*/ None,
/*turn_metadata_header*/ None,
@@ -1062,7 +1062,7 @@ async fn responses_websocket_emits_rate_limit_events() {
&prompt,
&harness.model_info,
&harness.session_telemetry,
harness.effort,
harness.effort.clone(),
harness.summary,
/*service_tier*/ None,
/*turn_metadata_header*/ None,
@@ -1718,7 +1718,7 @@ async fn responses_websocket_v2_after_error_uses_full_create_without_previous_re
&prompt_two,
&harness.model_info,
&harness.session_telemetry,
harness.effort,
harness.effort.clone(),
harness.summary,
/*service_tier*/ None,
/*turn_metadata_header*/ None,
@@ -1806,7 +1806,7 @@ async fn responses_websocket_v2_surfaces_terminal_error_without_close_handshake(
&prompt_two,
&harness.model_info,
&harness.session_telemetry,
harness.effort,
harness.effort.clone(),
harness.summary,
/*service_tier*/ None,
/*turn_metadata_header*/ None,
@@ -2073,7 +2073,7 @@ async fn stream_until_complete_with_request_metadata(
prompt,
&harness.model_info,
&harness.session_telemetry,
harness.effort,
harness.effort.clone(),
harness.summary,
service_tier.map(|service_tier| service_tier.request_value().to_string()),
turn_metadata_header,
+1 -1
View File
@@ -58,7 +58,7 @@ fn read_only_user_turn(test: &TestCodex, items: Vec<UserInput>, model: String) -
mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings {
model,
reasoning_effort: test.config.model_reasoning_effort,
reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None,
},
}),
@@ -135,7 +135,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings {
model: test.session_configured.model.clone(),
reasoning_effort: test.config.model_reasoning_effort,
reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None,
},
}),
@@ -172,7 +172,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings {
model: test.session_configured.model.clone(),
reasoning_effort: test.config.model_reasoning_effort,
reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None,
},
}),
@@ -261,7 +261,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings {
model: test.session_configured.model.clone(),
reasoning_effort: test.config.model_reasoning_effort,
reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None,
},
}),
@@ -295,7 +295,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings {
model: test.session_configured.model.clone(),
reasoning_effort: test.config.model_reasoning_effort,
reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None,
},
}),
@@ -422,7 +422,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings {
model: resumed.session_configured.model.clone(),
reasoning_effort: resumed.config.model_reasoning_effort,
reasoning_effort: resumed.config.model_reasoning_effort.clone(),
developer_instructions: None,
},
}),
+1 -1
View File
@@ -79,7 +79,7 @@ fn read_only_text_turn_with_personality(
mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings {
model,
reasoning_effort: test.config.model_reasoning_effort,
reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None,
},
}),
+3 -3
View File
@@ -870,7 +870,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
let default_approval_policy = config.permissions.approval_policy.value();
let default_sandbox_policy = &config.legacy_sandbox_policy();
let default_model = session_configured.model;
let default_effort = config.model_reasoning_effort;
let default_effort = config.model_reasoning_effort.clone();
let default_summary = config.model_reasoning_summary;
codex
@@ -892,7 +892,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings {
model: default_model.clone(),
reasoning_effort: default_effort,
reasoning_effort: default_effort.clone(),
developer_instructions: None,
},
}),
@@ -1013,7 +1013,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
let default_approval_policy = config.permissions.approval_policy.value();
let default_sandbox_policy = &config.legacy_sandbox_policy();
let default_model = session_configured.model;
let default_effort = config.model_reasoning_effort;
let default_effort = config.model_reasoning_effort.clone();
let default_summary = config.model_reasoning_summary;
codex
+6 -5
View File
@@ -318,7 +318,7 @@ async fn remote_models_use_context_window_when_config_override_is_absent() -> Re
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<()> {
async fn remote_models_long_model_slug_is_sent_with_custom_reasoning() -> Result<()> {
skip_if_no_network!(Ok(()));
skip_if_sandbox!(Ok(()));
@@ -331,15 +331,16 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<(
/*priority*/ 1_000,
TruncationPolicyConfig::bytes(/*limit*/ 10_000),
);
remote_model.default_reasoning_level = Some(ReasoningEffort::High);
let custom_reasoning_effort = ReasoningEffort::Custom("max".to_string());
remote_model.default_reasoning_level = Some(custom_reasoning_effort.clone());
remote_model.supported_reasoning_levels = vec![
ReasoningEffortPreset {
effort: ReasoningEffort::Medium,
description: ReasoningEffort::Medium.to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::High,
description: ReasoningEffort::High.to_string(),
effort: custom_reasoning_effort.clone(),
description: custom_reasoning_effort.to_string(),
},
];
remote_model.supports_reasoning_summaries = true;
@@ -393,7 +394,7 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<(
.and_then(|reasoning| reasoning.get("summary"))
.and_then(|value| value.as_str());
assert_eq!(body["model"].as_str(), Some(requested_model));
assert_eq!(reasoning_effort, Some("high"));
assert_eq!(reasoning_effort, Some("max"));
assert_eq!(reasoning_summary, Some("detailed"));
Ok(())
+1 -1
View File
@@ -41,7 +41,7 @@ fn resume_history(
collaboration_mode: None,
multi_agent_version: None,
realtime_active: None,
effort: config.model_reasoning_effort,
effort: config.model_reasoning_effort.clone(),
summary: config
.model_reasoning_summary
.unwrap_or(ReasoningSummary::Auto),
@@ -55,7 +55,7 @@ fn disabled_text_turn(test: &TestCodex, text: &str) -> Op {
mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings {
model: REQUESTED_MODEL.to_string(),
reasoning_effort: test.config.model_reasoning_effort,
reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None,
},
}),
+1 -1
View File
@@ -68,7 +68,7 @@ Each response yields:
- `data` - ordered list of models. A model includes:
- `id`, `model`, `displayName`, `description`
- `supportedReasoningEfforts` - array of objects with:
- `reasoningEffort` - one of `none|minimal|low|medium|high|xhigh`
- `reasoningEffort` - a string value advertised by the model; common values are `none|minimal|low|medium|high|xhigh`
- `description` - human-friendly label for the effort
- `defaultReasoningEffort` - suggested effort for the UI
- `inputModalities` - accepted input types for the model
@@ -446,7 +446,8 @@ fn config_summary_entries(
"reasoning effort",
config
.model_reasoning_effort
.map(|effort| effort.to_string())
.as_ref()
.map(std::string::ToString::to_string)
.unwrap_or_else(|| "none".to_string()),
));
entries.push((
+3 -3
View File
@@ -694,7 +694,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
let default_cwd = config.cwd.to_path_buf();
let default_approval_policy = config.permissions.approval_policy.value();
let default_effort = config.model_reasoning_effort;
let default_effort = config.model_reasoning_effort.clone();
let (initial_operation, prompt_summary) = match (command.as_ref(), prompt, images) {
(Some(ExecCommand::Review(review_cli)), _, _) => {
@@ -1169,7 +1169,7 @@ fn session_configured_from_thread_start_response(
config.permissions.effective_permission_profile(),
response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(),
response.reasoning_effort,
response.reasoning_effort.clone(),
)
}
@@ -1192,7 +1192,7 @@ fn session_configured_from_thread_resume_response(
config.permissions.effective_permission_profile(),
response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(),
response.reasoning_effort,
response.reasoning_effort.clone(),
)
}
+1 -1
View File
@@ -194,7 +194,7 @@ impl MemoryStartupContext {
prompt,
&context.model_info,
&context.session_telemetry,
context.reasoning_effort,
context.reasoning_effort.clone(),
context.reasoning_summary,
context.service_tier.clone(),
context.turn_metadata_header.as_deref(),
@@ -458,7 +458,7 @@ impl SessionTelemetry {
auth.env_provider_key_name = self.metadata.auth_env.provider_env_key_name.as_deref(),
auth.env_provider_key_present = self.metadata.auth_env.provider_env_key_present,
auth.env_refresh_token_url_override_present = self.metadata.auth_env.refresh_token_url_override_present,
reasoning_effort = reasoning_effort.map(|e| e.to_string()),
reasoning_effort = reasoning_effort.as_ref().map(ToString::to_string),
reasoning_summary = %reasoning_summary,
context_window = context_window,
auto_compact_token_limit = auto_compact_token_limit,
+2 -1
View File
@@ -59,4 +59,5 @@ tempfile = { workspace = true }
[package.metadata.cargo-shear]
# Required because:
# `icu_provider`: contains a required `sync` feature for `icu_decimal`
ignored = ["icu_provider"]
# `strum`: is referenced by generated `EnumIter` derive implementations
ignored = ["icu_provider", "strum"]
+6 -3
View File
@@ -635,7 +635,7 @@ impl CollaborationMode {
}
pub fn reasoning_effort(&self) -> Option<ReasoningEffort> {
self.settings_ref().reasoning_effort
self.settings_ref().reasoning_effort.clone()
}
/// Updates the collaboration mode with new model and/or effort values.
@@ -654,7 +654,7 @@ impl CollaborationMode {
let settings = self.settings_ref();
let updated_settings = Settings {
model: model.unwrap_or_else(|| settings.model.clone()),
reasoning_effort: effort.unwrap_or(settings.reasoning_effort),
reasoning_effort: effort.unwrap_or_else(|| settings.reasoning_effort.clone()),
developer_instructions: developer_instructions
.unwrap_or_else(|| settings.developer_instructions.clone()),
};
@@ -676,7 +676,10 @@ impl CollaborationMode {
mode: mask.mode.unwrap_or(self.mode),
settings: Settings {
model: mask.model.clone().unwrap_or_else(|| settings.model.clone()),
reasoning_effort: mask.reasoning_effort.unwrap_or(settings.reasoning_effort),
reasoning_effort: mask
.reasoning_effort
.clone()
.unwrap_or_else(|| settings.reasoning_effort.clone()),
developer_instructions: mask
.developer_instructions
.clone()
+182 -44
View File
@@ -4,14 +4,22 @@
//! are used to preserve compatibility when older payloads omit newly introduced attributes.
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use schemars::JsonSchema;
use schemars::r#gen::SchemaGenerator;
use schemars::schema::InstanceType;
use schemars::schema::Metadata;
use schemars::schema::Schema;
use schemars::schema::SchemaObject;
use schemars::schema::StringValidation;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::Serializer;
use serde::de::DeserializeOwned;
use strum::IntoEnumIterator;
use serde::de::Error;
use strum_macros::Display;
use strum_macros::EnumIter;
use tracing::warn;
@@ -28,23 +36,8 @@ const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}";
pub const SPEED_TIER_FAST: &str = "fast";
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
#[derive(
Debug,
Serialize,
Deserialize,
Default,
Clone,
Copy,
PartialEq,
Eq,
Display,
JsonSchema,
TS,
EnumIter,
Hash,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
#[derive(Debug, Default, Clone, PartialEq, Eq, TS, Hash)]
#[ts(type = "string")]
pub enum ReasoningEffort {
None,
Minimal,
@@ -53,14 +46,113 @@ pub enum ReasoningEffort {
Medium,
High,
XHigh,
/// A model-defined effort value that this client does not know yet.
Custom(String),
}
impl ReasoningEffort {
/// Returns the exact value used on the wire.
pub fn as_str(&self) -> &str {
match self {
Self::None => "none",
Self::Minimal => "minimal",
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::XHigh => "xhigh",
Self::Custom(effort) => effort,
}
}
/// Returns the built-in effort values in ascending order.
pub fn known_values() -> impl DoubleEndedIterator<Item = Self> + ExactSizeIterator {
[
Self::None,
Self::Minimal,
Self::Low,
Self::Medium,
Self::High,
Self::XHigh,
]
.into_iter()
}
/// Returns the built-in ordering rank, or `None` for model-defined values.
pub const fn known_rank(&self) -> Option<usize> {
match self {
Self::None => Some(0),
Self::Minimal => Some(1),
Self::Low => Some(2),
Self::Medium => Some(3),
Self::High => Some(4),
Self::XHigh => Some(5),
Self::Custom(_) => None,
}
}
}
impl fmt::Display for ReasoningEffort {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl JsonSchema for ReasoningEffort {
fn schema_name() -> String {
"ReasoningEffort".to_string()
}
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
metadata: Some(Box::new(Metadata {
description: Some(
"A non-empty reasoning effort value advertised by the model.".to_string(),
),
..Default::default()
})),
string: Some(Box::new(StringValidation {
min_length: Some(1),
..Default::default()
})),
..Default::default()
})
}
}
impl Serialize for ReasoningEffort {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for ReasoningEffort {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let effort = String::deserialize(deserializer)?;
effort.parse().map_err(D::Error::custom)
}
}
impl FromStr for ReasoningEffort {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_value(serde_json::Value::String(s.to_string()))
.map_err(|_| format!("invalid reasoning_effort: {s}"))
match s {
"none" => Ok(Self::None),
"minimal" => Ok(Self::Minimal),
"low" => Ok(Self::Low),
"medium" => Ok(Self::Medium),
"high" => Ok(Self::High),
"xhigh" => Ok(Self::XHigh),
"" => Err("reasoning_effort must not be empty".to_string()),
effort => Ok(Self::Custom(effort.to_string())),
}
}
}
@@ -579,39 +671,43 @@ fn reasoning_effort_mapping_from_presets(
}
// Map every canonical effort to the closest supported effort for the new model.
let supported: Vec<ReasoningEffort> = presets.iter().map(|p| p.effort).collect();
let supported: Vec<ReasoningEffort> = presets.iter().map(|p| p.effort.clone()).collect();
let mut map = HashMap::new();
for effort in ReasoningEffort::iter() {
let nearest = nearest_effort(effort, &supported);
for effort in ReasoningEffort::known_values() {
let nearest = nearest_effort(&effort, &supported);
map.insert(effort, nearest);
}
Some(map)
}
fn effort_rank(effort: ReasoningEffort) -> i32 {
match effort {
ReasoningEffort::None => 0,
ReasoningEffort::Minimal => 1,
ReasoningEffort::Low => 2,
ReasoningEffort::Medium => 3,
ReasoningEffort::High => 4,
ReasoningEffort::XHigh => 5,
}
}
fn nearest_effort(target: ReasoningEffort, supported: &[ReasoningEffort]) -> ReasoningEffort {
let target_rank = effort_rank(target);
fn nearest_effort(target: &ReasoningEffort, supported: &[ReasoningEffort]) -> ReasoningEffort {
let Some(target_rank) = target.known_rank() else {
return supported
.iter()
.find(|candidate| *candidate == target)
.unwrap_or(target)
.clone();
};
supported
.iter()
.copied()
.min_by_key(|candidate| (effort_rank(*candidate) - target_rank).abs())
.filter_map(|candidate| {
candidate
.known_rank()
.map(|rank| (rank.abs_diff(target_rank), candidate))
})
.min_by_key(|(distance, _)| *distance)
.map(|(_, effort)| effort)
.or_else(|| supported.first())
.unwrap_or(target)
.clone()
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use serde_json::from_str;
use serde_json::to_string;
fn test_model(spec: Option<ModelMessages>) -> ModelInfo {
ModelInfo {
@@ -663,16 +759,58 @@ mod tests {
}
#[test]
fn reasoning_effort_from_str_accepts_known_values() {
assert_eq!("high".parse(), Ok(ReasoningEffort::High));
assert_eq!("minimal".parse(), Ok(ReasoningEffort::Minimal));
fn reasoning_effort_accepts_known_and_custom_values() {
let custom = ReasoningEffort::Custom("max".to_string());
let deserialized = from_str::<ReasoningEffort>(r#""max""#)
.expect("custom reasoning effort should deserialize");
let serialized = to_string(&custom).expect("custom reasoning effort should serialize");
assert_eq!(
(
"high".parse(),
"max".parse(),
deserialized,
serialized,
custom.to_string(),
),
(
Ok(ReasoningEffort::High),
Ok(custom.clone()),
custom,
r#""max""#.to_string(),
"max".to_string(),
)
);
}
#[test]
fn reasoning_effort_from_str_rejects_unknown_values() {
fn reasoning_effort_rejects_empty_values() {
assert_eq!(
"unsupported".parse::<ReasoningEffort>(),
Err("invalid reasoning_effort: unsupported".to_string())
"".parse::<ReasoningEffort>(),
Err("reasoning_effort must not be empty".to_string())
);
}
#[test]
fn reasoning_effort_json_schema_is_an_open_string() {
let mut effort_generator = SchemaGenerator::default();
assert_eq!(
ReasoningEffort::json_schema(&mut effort_generator),
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
metadata: Some(Box::new(Metadata {
description: Some(
"A non-empty reasoning effort value advertised by the model.".to_string(),
),
..Default::default()
})),
string: Some(Box::new(StringValidation {
min_length: Some(1),
..Default::default()
})),
..Default::default()
})
);
}
+1 -1
View File
@@ -75,7 +75,7 @@ fn apply_turn_context(metadata: &mut ThreadMetadata, turn_ctx: &TurnContextItem)
metadata.cwd = turn_ctx.cwd.clone();
}
metadata.model = Some(turn_ctx.model.clone());
metadata.reasoning_effort = turn_ctx.effort;
metadata.reasoning_effort = turn_ctx.effort.clone();
metadata.sandbox_policy =
serde_json::to_string(&turn_ctx.permission_profile()).unwrap_or_default();
metadata.approval_mode = enum_to_string(&turn_ctx.approval_policy);
+2 -2
View File
@@ -584,13 +584,13 @@ mod tests {
}
#[test]
fn thread_row_ignores_unknown_reasoning_effort_values() {
fn thread_row_preserves_model_defined_reasoning_effort_values() {
let metadata = ThreadMetadata::try_from(thread_row(Some("future")))
.expect("thread metadata should parse");
assert_eq!(
metadata,
expected_thread_metadata(/*reasoning_effort*/ None)
expected_thread_metadata(Some(ReasoningEffort::Custom("future".to_string())))
);
}
}
+1 -1
View File
@@ -369,7 +369,7 @@ fn stored_thread_from_state(
.and_then(|metadata| metadata.model_provider.clone())
.unwrap_or_else(|| "test".to_string()),
model: metadata.and_then(|metadata| metadata.model.clone()),
reasoning_effort: metadata.and_then(|metadata| metadata.reasoning_effort),
reasoning_effort: metadata.and_then(|metadata| metadata.reasoning_effort.clone()),
created_at: metadata
.and_then(|metadata| metadata.created_at)
.unwrap_or_else(Utc::now),
@@ -232,7 +232,7 @@ impl ThreadMetadataSync {
update.cwd = Some(turn_ctx.cwd.clone());
}
update.model = Some(turn_ctx.model.clone());
update.reasoning_effort = turn_ctx.effort;
update.reasoning_effort = turn_ctx.effort.clone();
update.approval_mode = Some(turn_ctx.approval_policy);
update.permission_profile = Some(turn_ctx.permission_profile());
}
+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));
}
+5 -4
View File
@@ -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
+1 -1
View File
@@ -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));
}
+80 -79
View File
@@ -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 });
}
+4 -2
View File
@@ -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(&current_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 == &current_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![
+2 -2
View File
@@ -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();
+7 -11
View File
@@ -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);
@@ -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;
+6 -11
View File
@@ -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());
+1 -1
View File
@@ -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,
}
+1 -1
View File
@@ -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));
@@ -25,7 +25,8 @@ pub fn create_config_summary_entries(config: &Config, model: &str) -> Vec<(&'sta
if config.model_provider.wire_api == WireApi::Responses {
let reasoning_effort = config
.model_reasoning_effort
.map(|effort| effort.to_string());
.as_ref()
.map(std::string::ToString::to_string);
entries.push((
"reasoning effort",
reasoning_effort.unwrap_or_else(|| "none".to_string()),