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

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

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

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

No files matched your search

+2 -9
View File
@@ -1971,15 +1971,8 @@
"type": "string" "type": "string"
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"ReasoningItemContent": { "ReasoningItemContent": {
@@ -2777,15 +2777,8 @@
"type": "string" "type": "string"
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"ReasoningSummary": { "ReasoningSummary": {
@@ -13525,15 +13525,8 @@
"type": "object" "type": "object"
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"ReasoningEffortOption": { "ReasoningEffortOption": {
@@ -10047,15 +10047,8 @@
"type": "object" "type": "object"
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"ReasoningEffortOption": { "ReasoningEffortOption": {
@@ -667,15 +667,8 @@
"type": "string" "type": "string"
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"ReasoningSummary": { "ReasoningSummary": {
@@ -462,15 +462,8 @@
] ]
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"TextElement": { "TextElement": {
@@ -462,15 +462,8 @@
] ]
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"TextElement": { "TextElement": {
@@ -185,15 +185,8 @@
"type": "object" "type": "object"
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"ReasoningEffortOption": { "ReasoningEffortOption": {
@@ -606,15 +606,8 @@
] ]
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"TextElement": { "TextElement": {
@@ -718,15 +718,8 @@
] ]
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"SandboxPolicy": { "SandboxPolicy": {
@@ -632,15 +632,8 @@
] ]
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"SessionSource": { "SessionSource": {
@@ -632,15 +632,8 @@
] ]
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"SessionSource": { "SessionSource": {
@@ -632,15 +632,8 @@
] ]
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"SessionSource": { "SessionSource": {
@@ -718,15 +718,8 @@
] ]
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"SandboxPolicy": { "SandboxPolicy": {
@@ -632,15 +632,8 @@
] ]
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"SessionSource": { "SessionSource": {
@@ -124,15 +124,8 @@
"type": "string" "type": "string"
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"ReasoningSummary": { "ReasoningSummary": {
@@ -718,15 +718,8 @@
] ]
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"SandboxPolicy": { "SandboxPolicy": {
@@ -632,15 +632,8 @@
] ]
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"SessionSource": { "SessionSource": {
@@ -632,15 +632,8 @@
] ]
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"SessionSource": { "SessionSource": {
@@ -606,15 +606,8 @@
] ]
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"TextElement": { "TextElement": {
@@ -154,15 +154,8 @@
"type": "string" "type": "string"
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"ReasoningSummary": { "ReasoningSummary": {
@@ -606,15 +606,8 @@
] ]
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"TextElement": { "TextElement": {
@@ -606,15 +606,8 @@
] ]
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"TextElement": { "TextElement": {
@@ -5,4 +5,4 @@
/** /**
* See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning * 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(), receiver_thread_ids: Vec::new(),
prompt: Some(payload.prompt.clone()), prompt: Some(payload.prompt.clone()),
model: Some(payload.model.clone()), model: Some(payload.model.clone()),
reasoning_effort: Some(payload.reasoning_effort), reasoning_effort: Some(payload.reasoning_effort.clone()),
agents_states: HashMap::new(), agents_states: HashMap::new(),
}; };
self.upsert_item_in_current_turn(item); self.upsert_item_in_current_turn(item);
@@ -656,7 +656,7 @@ impl ThreadHistoryBuilder {
receiver_thread_ids, receiver_thread_ids,
prompt: Some(payload.prompt.clone()), prompt: Some(payload.prompt.clone()),
model: Some(payload.model.clone()), model: Some(payload.model.clone()),
reasoning_effort: Some(payload.reasoning_effort), reasoning_effort: Some(payload.reasoning_effort.clone()),
agents_states, 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/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/unwatch` — stop sending notifications for a prior `fs/watch`; returns `{}`.
- `fs/changed` — notification emitted when watched paths change, including the `watchId` and `changedPaths`. - `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. - `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`. - `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. - `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>, efforts: Vec<ReasoningEffortPreset>,
) -> Vec<ReasoningEffortOption> { ) -> Vec<ReasoningEffortOption> {
efforts efforts
.iter() .into_iter()
.map(|preset| ReasoningEffortOption { .map(|preset| ReasoningEffortOption {
reasoning_effort: preset.effort, reasoning_effort: preset.effort,
description: preset.description.to_string(), description: preset.description,
}) })
.collect() .collect()
} }
@@ -158,7 +158,7 @@ fn merge_persisted_resume_metadata(
typesafe_overrides.model = persisted_metadata.model.clone(); typesafe_overrides.model = persisted_metadata.model.clone();
typesafe_overrides.model_provider = Some(persisted_metadata.model_provider.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( request_overrides.get_or_insert_with(HashMap::new).insert(
"model_reasoning_effort".to_string(), "model_reasoning_effort".to_string(),
serde_json::Value::String(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: config_snapshot.model.clone(),
model_provider: config_snapshot.model_provider_id.clone(), model_provider: config_snapshot.model_provider_id.clone(),
service_tier: config_snapshot.service_tier.clone(), service_tier: config_snapshot.service_tier.clone(),
effort: config_snapshot.reasoning_effort, effort: config_snapshot.reasoning_effort.clone(),
summary: config_snapshot.reasoning_summary, summary: config_snapshot.reasoning_summary,
collaboration_mode: config_snapshot.collaboration_mode.clone(), collaboration_mode: config_snapshot.collaboration_mode.clone(),
personality: config_snapshot.personality, personality: config_snapshot.personality,
@@ -629,7 +629,7 @@ impl TurnRequestProcessor {
profile_workspace_roots: profile_workspace_roots.clone(), profile_workspace_roots: profile_workspace_roots.clone(),
windows_sandbox_level: None, windows_sandbox_level: None,
model: model.clone(), model: model.clone(),
effort, effort: effort.clone(),
summary, summary,
service_tier: service_tier.clone(), service_tier: service_tier.clone(),
collaboration_mode: collaboration_mode.clone(), collaboration_mode: collaboration_mode.clone(),
@@ -18,7 +18,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
slug: preset.id.clone(), slug: preset.id.clone(),
display_name: preset.display_name.clone(), display_name: preset.display_name.clone(),
description: Some(preset.description.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(), supported_reasoning_levels: preset.supported_reasoning_efforts.clone(),
shell_type: ConfigShellToolType::ShellCommand, shell_type: ConfigShellToolType::ShellCommand,
visibility: if preset.show_in_picker { visibility: if preset.show_in_picker {
@@ -48,11 +48,11 @@ fn model_from_preset(preset: &ModelPreset) -> Model {
.supported_reasoning_efforts .supported_reasoning_efforts
.iter() .iter()
.map(|preset| ReasoningEffortOption { .map(|preset| ReasoningEffortOption {
reasoning_effort: preset.effort, reasoning_effort: preset.effort.clone(),
description: preset.description.clone(), description: preset.description.clone(),
}) })
.collect(), .collect(),
default_reasoning_effort: preset.default_reasoning_effort, default_reasoning_effort: preset.default_reasoning_effort.clone(),
input_modalities: preset.input_modalities.clone(), input_modalities: preset.input_modalities.clone(),
// `write_models_cache()` round-trips through a simplified ModelInfo fixture that does not // `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 // 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", "slug": "chatgpt-remote-only",
"display_name": "ChatGPT Remote Only", "display_name": "ChatGPT Remote Only",
"description": "Remote-only model for app-server model/list coverage", "description": "Remote-only model for app-server model/list coverage",
"default_reasoning_level": "medium", "default_reasoning_level": "max",
"supported_reasoning_levels": [ "supported_reasoning_levels": [
{"effort": "low", "description": "low"}, {"effort": "low", "description": "low"},
{"effort": "medium", "description": "medium"} {"effort": "max", "description": "Maximum"}
], ],
"shell_type": "shell_command", "shell_type": "shell_command",
"visibility": "list", "visibility": "list",
+2 -9
View File
@@ -2503,15 +2503,8 @@
"type": "string" "type": "string"
}, },
"ReasoningEffort": { "ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "description": "A non-empty reasoning effort value advertised by the model.",
"enum": [ "minLength": 1,
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"type": "string" "type": "string"
}, },
"ReasoningSummary": { "ReasoningSummary": {
+4 -4
View File
@@ -721,7 +721,7 @@ impl ModelClient {
) -> Option<Reasoning> { ) -> Option<Reasoning> {
if model_info.supports_reasoning_summaries { if model_info.supports_reasoning_summaries {
Some(Reasoning { 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 { summary: if summary == ReasoningSummaryConfig::None {
None None
} else { } else {
@@ -1267,7 +1267,7 @@ impl ModelClientSession {
&client_setup.api_provider, &client_setup.api_provider,
prompt, prompt,
model_info, model_info,
effort, effort.clone(),
summary, summary,
service_tier.clone(), service_tier.clone(),
)?; )?;
@@ -1376,7 +1376,7 @@ impl ModelClientSession {
&client_setup.api_provider, &client_setup.api_provider,
prompt, prompt,
model_info, model_info,
effort, effort.clone(),
summary, summary,
service_tier.clone(), service_tier.clone(),
)?; )?;
@@ -1595,7 +1595,7 @@ impl ModelClientSession {
prompt, prompt,
model_info, model_info,
session_telemetry, session_telemetry,
effort, effort.clone(),
summary, summary,
service_tier.clone(), service_tier.clone(),
turn_metadata_header, turn_metadata_header,
+1 -1
View File
@@ -573,7 +573,7 @@ async fn drain_to_completed(
prompt, prompt,
&turn_context.model_info, &turn_context.model_info,
&turn_context.session_telemetry, &turn_context.session_telemetry,
turn_context.reasoning_effort, turn_context.reasoning_effort.clone(),
turn_context.reasoning_summary, turn_context.reasoning_summary,
turn_context.config.service_tier.clone(), turn_context.config.service_tier.clone(),
turn_metadata_header, turn_metadata_header,
+1 -1
View File
@@ -242,7 +242,7 @@ async fn run_remote_compact_task_inner_impl(
&prompt, &prompt,
&turn_context.model_info, &turn_context.model_info,
CompactConversationRequestSettings { CompactConversationRequestSettings {
effort: turn_context.reasoning_effort, effort: turn_context.reasoning_effort.clone(),
summary: turn_context.reasoning_summary, summary: turn_context.reasoning_summary,
service_tier: if sess.services.auth_manager.auth_mode() == Some(AuthMode::ApiKey) { service_tier: if sess.services.auth_manager.auth_mode() == Some(AuthMode::ApiKey) {
None None
+1 -1
View File
@@ -339,7 +339,7 @@ async fn run_remote_compaction_request_v2(
prompt, prompt,
&turn_context.model_info, &turn_context.model_info,
&turn_context.session_telemetry, &turn_context.session_telemetry,
turn_context.reasoning_effort, turn_context.reasoning_effort.clone(),
turn_context.reasoning_summary, turn_context.reasoning_summary,
turn_context.config.service_tier.clone(), turn_context.config.service_tier.clone(),
turn_metadata_header, turn_metadata_header,
+1 -1
View File
@@ -225,7 +225,7 @@ impl ConfigDocument {
); );
mutated |= self.write_optional_value( mutated |= self.write_optional_value(
&["model_reasoning_effort"], &["model_reasoning_effort"],
effort.map(|effort| value(effort.to_string())), effort.as_ref().map(|effort| value(effort.to_string())),
); );
mutated mutated
}), }),
+4 -3
View File
@@ -702,7 +702,7 @@ pub(super) async fn run_guardian_review_session(
.supported_reasoning_efforts .supported_reasoning_efforts
.iter() .iter()
.any(|effort| effort.effort == codex_protocol::openai_models::ReasoningEffort::Low), .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) (review_model_id.to_string(), reasoning_effort)
} else { } else {
@@ -712,7 +712,8 @@ pub(super) async fn run_guardian_review_session(
.iter() .iter()
.any(|preset| preset.effort == codex_protocol::openai_models::ReasoningEffort::Low), .any(|preset| preset.effort == codex_protocol::openai_models::ReasoningEffort::Low),
turn.reasoning_effort turn.reasoning_effort
.or(turn.model_info.default_reasoning_level), .clone()
.or_else(|| turn.model_info.default_reasoning_level.clone()),
); );
( (
model_override model_override
@@ -725,7 +726,7 @@ pub(super) async fn run_guardian_review_session(
turn.config.as_ref(), turn.config.as_ref(),
live_network_config.clone(), live_network_config.clone(),
guardian_model.as_str(), guardian_model.as_str(),
guardian_reasoning_effort, guardian_reasoning_effort.clone(),
); );
let guardian_config = match guardian_config { let guardian_config = match guardian_config {
Ok(config) => config, Ok(config) => config,
+6 -5
View File
@@ -167,7 +167,7 @@ impl GuardianReviewSessionReuseKey {
model_context_window: spawn_config.model_context_window, 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: spawn_config.model_auto_compact_token_limit,
model_auto_compact_token_limit_scope: spawn_config.model_auto_compact_token_limit_scope, 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, model_reasoning_summary: spawn_config.model_reasoning_summary,
permissions: spawn_config.permissions.clone(), permissions: spawn_config.permissions.clone(),
developer_instructions: spawn_config.developer_instructions.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 { let guardian_reasoning_effort = if model_info.supports_reasoning_summaries {
params params
.reasoning_effort .reasoning_effort
.or(model_info.default_reasoning_level) .clone()
.or_else(|| model_info.default_reasoning_level.clone())
} else { } else {
None None
}; };
@@ -742,7 +743,7 @@ async fn run_review_on_session(
mode: codex_protocol::config_types::ModeKind::Default, mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: codex_protocol::config_types::Settings {
model: params.model.clone(), model: params.model.clone(),
reasoning_effort: params.reasoning_effort, reasoning_effort: params.reasoning_effort.clone(),
developer_instructions: None, developer_instructions: None,
}, },
}), }),
@@ -1107,7 +1108,7 @@ mod tests {
async fn test_review_params() -> GuardianReviewSessionParams { async fn test_review_params() -> GuardianReviewSessionParams {
let (session, turn) = crate::session::tests::make_session_and_context().await; let (session, turn) = crate::session::tests::make_session_and_context().await;
let model = turn.model_info.slug.clone(); 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 reasoning_summary = turn.reasoning_summary;
let personality = turn.personality; let personality = turn.personality;
#[allow(deprecated)] #[allow(deprecated)]
@@ -1116,7 +1117,7 @@ mod tests {
turn.config.as_ref(), turn.config.as_ref(),
/*live_network_config*/ None, /*live_network_config*/ None,
model.as_str(), model.as_str(),
reasoning_effort, reasoning_effort.clone(),
) )
.expect("guardian config"); .expect("guardian config");
+1 -1
View File
@@ -129,7 +129,7 @@ fn save_config_resolved_fields(
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
lock_config.web_search = Some(config.web_search_mode.value()); lock_config.web_search = Some(config.web_search_mode.value());
lock_config.model_provider = Some(config.model_provider_id.clone()); 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.model_verbosity = config.model_verbosity;
lock_config.include_permissions_instructions = Some(config.include_permissions_instructions); lock_config.include_permissions_instructions = Some(config.include_permissions_instructions);
lock_config.include_apps_instructions = Some(config.include_apps_instructions); lock_config.include_apps_instructions = Some(config.include_apps_instructions);
+1 -1
View File
@@ -579,7 +579,7 @@ impl Codex {
mode: ModeKind::Default, mode: ModeKind::Default,
settings: Settings { settings: Settings {
model: model.clone(), model: model.clone(),
reasoning_effort: config.model_reasoning_effort, reasoning_effort: config.model_reasoning_effort.clone(),
developer_instructions: None, 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 auth_manager_for_context = auth_manager.clone();
let provider_for_context = provider.clone(); let provider_for_context = provider.clone();
let session_telemetry_for_context = session_telemetry.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 let reasoning_summary = per_turn_config
.model_reasoning_summary .model_reasoning_summary
.unwrap_or(model_info.default_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()), collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None, multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active), 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, summary: codex_protocol::config_types::ReasoningSummary::Auto,
}; };
let rollout_items = vec![RolloutItem::TurnContext(previous_context_item)]; 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()), collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None, multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active), 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, summary: codex_protocol::config_types::ReasoningSummary::Auto,
}; };
let turn_id = previous_context_item 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()), collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None, multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active), 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, summary: codex_protocol::config_types::ReasoningSummary::Auto,
}; };
let previous_turn_id = previous_context_item 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()), collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None, multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active), 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, summary: codex_protocol::config_types::ReasoningSummary::Auto,
})) }))
.expect("serialize expected reference context item") .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()), collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None, multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active), 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, summary: codex_protocol::config_types::ReasoningSummary::Auto,
}; };
let previous_turn_id = previous_context_item 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()), collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None, multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active), 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, 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()), collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None, multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active), 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, summary: codex_protocol::config_types::ReasoningSummary::Auto,
}; };
let previous_turn_id = previous_context_item 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()), collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None, multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active), 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, summary: codex_protocol::config_types::ReasoningSummary::Auto,
}; };
let previous_turn_id = previous_context_item 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()), collaboration_mode: Some(turn_context.collaboration_mode.clone()),
multi_agent_version: None, multi_agent_version: None,
realtime_active: Some(turn_context.realtime_active), 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, summary: codex_protocol::config_types::ReasoningSummary::Auto,
}; };
let turn_id = previous_context_item 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 = get_model_offline_for_tests(config.model.as_deref());
let model_info = let model_info =
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config()); 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 { let collaboration_mode = CollaborationMode {
mode: ModeKind::Default, mode: ModeKind::Default,
settings: Settings { 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 = get_model_offline_for_tests(config.model.as_deref());
let model_info = let model_info =
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config()); 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 { let collaboration_mode = CollaborationMode {
mode: ModeKind::Default, mode: ModeKind::Default,
settings: Settings { 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 = get_model_offline_for_tests(config.model.as_deref());
let model_info = let model_info =
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config()); 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 { let collaboration_mode = CollaborationMode {
mode: ModeKind::Default, mode: ModeKind::Default,
settings: Settings { settings: Settings {
@@ -4614,7 +4614,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
mode: ModeKind::Default, mode: ModeKind::Default,
settings: Settings { settings: Settings {
model, model,
reasoning_effort: config.model_reasoning_effort, reasoning_effort: config.model_reasoning_effort.clone(),
developer_instructions: None, 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 = get_model_offline_for_tests(config.model.as_deref());
let model_info = let model_info =
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config()); 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 { let collaboration_mode = CollaborationMode {
mode: ModeKind::Default, mode: ModeKind::Default,
settings: Settings { settings: Settings {
@@ -4959,7 +4959,7 @@ async fn make_session_with_config_and_rx(
mode: ModeKind::Default, mode: ModeKind::Default,
settings: Settings { settings: Settings {
model, model,
reasoning_effort: config.model_reasoning_effort, reasoning_effort: config.model_reasoning_effort.clone(),
developer_instructions: None, developer_instructions: None,
}, },
}; };
@@ -5064,7 +5064,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
mode: ModeKind::Default, mode: ModeKind::Default,
settings: Settings { settings: Settings {
model, model,
reasoning_effort: config.model_reasoning_effort, reasoning_effort: config.model_reasoning_effort.clone(),
developer_instructions: None, developer_instructions: None,
}, },
}; };
@@ -5999,7 +5999,7 @@ async fn user_turn_updates_approvals_reviewer() {
mode: codex_protocol::config_types::ModeKind::Default, mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: codex_protocol::config_types::Settings {
model: turn_context.model_info.slug.clone(), model: turn_context.model_info.slug.clone(),
reasoning_effort: config.model_reasoning_effort, reasoning_effort: config.model_reasoning_effort.clone(),
developer_instructions: None, developer_instructions: None,
}, },
}), }),
@@ -6806,7 +6806,7 @@ where
let model = get_model_offline_for_tests(config.model.as_deref()); let model = get_model_offline_for_tests(config.model.as_deref());
let model_info = let model_info =
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config()); 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 { let collaboration_mode = CollaborationMode {
mode: ModeKind::Default, mode: ModeKind::Default,
settings: Settings { settings: Settings {
+2 -2
View File
@@ -698,7 +698,7 @@ async fn track_turn_resolved_config_analytics(
permission_profile: turn_context.permission_profile(), permission_profile: turn_context.permission_profile(),
#[allow(deprecated)] #[allow(deprecated)]
permission_profile_cwd: turn_context.cwd.to_path_buf(), 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), reasoning_summary: Some(turn_context.reasoning_summary),
service_tier: turn_context service_tier: turn_context
.config .config
@@ -1799,7 +1799,7 @@ async fn try_run_sampling_request(
prompt, prompt,
&turn_context.model_info, &turn_context.model_info,
&turn_context.session_telemetry, &turn_context.session_telemetry,
turn_context.reasoning_effort, turn_context.reasoning_effort.clone(),
turn_context.reasoning_summary, turn_context.reasoning_summary,
turn_context.config.service_tier.clone(), turn_context.config.service_tier.clone(),
turn_metadata_header, turn_metadata_header,
+12 -10
View File
@@ -136,7 +136,8 @@ impl TurnContext {
pub(crate) fn effective_reasoning_effort(&self) -> Option<ReasoningEffortConfig> { pub(crate) fn effective_reasoning_effort(&self) -> Option<ReasoningEffortConfig> {
if self.model_info.supports_reasoning_summaries { if self.model_info.supports_reasoning_summaries {
self.reasoning_effort self.reasoning_effort
.or(self.model_info.default_reasoning_level) .clone()
.or_else(|| self.model_info.default_reasoning_level.clone())
} else { } else {
None None
} }
@@ -196,28 +197,29 @@ impl TurnContext {
let supported_reasoning_levels = model_info let supported_reasoning_levels = model_info
.supported_reasoning_levels .supported_reasoning_levels
.iter() .iter()
.map(|preset| preset.effort) .map(|preset| preset.effort.clone())
.collect::<Vec<_>>(); .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) { if supported_reasoning_levels.contains(&current_reasoning_effort) {
Some(current_reasoning_effort) Some(current_reasoning_effort)
} else { } else {
supported_reasoning_levels supported_reasoning_levels
.get(supported_reasoning_levels.len().saturating_sub(1) / 2) .get(supported_reasoning_levels.len().saturating_sub(1) / 2)
.copied() .cloned()
.or(model_info.default_reasoning_level) .or_else(|| model_info.default_reasoning_level.clone())
} }
} else { } else {
supported_reasoning_levels supported_reasoning_levels
.get(supported_reasoning_levels.len().saturating_sub(1) / 2) .get(supported_reasoning_levels.len().saturating_sub(1) / 2)
.copied() .cloned()
.or(model_info.default_reasoning_level) .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( let collaboration_mode = self.collaboration_mode.with_updates(
Some(model.clone()), Some(model.clone()),
Some(reasoning_effort), Some(reasoning_effort.clone()),
/*developer_instructions*/ None, /*developer_instructions*/ None,
); );
let features = self.features.clone(); let features = self.features.clone();
@@ -363,7 +365,7 @@ impl TurnContext {
collaboration_mode: Some(self.collaboration_mode.clone()), collaboration_mode: Some(self.collaboration_mode.clone()),
multi_agent_version: Some(self.multi_agent_version), multi_agent_version: Some(self.multi_agent_version),
realtime_active: Some(self.realtime_active), realtime_active: Some(self.realtime_active),
effort: self.reasoning_effort, effort: self.reasoning_effort.clone(),
summary: ReasoningSummaryConfig::Auto, summary: ReasoningSummaryConfig::Auto,
} }
} }
+1 -1
View File
@@ -271,7 +271,7 @@ async fn schedule_startup_prewarm_inner(
&startup_prompt, &startup_prompt,
&startup_turn_context.model_info, &startup_turn_context.model_info,
&startup_turn_context.session_telemetry, &startup_turn_context.session_telemetry,
startup_turn_context.reasoning_effort, startup_turn_context.reasoning_effort.clone(),
startup_turn_context.reasoning_summary, startup_turn_context.reasoning_summary,
startup_turn_context.config.service_tier.clone(), startup_turn_context.config.service_tier.clone(),
startup_turn_metadata_header.as_deref(), startup_turn_metadata_header.as_deref(),
@@ -83,7 +83,7 @@ async fn handle_spawn_agent(
sender_thread_id: session.thread_id, sender_thread_id: session.thread_id,
prompt: prompt.clone(), prompt: prompt.clone(),
model: args.model.clone().unwrap_or_default(), 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(), .into(),
) )
@@ -94,14 +94,18 @@ async fn handle_spawn_agent(
config.service_tier = Some(service_tier.clone()); config.service_tier = Some(service_tier.clone());
} }
if args.fork_context { 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 { } else {
apply_requested_spawn_agent_model_overrides( apply_requested_spawn_agent_model_overrides(
&session, &session,
turn.as_ref(), turn.as_ref(),
&mut config, &mut config,
args.model.as_deref(), args.model.as_deref(),
args.reasoning_effort, args.reasoning_effort.clone(),
) )
.await?; .await?;
apply_role_to_config(&mut config, role_name) 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()); .unwrap_or_else(|| args.model.clone().unwrap_or_default());
let effective_reasoning_effort = agent_snapshot let effective_reasoning_effort = agent_snapshot
.as_ref() .as_ref()
.and_then(|snapshot| snapshot.reasoning_effort) .and_then(|snapshot| snapshot.reasoning_effort.clone())
.unwrap_or(args.reasoning_effort.unwrap_or_default()); .unwrap_or(args.reasoning_effort.unwrap_or_default());
let nickname = new_agent_nickname.clone(); let nickname = new_agent_nickname.clone();
session session
@@ -224,7 +224,8 @@ fn build_agent_shared_config(turn: &TurnContext) -> Result<Config, FunctionCallE
config.model_provider = turn.provider.info().clone(); config.model_provider = turn.provider.info().clone();
config.model_reasoning_effort = turn config.model_reasoning_effort = turn
.reasoning_effort .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.model_reasoning_summary = Some(turn.reasoning_summary);
config.developer_instructions = turn.developer_instructions.clone(); config.developer_instructions = turn.developer_instructions.clone();
config.compact_prompt = turn.compact_prompt.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( validate_spawn_agent_reasoning_effort(
&selected_model_name, &selected_model_name,
&selected_model_info.supported_reasoning_levels, &selected_model_info.supported_reasoning_levels,
reasoning_effort, &reasoning_effort,
)?; )?;
config.model_reasoning_effort = Some(reasoning_effort); config.model_reasoning_effort = Some(reasoning_effort);
} else { } else {
@@ -318,7 +319,7 @@ pub(crate) async fn apply_requested_spawn_agent_model_overrides(
validate_spawn_agent_reasoning_effort( validate_spawn_agent_reasoning_effort(
&turn.model_info.slug, &turn.model_info.slug,
&turn.model_info.supported_reasoning_levels, &turn.model_info.supported_reasoning_levels,
reasoning_effort, &reasoning_effort,
)?; )?;
config.model_reasoning_effort = Some(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( fn validate_spawn_agent_reasoning_effort(
model: &str, model: &str,
supported_reasoning_levels: &[ReasoningEffortPreset], supported_reasoning_levels: &[ReasoningEffortPreset],
requested_reasoning_effort: ReasoningEffort, requested_reasoning_effort: &ReasoningEffort,
) -> Result<(), FunctionCallError> { ) -> Result<(), FunctionCallError> {
if supported_reasoning_levels if supported_reasoning_levels
.iter() .iter()
.any(|preset| preset.effort == requested_reasoning_effort) .any(|preset| &preset.effort == requested_reasoning_effort)
{ {
return Ok(()); return Ok(());
} }
@@ -17,6 +17,7 @@ const SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION: &str =
const SPAWN_AGENT_SERVICE_TIER_OVERRIDE_DESCRIPTION: &str = const SPAWN_AGENT_SERVICE_TIER_OVERRIDE_DESCRIPTION: &str =
"Service tier override for the new agent. Omit unless explicitly requested."; "Service tier override for the new agent. Omit unless explicitly requested.";
const MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT_DESCRIPTION: usize = 5; 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)] #[derive(Debug, Clone, Default)]
pub struct SpawnAgentToolOptions { pub struct SpawnAgentToolOptions {
@@ -762,13 +763,20 @@ fn spawn_agent_models_description(models: &[ModelPreset]) -> String {
let model_descriptions = visible_models let model_descriptions = visible_models
.into_iter() .into_iter()
.map(|model| { .map(|model| {
let default_reasoning_effort = model.default_reasoning_effort; let default_reasoning_effort = &model.default_reasoning_effort;
let efforts = model let efforts = model
.supported_reasoning_efforts .supported_reasoning_efforts
.iter() .iter()
.map(|preset| { .map(|preset| {
let effort = preset.effort; let effort = preset.effort.as_str();
if effort == default_reasoning_effort { 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)") format!("{effort} (default)")
} else { } else {
effort.to_string() effort.to_string()
@@ -186,6 +186,27 @@ fn spawn_agent_tool_caps_visible_model_summaries() {
assert!(!description.contains("`sixth-model`")); 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] #[test]
fn spawn_agent_tool_hides_service_tier_with_spawn_metadata() { fn spawn_agent_tool_hides_service_tier_with_spawn_metadata() {
let tool = create_spawn_agent_tool_v2(SpawnAgentToolOptions { 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.base_instructions = Some(base_instructions.text);
expected.model = Some(turn.model_info.slug.clone()); expected.model = Some(turn.model_info.slug.clone());
expected.model_provider = turn.provider.info().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.model_reasoning_summary = Some(turn.reasoning_summary);
expected.developer_instructions = turn.developer_instructions.clone(); expected.developer_instructions = turn.developer_instructions.clone();
expected.compact_prompt = turn.compact_prompt.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.base_instructions = None;
expected.model = Some(turn.model_info.slug.clone()); expected.model = Some(turn.model_info.slug.clone());
expected.model_provider = turn.provider.info().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.model_reasoning_summary = Some(turn.reasoning_summary);
expected.developer_instructions = turn.developer_instructions.clone(); expected.developer_instructions = turn.developer_instructions.clone();
expected.compact_prompt = turn.compact_prompt.clone(); expected.compact_prompt = turn.compact_prompt.clone();
@@ -75,7 +75,7 @@ async fn handle_spawn_agent(
sender_thread_id: session.thread_id, sender_thread_id: session.thread_id,
prompt: prompt.clone(), prompt: prompt.clone(),
model: args.model.clone().unwrap_or_default(), 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(), .into(),
) )
@@ -86,14 +86,18 @@ async fn handle_spawn_agent(
config.service_tier = Some(service_tier.clone()); config.service_tier = Some(service_tier.clone());
} }
if matches!(fork_mode, Some(SpawnAgentForkMode::FullHistory)) { 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 { } else {
apply_requested_spawn_agent_model_overrides( apply_requested_spawn_agent_model_overrides(
&session, &session,
turn.as_ref(), turn.as_ref(),
&mut config, &mut config,
args.model.as_deref(), args.model.as_deref(),
args.reasoning_effort, args.reasoning_effort.clone(),
) )
.await?; .await?;
apply_role_to_config(&mut config, role_name) 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()); .unwrap_or_else(|| args.model.clone().unwrap_or_default());
let effective_reasoning_effort = agent_snapshot let effective_reasoning_effort = agent_snapshot
.as_ref() .as_ref()
.and_then(|snapshot| snapshot.reasoning_effort) .and_then(|snapshot| snapshot.reasoning_effort.clone())
.unwrap_or(args.reasoning_effort.unwrap_or_default()); .unwrap_or(args.reasoning_effort.unwrap_or_default());
let nickname = new_agent_nickname.clone(); let nickname = new_agent_nickname.clone();
session 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; let mut config = load_default_config_for_test(&codex_home).await;
config.model_provider_id = provider.name.clone(); config.model_provider_id = provider.name.clone();
config.model_provider = provider.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 summary = config.model_reasoning_summary;
let model = codex_core::test_support::get_model_offline(config.model.as_deref()); let model = codex_core::test_support::get_model_offline(config.model.as_deref());
config.model = Some(model.clone()); 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; let mut config = load_default_config_for_test(&codex_home).await;
config.model_provider_id = provider.name.clone(); config.model_provider_id = provider.name.clone();
config.model_provider = provider.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 summary = config.model_reasoning_summary;
let model = codex_core::test_support::get_model_offline(config.model.as_deref()); let model = codex_core::test_support::get_model_offline(config.model.as_deref());
config.model = Some(model.clone()); 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_provider = provider.clone();
config.model_supports_reasoning_summaries = Some(true); config.model_supports_reasoning_summaries = Some(true);
config.model_reasoning_summary = Some(ReasoningSummary::Detailed); 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 summary = config.model_reasoning_summary;
let model = config.model.clone().expect("model configured"); let model = config.model.clone().expect("model configured");
let config = Arc::new(config); 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; let mut config = load_default_config_for_test(&codex_home).await;
config.model_provider_id = provider.name.clone(); config.model_provider_id = provider.name.clone();
config.model_provider = provider.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 summary = config.model_reasoning_summary;
let model = codex_core::test_support::get_model_offline(config.model.as_deref()); let model = codex_core::test_support::get_model_offline(config.model.as_deref());
config.model = Some(model.clone()); 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; let mut config = load_default_config_for_test(&codex_home).await;
config.model_provider_id = provider.name.clone(); config.model_provider_id = provider.name.clone();
config.model_provider = provider.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 summary = config.model_reasoning_summary;
let model = codex_core::test_support::get_model_offline(config.model.as_deref()); let model = codex_core::test_support::get_model_offline(config.model.as_deref());
config.model = Some(model.clone()); config.model = Some(model.clone());
+12 -12
View File
@@ -343,7 +343,7 @@ async fn responses_websocket_request_prewarm_reuses_connection() {
&prompt, &prompt,
&harness.model_info, &harness.model_info,
&harness.session_telemetry, &harness.session_telemetry,
harness.effort, harness.effort.clone(),
harness.summary, harness.summary,
/*service_tier*/ None, /*service_tier*/ None,
/*turn_metadata_header*/ None, /*turn_metadata_header*/ None,
@@ -397,7 +397,7 @@ async fn responses_websocket_request_prewarm_traces_logical_request() {
&prompt, &prompt,
&harness.model_info, &harness.model_info,
&harness.session_telemetry, &harness.session_telemetry,
harness.effort, harness.effort.clone(),
harness.summary, harness.summary,
/*service_tier*/ None, /*service_tier*/ None,
/*turn_metadata_header*/ None, /*turn_metadata_header*/ None,
@@ -442,7 +442,7 @@ async fn responses_websocket_request_prewarm_traces_logical_request() {
&prompt, &prompt,
&harness.model_info, &harness.model_info,
&harness.session_telemetry, &harness.session_telemetry,
harness.effort, harness.effort.clone(),
harness.summary, harness.summary,
/*service_tier*/ None, /*service_tier*/ None,
/*turn_metadata_header*/ None, /*turn_metadata_header*/ None,
@@ -534,7 +534,7 @@ async fn responses_websocket_preconnect_is_reused_even_with_header_changes() {
&prompt, &prompt,
&harness.model_info, &harness.model_info,
&harness.session_telemetry, &harness.session_telemetry,
harness.effort, harness.effort.clone(),
harness.summary, harness.summary,
/*service_tier*/ None, /*service_tier*/ None,
/*turn_metadata_header*/ None, /*turn_metadata_header*/ None,
@@ -573,7 +573,7 @@ async fn responses_websocket_request_prewarm_is_reused_even_with_header_changes(
&prompt, &prompt,
&harness.model_info, &harness.model_info,
&harness.session_telemetry, &harness.session_telemetry,
harness.effort, harness.effort.clone(),
harness.summary, harness.summary,
/*service_tier*/ None, /*service_tier*/ None,
/*turn_metadata_header*/ None, /*turn_metadata_header*/ None,
@@ -585,7 +585,7 @@ async fn responses_websocket_request_prewarm_is_reused_even_with_header_changes(
&prompt, &prompt,
&harness.model_info, &harness.model_info,
&harness.session_telemetry, &harness.session_telemetry,
harness.effort, harness.effort.clone(),
harness.summary, harness.summary,
/*service_tier*/ None, /*service_tier*/ None,
/*turn_metadata_header*/ None, /*turn_metadata_header*/ None,
@@ -639,7 +639,7 @@ async fn responses_websocket_prewarm_uses_v2_when_provider_supports_websockets()
&prompt, &prompt,
&harness.model_info, &harness.model_info,
&harness.session_telemetry, &harness.session_telemetry,
harness.effort, harness.effort.clone(),
harness.summary, harness.summary,
/*service_tier*/ None, /*service_tier*/ None,
/*turn_metadata_header*/ None, /*turn_metadata_header*/ None,
@@ -988,7 +988,7 @@ async fn responses_websocket_emits_reasoning_included_event() {
&prompt, &prompt,
&harness.model_info, &harness.model_info,
&harness.session_telemetry, &harness.session_telemetry,
harness.effort, harness.effort.clone(),
harness.summary, harness.summary,
/*service_tier*/ None, /*service_tier*/ None,
/*turn_metadata_header*/ None, /*turn_metadata_header*/ None,
@@ -1062,7 +1062,7 @@ async fn responses_websocket_emits_rate_limit_events() {
&prompt, &prompt,
&harness.model_info, &harness.model_info,
&harness.session_telemetry, &harness.session_telemetry,
harness.effort, harness.effort.clone(),
harness.summary, harness.summary,
/*service_tier*/ None, /*service_tier*/ None,
/*turn_metadata_header*/ None, /*turn_metadata_header*/ None,
@@ -1718,7 +1718,7 @@ async fn responses_websocket_v2_after_error_uses_full_create_without_previous_re
&prompt_two, &prompt_two,
&harness.model_info, &harness.model_info,
&harness.session_telemetry, &harness.session_telemetry,
harness.effort, harness.effort.clone(),
harness.summary, harness.summary,
/*service_tier*/ None, /*service_tier*/ None,
/*turn_metadata_header*/ None, /*turn_metadata_header*/ None,
@@ -1806,7 +1806,7 @@ async fn responses_websocket_v2_surfaces_terminal_error_without_close_handshake(
&prompt_two, &prompt_two,
&harness.model_info, &harness.model_info,
&harness.session_telemetry, &harness.session_telemetry,
harness.effort, harness.effort.clone(),
harness.summary, harness.summary,
/*service_tier*/ None, /*service_tier*/ None,
/*turn_metadata_header*/ None, /*turn_metadata_header*/ None,
@@ -2073,7 +2073,7 @@ async fn stream_until_complete_with_request_metadata(
prompt, prompt,
&harness.model_info, &harness.model_info,
&harness.session_telemetry, &harness.session_telemetry,
harness.effort, harness.effort.clone(),
harness.summary, harness.summary,
service_tier.map(|service_tier| service_tier.request_value().to_string()), service_tier.map(|service_tier| service_tier.request_value().to_string()),
turn_metadata_header, 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, mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: codex_protocol::config_types::Settings {
model, model,
reasoning_effort: test.config.model_reasoning_effort, reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None, developer_instructions: None,
}, },
}), }),
@@ -135,7 +135,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
mode: codex_protocol::config_types::ModeKind::Default, mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: codex_protocol::config_types::Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: test.config.model_reasoning_effort, reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None, developer_instructions: None,
}, },
}), }),
@@ -172,7 +172,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
mode: codex_protocol::config_types::ModeKind::Default, mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: codex_protocol::config_types::Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: test.config.model_reasoning_effort, reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None, 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, mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: codex_protocol::config_types::Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: test.config.model_reasoning_effort, reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None, 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, mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: codex_protocol::config_types::Settings {
model: test.session_configured.model.clone(), model: test.session_configured.model.clone(),
reasoning_effort: test.config.model_reasoning_effort, reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None, 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, mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: codex_protocol::config_types::Settings {
model: resumed.session_configured.model.clone(), model: resumed.session_configured.model.clone(),
reasoning_effort: resumed.config.model_reasoning_effort, reasoning_effort: resumed.config.model_reasoning_effort.clone(),
developer_instructions: None, 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, mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: codex_protocol::config_types::Settings {
model, model,
reasoning_effort: test.config.model_reasoning_effort, reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None, 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_approval_policy = config.permissions.approval_policy.value();
let default_sandbox_policy = &config.legacy_sandbox_policy(); let default_sandbox_policy = &config.legacy_sandbox_policy();
let default_model = session_configured.model; 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; let default_summary = config.model_reasoning_summary;
codex 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, mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: codex_protocol::config_types::Settings {
model: default_model.clone(), model: default_model.clone(),
reasoning_effort: default_effort, reasoning_effort: default_effort.clone(),
developer_instructions: None, 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_approval_policy = config.permissions.approval_policy.value();
let default_sandbox_policy = &config.legacy_sandbox_policy(); let default_sandbox_policy = &config.legacy_sandbox_policy();
let default_model = session_configured.model; 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; let default_summary = config.model_reasoning_summary;
codex 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)] #[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_no_network!(Ok(()));
skip_if_sandbox!(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, /*priority*/ 1_000,
TruncationPolicyConfig::bytes(/*limit*/ 10_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![ remote_model.supported_reasoning_levels = vec![
ReasoningEffortPreset { ReasoningEffortPreset {
effort: ReasoningEffort::Medium, effort: ReasoningEffort::Medium,
description: ReasoningEffort::Medium.to_string(), description: ReasoningEffort::Medium.to_string(),
}, },
ReasoningEffortPreset { ReasoningEffortPreset {
effort: ReasoningEffort::High, effort: custom_reasoning_effort.clone(),
description: ReasoningEffort::High.to_string(), description: custom_reasoning_effort.to_string(),
}, },
]; ];
remote_model.supports_reasoning_summaries = true; 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(|reasoning| reasoning.get("summary"))
.and_then(|value| value.as_str()); .and_then(|value| value.as_str());
assert_eq!(body["model"].as_str(), Some(requested_model)); 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")); assert_eq!(reasoning_summary, Some("detailed"));
Ok(()) Ok(())
+1 -1
View File
@@ -41,7 +41,7 @@ fn resume_history(
collaboration_mode: None, collaboration_mode: None,
multi_agent_version: None, multi_agent_version: None,
realtime_active: None, realtime_active: None,
effort: config.model_reasoning_effort, effort: config.model_reasoning_effort.clone(),
summary: config summary: config
.model_reasoning_summary .model_reasoning_summary
.unwrap_or(ReasoningSummary::Auto), .unwrap_or(ReasoningSummary::Auto),
@@ -55,7 +55,7 @@ fn disabled_text_turn(test: &TestCodex, text: &str) -> Op {
mode: codex_protocol::config_types::ModeKind::Default, mode: codex_protocol::config_types::ModeKind::Default,
settings: codex_protocol::config_types::Settings { settings: codex_protocol::config_types::Settings {
model: REQUESTED_MODEL.to_string(), model: REQUESTED_MODEL.to_string(),
reasoning_effort: test.config.model_reasoning_effort, reasoning_effort: test.config.model_reasoning_effort.clone(),
developer_instructions: None, developer_instructions: None,
}, },
}), }),
+1 -1
View File
@@ -68,7 +68,7 @@ Each response yields:
- `data` - ordered list of models. A model includes: - `data` - ordered list of models. A model includes:
- `id`, `model`, `displayName`, `description` - `id`, `model`, `displayName`, `description`
- `supportedReasoningEfforts` - array of objects with: - `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 - `description` - human-friendly label for the effort
- `defaultReasoningEffort` - suggested effort for the UI - `defaultReasoningEffort` - suggested effort for the UI
- `inputModalities` - accepted input types for the model - `inputModalities` - accepted input types for the model
@@ -446,7 +446,8 @@ fn config_summary_entries(
"reasoning effort", "reasoning effort",
config config
.model_reasoning_effort .model_reasoning_effort
.map(|effort| effort.to_string()) .as_ref()
.map(std::string::ToString::to_string)
.unwrap_or_else(|| "none".to_string()), .unwrap_or_else(|| "none".to_string()),
)); ));
entries.push(( 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_cwd = config.cwd.to_path_buf();
let default_approval_policy = config.permissions.approval_policy.value(); 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) { let (initial_operation, prompt_summary) = match (command.as_ref(), prompt, images) {
(Some(ExecCommand::Review(review_cli)), _, _) => { (Some(ExecCommand::Review(review_cli)), _, _) => {
@@ -1169,7 +1169,7 @@ fn session_configured_from_thread_start_response(
config.permissions.effective_permission_profile(), config.permissions.effective_permission_profile(),
response.active_permission_profile.clone().map(Into::into), response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(), 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(), config.permissions.effective_permission_profile(),
response.active_permission_profile.clone().map(Into::into), response.active_permission_profile.clone().map(Into::into),
response.cwd.clone(), response.cwd.clone(),
response.reasoning_effort, response.reasoning_effort.clone(),
) )
} }
+1 -1
View File
@@ -194,7 +194,7 @@ impl MemoryStartupContext {
prompt, prompt,
&context.model_info, &context.model_info,
&context.session_telemetry, &context.session_telemetry,
context.reasoning_effort, context.reasoning_effort.clone(),
context.reasoning_summary, context.reasoning_summary,
context.service_tier.clone(), context.service_tier.clone(),
context.turn_metadata_header.as_deref(), 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_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_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, 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, reasoning_summary = %reasoning_summary,
context_window = context_window, context_window = context_window,
auto_compact_token_limit = auto_compact_token_limit, auto_compact_token_limit = auto_compact_token_limit,
+2 -1
View File
@@ -59,4 +59,5 @@ tempfile = { workspace = true }
[package.metadata.cargo-shear] [package.metadata.cargo-shear]
# Required because: # Required because:
# `icu_provider`: contains a required `sync` feature for `icu_decimal` # `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> { 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. /// Updates the collaboration mode with new model and/or effort values.
@@ -654,7 +654,7 @@ impl CollaborationMode {
let settings = self.settings_ref(); let settings = self.settings_ref();
let updated_settings = Settings { let updated_settings = Settings {
model: model.unwrap_or_else(|| settings.model.clone()), 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 developer_instructions: developer_instructions
.unwrap_or_else(|| settings.developer_instructions.clone()), .unwrap_or_else(|| settings.developer_instructions.clone()),
}; };
@@ -676,7 +676,10 @@ impl CollaborationMode {
mode: mask.mode.unwrap_or(self.mode), mode: mask.mode.unwrap_or(self.mode),
settings: Settings { settings: Settings {
model: mask.model.clone().unwrap_or_else(|| settings.model.clone()), 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: mask
.developer_instructions .developer_instructions
.clone() .clone()
+182 -44
View File
@@ -4,14 +4,22 @@
//! are used to preserve compatibility when older payloads omit newly introduced attributes. //! are used to preserve compatibility when older payloads omit newly introduced attributes.
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use schemars::JsonSchema; 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::Deserialize;
use serde::Deserializer; use serde::Deserializer;
use serde::Serialize; use serde::Serialize;
use serde::Serializer;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use strum::IntoEnumIterator; use serde::de::Error;
use strum_macros::Display; use strum_macros::Display;
use strum_macros::EnumIter; use strum_macros::EnumIter;
use tracing::warn; use tracing::warn;
@@ -28,23 +36,8 @@ const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}";
pub const SPEED_TIER_FAST: &str = "fast"; pub const SPEED_TIER_FAST: &str = "fast";
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning /// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
#[derive( #[derive(Debug, Default, Clone, PartialEq, Eq, TS, Hash)]
Debug, #[ts(type = "string")]
Serialize,
Deserialize,
Default,
Clone,
Copy,
PartialEq,
Eq,
Display,
JsonSchema,
TS,
EnumIter,
Hash,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum ReasoningEffort { pub enum ReasoningEffort {
None, None,
Minimal, Minimal,
@@ -53,14 +46,113 @@ pub enum ReasoningEffort {
Medium, Medium,
High, High,
XHigh, 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 { impl FromStr for ReasoningEffort {
type Err = String; type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_value(serde_json::Value::String(s.to_string())) match s {
.map_err(|_| format!("invalid reasoning_effort: {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. // 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(); let mut map = HashMap::new();
for effort in ReasoningEffort::iter() { for effort in ReasoningEffort::known_values() {
let nearest = nearest_effort(effort, &supported); let nearest = nearest_effort(&effort, &supported);
map.insert(effort, nearest); map.insert(effort, nearest);
} }
Some(map) Some(map)
} }
fn effort_rank(effort: ReasoningEffort) -> i32 { fn nearest_effort(target: &ReasoningEffort, supported: &[ReasoningEffort]) -> ReasoningEffort {
match effort { let Some(target_rank) = target.known_rank() else {
ReasoningEffort::None => 0, return supported
ReasoningEffort::Minimal => 1, .iter()
ReasoningEffort::Low => 2, .find(|candidate| *candidate == target)
ReasoningEffort::Medium => 3, .unwrap_or(target)
ReasoningEffort::High => 4, .clone();
ReasoningEffort::XHigh => 5, };
}
}
fn nearest_effort(target: ReasoningEffort, supported: &[ReasoningEffort]) -> ReasoningEffort {
let target_rank = effort_rank(target);
supported supported
.iter() .iter()
.copied() .filter_map(|candidate| {
.min_by_key(|candidate| (effort_rank(*candidate) - target_rank).abs()) 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) .unwrap_or(target)
.clone()
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serde_json::from_str;
use serde_json::to_string;
fn test_model(spec: Option<ModelMessages>) -> ModelInfo { fn test_model(spec: Option<ModelMessages>) -> ModelInfo {
ModelInfo { ModelInfo {
@@ -663,16 +759,58 @@ mod tests {
} }
#[test] #[test]
fn reasoning_effort_from_str_accepts_known_values() { fn reasoning_effort_accepts_known_and_custom_values() {
assert_eq!("high".parse(), Ok(ReasoningEffort::High)); let custom = ReasoningEffort::Custom("max".to_string());
assert_eq!("minimal".parse(), Ok(ReasoningEffort::Minimal)); 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] #[test]
fn reasoning_effort_from_str_rejects_unknown_values() { fn reasoning_effort_rejects_empty_values() {
assert_eq!( assert_eq!(
"unsupported".parse::<ReasoningEffort>(), "".parse::<ReasoningEffort>(),
Err("invalid reasoning_effort: unsupported".to_string()) 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.cwd = turn_ctx.cwd.clone();
} }
metadata.model = Some(turn_ctx.model.clone()); metadata.model = Some(turn_ctx.model.clone());
metadata.reasoning_effort = turn_ctx.effort; metadata.reasoning_effort = turn_ctx.effort.clone();
metadata.sandbox_policy = metadata.sandbox_policy =
serde_json::to_string(&turn_ctx.permission_profile()).unwrap_or_default(); serde_json::to_string(&turn_ctx.permission_profile()).unwrap_or_default();
metadata.approval_mode = enum_to_string(&turn_ctx.approval_policy); metadata.approval_mode = enum_to_string(&turn_ctx.approval_policy);
+2 -2
View File
@@ -584,13 +584,13 @@ mod tests {
} }
#[test] #[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"))) let metadata = ThreadMetadata::try_from(thread_row(Some("future")))
.expect("thread metadata should parse"); .expect("thread metadata should parse");
assert_eq!( assert_eq!(
metadata, 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()) .and_then(|metadata| metadata.model_provider.clone())
.unwrap_or_else(|| "test".to_string()), .unwrap_or_else(|| "test".to_string()),
model: metadata.and_then(|metadata| metadata.model.clone()), 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 created_at: metadata
.and_then(|metadata| metadata.created_at) .and_then(|metadata| metadata.created_at)
.unwrap_or_else(Utc::now), .unwrap_or_else(Utc::now),
@@ -232,7 +232,7 @@ impl ThreadMetadataSync {
update.cwd = Some(turn_ctx.cwd.clone()); update.cwd = Some(turn_ctx.cwd.clone());
} }
update.model = Some(turn_ctx.model.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.approval_mode = Some(turn_ctx.approval_policy);
update.permission_profile = Some(turn_ctx.permission_profile()); 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); .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 { match reasoning_effort {
Some(ReasoningEffortConfig::Minimal) => "minimal", None | Some(ReasoningEffortConfig::None) => "default".to_string(),
Some(ReasoningEffortConfig::Low) => "low", Some(reasoning_effort) => reasoning_effort.as_str().to_string(),
Some(ReasoningEffortConfig::Medium) => "medium",
Some(ReasoningEffortConfig::High) => "high",
Some(ReasoningEffortConfig::XHigh) => "xhigh",
None | Some(ReasoningEffortConfig::None) => "default",
} }
} }
pub(super) fn reasoning_label_for( pub(super) fn reasoning_label_for(
model: &str, model: &str,
reasoning_effort: Option<ReasoningEffortConfig>, reasoning_effort: Option<&ReasoningEffortConfig>,
) -> Option<&'static str> { ) -> Option<String> {
(!model.starts_with("codex-auto-")).then(|| Self::reasoning_label(reasoning_effort)) (!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>) { 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. // 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. // 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); 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); self.chat_widget.on_connectors_loaded(result, is_final);
} }
AppEvent::UpdateReasoningEffort(effort) => { 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) self.sync_active_thread_reasoning_setting(app_server, effort)
.await; .await;
} }
@@ -1300,19 +1300,23 @@ impl App {
AppEvent::PersistModelSelection { model, effort } => { AppEvent::PersistModelSelection { model, effort } => {
match crate::config_update::write_config_batch( match crate::config_update::write_config_batch(
app_server.request_handle(), 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 .await
{ {
Ok(_) => { Ok(_) => {
let effort_label = effort 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()); .unwrap_or_else(|| "default".to_string());
tracing::info!("Selected model: {model}, Selected effort: {effort_label}"); tracing::info!("Selected model: {model}, Selected effort: {effort_label}");
let mut message = format!("Model changed to {model}"); 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(' ');
message.push_str(label); message.push_str(&label);
} }
self.chat_widget.add_info_message(message, /*hint*/ None); 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); self.chat_widget.set_rate_limit_switch_prompt_hidden(hidden);
} }
AppEvent::UpdatePlanModeReasoningEffort(effort) => { 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.chat_widget.set_plan_mode_reasoning_effort(effort);
self.sync_active_thread_plan_mode_reasoning_setting(app_server) self.sync_active_thread_plan_mode_reasoning_setting(app_server)
.await; .await;
+5 -3
View File
@@ -152,9 +152,11 @@ pub(super) fn apply_accepted_model_migration(
}); });
config.model = Some(target_model.clone()); 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::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 { app_event_tx.send(AppEvent::PersistModelSelection {
model: target_model, model: target_model,
effort: Some(target_default_effort), effort: Some(target_default_effort),
@@ -290,7 +292,7 @@ pub(super) async fn handle_model_migration_prompt_if_needed(
app_event_tx, app_event_tx,
model.to_string(), model.to_string(),
target_model.clone(), target_model.clone(),
target_preset.default_reasoning_effort, target_preset.default_reasoning_effort.clone(),
); );
} }
ModelMigrationOutcome::Rejected => { ModelMigrationOutcome::Rejected => {
+2 -2
View File
@@ -5241,7 +5241,7 @@ async fn override_turn_context_sends_thread_settings_update() {
.expect("thread/start should succeed"); .expect("thread/start should succeed");
let thread_id = started.session.thread_id; let thread_id = started.session.thread_id;
let initial_model = started.session.model.clone(); 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) app.enqueue_primary_thread_session(started.session, started.turns)
.await .await
.expect("primary thread should be registered"); .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: "gpt-plan".to_string(),
model_provider: "openai".to_string(), model_provider: "openai".to_string(),
service_tier: None, service_tier: None,
effort: collaboration_mode.settings.reasoning_effort, effort: collaboration_mode.settings.reasoning_effort.clone(),
summary: None, summary: None,
collaboration_mode: collaboration_mode.clone(), collaboration_mode: collaboration_mode.clone(),
personality: Some(Personality::Pragmatic), personality: Some(Personality::Pragmatic),
+1 -1
View File
@@ -606,7 +606,7 @@ impl App {
permissions_override, permissions_override,
config.permissions.user_visible_workspace_roots(), config.permissions.user_visible_workspace_roots(),
model.to_string(), model.to_string(),
*effort, effort.clone(),
*summary, *summary,
service_tier.clone(), service_tier.clone(),
collaboration_mode.clone(), collaboration_mode.clone(),
+3 -3
View File
@@ -124,7 +124,7 @@ impl App {
.as_ref() .as_ref()
.map(|profile| profile.id.clone()), .map(|profile| profile.id.clone()),
model: model.clone(), model: model.clone(),
effort: effort.unwrap_or_default(), effort: effort.clone().unwrap_or_default(),
summary: *summary, summary: *summary,
service_tier: service_tier.clone(), service_tier: service_tier.clone(),
collaboration_mode: collaboration_mode.clone(), collaboration_mode: collaboration_mode.clone(),
@@ -172,7 +172,7 @@ impl App {
fn apply_thread_settings_to_session(session: &mut ThreadSessionState, settings: &ThreadSettings) { fn apply_thread_settings_to_session(session: &mut ThreadSessionState, settings: &ThreadSettings) {
if settings.collaboration_mode.mode == ModeKind::Default { if settings.collaboration_mode.mode == ModeKind::Default {
session.model = settings.model.clone(); 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.model_provider_id = settings.model_provider.clone();
session.service_tier = settings.service_tier.clone(); session.service_tier = settings.service_tier.clone();
@@ -190,7 +190,7 @@ fn apply_thread_settings_to_session(session: &mut ThreadSessionState, settings:
.settings .settings
.model .model
.clone_from(&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)); 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", "model_reasoning_effort",
config config
.model_reasoning_effort .model_reasoning_effort
.map(|effort| effort.to_string()), .as_ref()
.map(std::string::ToString::to_string),
); );
insert( insert(
"model_reasoning_summary", "model_reasoning_summary",
@@ -1586,7 +1587,7 @@ async fn thread_session_state_from_thread_start_response(
response.cwd.clone(), response.cwd.clone(),
response.runtime_workspace_roots.clone(), response.runtime_workspace_roots.clone(),
response.instruction_sources.clone(), response.instruction_sources.clone(),
response.reasoning_effort, response.reasoning_effort.clone(),
config, config,
) )
.await .await
@@ -1627,7 +1628,7 @@ async fn thread_session_state_from_thread_resume_response(
response.cwd.clone(), response.cwd.clone(),
response.runtime_workspace_roots.clone(), response.runtime_workspace_roots.clone(),
response.instruction_sources.clone(), response.instruction_sources.clone(),
response.reasoning_effort, response.reasoning_effort.clone(),
config, config,
) )
.await .await
@@ -1659,7 +1660,7 @@ async fn thread_session_state_from_thread_fork_response(
response.cwd.clone(), response.cwd.clone(),
response.runtime_workspace_roots.clone(), response.runtime_workspace_roots.clone(),
response.instruction_sources.clone(), response.instruction_sources.clone(),
response.reasoning_effort, response.reasoning_effort.clone(),
config, config,
) )
.await .await
+1 -1
View File
@@ -166,7 +166,7 @@ impl ChatWidget {
mut collaboration_mode: CollaborationModeMask, mut collaboration_mode: CollaborationModeMask,
) { ) {
if collaboration_mode.mode == Some(ModeKind::Plan) 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)); collaboration_mode.reasoning_effort = Some(Some(effort));
} }
+80 -79
View File
@@ -100,11 +100,11 @@ impl ChatWidget {
let model = preset.model.clone(); let model = preset.model.clone();
let should_prompt_plan_mode_scope = self.should_prompt_plan_mode_reasoning_scope( let should_prompt_plan_mode_scope = self.should_prompt_plan_mode_reasoning_scope(
model.as_str(), model.as_str(),
Some(preset.default_reasoning_effort), Some(preset.default_reasoning_effort.clone()),
); );
let actions = Self::model_selection_actions( let actions = Self::model_selection_actions(
model.clone(), model.clone(),
Some(preset.default_reasoning_effort), Some(preset.default_reasoning_effort.clone()),
should_prompt_plan_mode_scope, should_prompt_plan_mode_scope,
); );
SelectionItem { SelectionItem {
@@ -222,16 +222,16 @@ impl ChatWidget {
if should_prompt_plan_mode_scope { if should_prompt_plan_mode_scope {
tx.send(AppEvent::OpenPlanReasoningScopePrompt { tx.send(AppEvent::OpenPlanReasoningScopePrompt {
model: model_for_action.clone(), model: model_for_action.clone(),
effort: effort_for_action, effort: effort_for_action.clone(),
}); });
return; return;
} }
tx.send(AppEvent::UpdateModel(model_for_action.clone())); 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 { tx.send(AppEvent::PersistModelSelection {
model: model_for_action.clone(), model: model_for_action.clone(),
effort: effort_for_action, effort: effort_for_action.clone(),
}); });
})] })]
} }
@@ -261,30 +261,34 @@ impl ChatWidget {
model: String, model: String,
effort: Option<ReasoningEffortConfig>, effort: Option<ReasoningEffortConfig>,
) { ) {
let reasoning_phrase = match effort { let reasoning_phrase = match effort.as_ref() {
Some(ReasoningEffortConfig::None) => "no reasoning".to_string(), Some(ReasoningEffortConfig::None) => "no reasoning".to_string(),
Some(selected_effort) => { Some(selected_effort) => {
format!( format!(
"{} reasoning", "{} reasoning",
Self::reasoning_effort_label(selected_effort).to_lowercase() Self::reasoning_effort_sentence_label(selected_effort)
) )
} }
None => "the selected reasoning".to_string(), None => "the selected reasoning".to_string(),
}; };
let plan_only_description = format!("Always use {reasoning_phrase} in Plan mode."); let plan_only_description = format!("Always use {reasoning_phrase} in Plan mode.");
let plan_reasoning_source = if let Some(plan_override) = let plan_reasoning_source = if let Some(plan_override) =
self.config.plan_mode_reasoning_effort self.config.plan_mode_reasoning_effort.as_ref()
{ {
format!( format!(
"user-chosen Plan override ({})", "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()) } 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!( Some(plan_effort) => format!(
"built-in Plan default ({})", "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(), 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 plan_only_actions: Vec<SelectionAction> = vec![Box::new({
let model = model.clone(); let model = model.clone();
let effort = effort.clone();
move |tx| { move |tx| {
tx.send(AppEvent::UpdateModel(model.clone())); tx.send(AppEvent::UpdateModel(model.clone()));
tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort)); tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort.clone()));
tx.send(AppEvent::PersistPlanModeReasoningEffort(effort)); tx.send(AppEvent::PersistPlanModeReasoningEffort(effort.clone()));
} }
})]; })];
let all_modes_actions: Vec<SelectionAction> = vec![Box::new(move |tx| { let all_modes_actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
tx.send(AppEvent::UpdateModel(model.clone())); tx.send(AppEvent::UpdateModel(model.clone()));
tx.send(AppEvent::UpdateReasoningEffort(effort)); tx.send(AppEvent::UpdateReasoningEffort(effort.clone()));
tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort)); tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort.clone()));
tx.send(AppEvent::PersistPlanModeReasoningEffort(effort)); tx.send(AppEvent::PersistPlanModeReasoningEffort(effort.clone()));
tx.send(AppEvent::PersistModelSelection { tx.send(AppEvent::PersistModelSelection {
model: model.clone(), 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. /// Open a popup to choose the reasoning effort (stage 2) for the given model.
pub(crate) fn open_reasoning_popup(&mut self, preset: ModelPreset) { 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 supported = preset.supported_reasoning_efforts;
let in_plan_mode = let in_plan_mode =
self.collaboration_modes_enabled() && self.active_mode_kind() == ModeKind::Plan; self.collaboration_modes_enabled() && self.active_mode_kind() == ModeKind::Plan;
@@ -362,7 +367,7 @@ impl ChatWidget {
} else { } else {
None 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); let effort_label = Self::reasoning_effort_label(effort);
format!("⚠ {effort_label} reasoning effort can quickly consume Plus plan rate limits.") 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.1-codex-max")
|| preset.model.starts_with("gpt-5.2"); || preset.model.starts_with("gpt-5.2");
struct EffortChoice { let mut choices: Vec<ReasoningEffortConfig> = ReasoningEffortConfig::known_values()
stored: Option<ReasoningEffortConfig>, .filter(|effort| supported.iter().any(|option| option.effort == *effort))
display: ReasoningEffortConfig, .collect();
} choices.extend(
let mut choices: Vec<EffortChoice> = Vec::new(); supported
for effort in ReasoningEffortConfig::iter() { .iter()
if supported.iter().any(|option| option.effort == effort) { .filter(|option| option.effort.known_rank().is_none())
choices.push(EffortChoice { .map(|option| option.effort.clone()),
stored: Some(effort), );
display: effort,
});
}
}
if choices.is_empty() { if choices.is_empty() {
choices.push(EffortChoice { choices.push(default_effort.clone());
stored: Some(default_effort),
display: default_effort,
});
} }
if choices.len() == 1 { 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; 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 self.app_event_tx
.send(AppEvent::OpenPlanReasoningScopePrompt { .send(AppEvent::OpenPlanReasoningScopePrompt {
model: selected_model, model: selected_model,
@@ -405,12 +405,10 @@ impl ChatWidget {
return; return;
} }
let default_choice: Option<ReasoningEffortConfig> = choices let default_choice = choices
.iter() .contains(&default_effort)
.any(|choice| choice.stored == Some(default_effort)) .then(|| default_effort.clone())
.then_some(Some(default_effort)) .or_else(|| choices.first().cloned())
.flatten()
.or_else(|| choices.iter().find_map(|choice| choice.stored))
.or(Some(default_effort)); .or(Some(default_effort));
let model_slug = preset.model.to_string(); let model_slug = preset.model.to_string();
@@ -419,40 +417,33 @@ impl ChatWidget {
if in_plan_mode { if in_plan_mode {
self.config self.config
.plan_mode_reasoning_effort .plan_mode_reasoning_effort
.or(self.effective_reasoning_effort()) .clone()
.or_else(|| self.effective_reasoning_effort())
} else { } else {
self.effective_reasoning_effort() self.effective_reasoning_effort()
} }
} else { } 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 let initial_selected_idx = choices
.iter() .iter()
.position(|choice| choice.stored == selection_choice) .position(|choice| Some(choice) == selection_choice.as_ref());
.or_else(|| {
selection_choice
.and_then(|effort| choices.iter().position(|choice| choice.display == effort))
});
let mut items: Vec<SelectionItem> = Vec::new(); let mut items: Vec<SelectionItem> = Vec::new();
for choice in choices.iter() { for choice in choices.iter() {
let effort = choice.display; let effort = choice.clone();
let mut effort_label = Self::reasoning_effort_label(effort).to_string(); let mut effort_label = Self::reasoning_effort_label(&effort);
if choice.stored == default_choice { if Some(choice) == default_choice.as_ref() {
effort_label.push_str(" (default)"); effort_label.push_str(" (default)");
} }
let description = choice let description = supported
.stored .iter()
.and_then(|effort| { .find(|option| option.effort == effort)
supported .map(|option| option.description.to_string())
.iter()
.find(|option| option.effort == effort)
.map(|option| option.description.to_string())
})
.filter(|text| !text.is_empty()); .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 { let selected_description = if show_warning {
warning_text.as_ref().map(|warning_message| { warning_text.as_ref().map(|warning_message| {
description.as_ref().map_or_else( description.as_ref().map_or_else(
@@ -465,21 +456,23 @@ impl ChatWidget {
}; };
let model_for_action = model_slug.clone(); let model_for_action = model_slug.clone();
let choice_effort = choice.stored; let choice_effort = Some(effort);
let should_prompt_plan_mode_scope = let should_prompt_plan_mode_scope = self.should_prompt_plan_mode_reasoning_scope(
self.should_prompt_plan_mode_reasoning_scope(model_slug.as_str(), choice_effort); model_slug.as_str(),
choice_effort.clone(),
);
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| { let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
if should_prompt_plan_mode_scope { if should_prompt_plan_mode_scope {
tx.send(AppEvent::OpenPlanReasoningScopePrompt { tx.send(AppEvent::OpenPlanReasoningScopePrompt {
model: model_for_action.clone(), model: model_for_action.clone(),
effort: choice_effort, effort: choice_effort.clone(),
}); });
} else { } else {
tx.send(AppEvent::UpdateModel(model_for_action.clone())); 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 { tx.send(AppEvent::PersistModelSelection {
model: model_for_action.clone(), model: model_for_action.clone(),
effort: choice_effort, effort: choice_effort.clone(),
}); });
} }
})]; })];
@@ -488,7 +481,7 @@ impl ChatWidget {
name: effort_label, name: effort_label,
description, description,
selected_description, selected_description,
is_current: is_current_model && choice.stored == highlight_choice, is_current: is_current_model && Some(choice) == highlight_choice.as_ref(),
actions, actions,
dismiss_on_select: true, dismiss_on_select: true,
..Default::default() ..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 { match effort {
ReasoningEffortConfig::None => "None", ReasoningEffortConfig::None => "None".to_string(),
ReasoningEffortConfig::Minimal => "Minimal", ReasoningEffortConfig::Minimal => "Minimal".to_string(),
ReasoningEffortConfig::Low => "Low", ReasoningEffortConfig::Low => "Low".to_string(),
ReasoningEffortConfig::Medium => "Medium", ReasoningEffortConfig::Medium => "Medium".to_string(),
ReasoningEffortConfig::High => "High", ReasoningEffortConfig::High => "High".to_string(),
ReasoningEffortConfig::XHigh => "Extra high", 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>) { 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 self.app_event_tx
.send(AppEvent::PersistModelSelection { model, effort }); .send(AppEvent::PersistModelSelection { model, effort });
} }
+4 -2
View File
@@ -343,14 +343,16 @@ impl ChatWidget {
/*active_permission_profile*/ None, /*active_permission_profile*/ None,
/*windows_sandbox_level*/ None, /*windows_sandbox_level*/ None,
Some(switch_model_for_events.clone()), Some(switch_model_for_events.clone()),
Some(Some(default_effort)), Some(Some(default_effort.clone())),
/*summary*/ None, /*summary*/ None,
/*service_tier*/ None, /*service_tier*/ None,
/*collaboration_mode*/ None, /*collaboration_mode*/ None,
/*personality*/ None, /*personality*/ None,
))); )));
tx.send(AppEvent::UpdateModel(switch_model_for_events.clone())); 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(); let keep_actions: Vec<SelectionAction> = Vec::new();
@@ -8,15 +8,14 @@
//! The shortcut state machine is deliberately narrow: it only handles key //! 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 //! 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 //! current model preset's default, and it walks only efforts advertised by the
//! active model. Unsupported current efforts are not normalized eagerly; the //! active model. Unsupported known efforts move to the nearest advertised known
//! next shortcut moves to the nearest supported effort in the requested //! effort in the requested direction. Unknown efforts anchor to the model
//! direction. //! default before stepping through the advertised order.
use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::ModeKind;
use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
use strum::IntoEnumIterator;
use super::ChatWidget; use super::ChatWidget;
use crate::app_event::AppEvent; use crate::app_event::AppEvent;
@@ -30,8 +29,8 @@ pub(super) enum ReasoningShortcutDirection {
} }
impl ReasoningShortcutDirection { impl ReasoningShortcutDirection {
fn bound_message(self, effort: ReasoningEffortConfig) -> String { fn bound_message(self, effort: &ReasoningEffortConfig) -> String {
let label = ChatWidget::reasoning_effort_label(effort).to_lowercase(); let label = ChatWidget::reasoning_effort_sentence_label(effort);
match self { match self {
Self::Lower => format!("Reasoning is already at the lowest level ({label})."), Self::Lower => format!("Reasoning is already at the lowest level ({label})."),
Self::Raise => format!("Reasoning is already at the highest 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 choices = reasoning_choices(&preset);
let current_effort = self let configured_effort = self
.effective_reasoning_effort() .effective_reasoning_effort()
.unwrap_or(preset.default_reasoning_effort); .unwrap_or_else(|| preset.default_reasoning_effort.clone());
let Some(next_effort) = next_reasoning_effort(&choices, Some(current_effort), direction) 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 { 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; return true;
}; };
@@ -120,18 +130,23 @@ impl ChatWidget {
} }
fn reasoning_choices(preset: &ModelPreset) -> Vec<ReasoningEffortConfig> { fn reasoning_choices(preset: &ModelPreset) -> Vec<ReasoningEffortConfig> {
let mut choices = Vec::new(); let mut choices: Vec<ReasoningEffortConfig> = ReasoningEffortConfig::known_values()
for effort in ReasoningEffortConfig::iter() { .filter(|effort| {
if preset preset
.supported_reasoning_efforts
.iter()
.any(|option| option.effort == *effort)
})
.collect();
choices.extend(
preset
.supported_reasoning_efforts .supported_reasoning_efforts
.iter() .iter()
.any(|option| option.effort == effort) .filter(|option| option.effort.known_rank().is_none())
{ .map(|option| option.effort.clone()),
choices.push(effort); );
}
}
if choices.is_empty() { if choices.is_empty() {
choices.push(preset.default_reasoning_effort); choices.push(preset.default_reasoning_effort.clone());
} }
choices choices
} }
@@ -142,32 +157,51 @@ fn next_reasoning_effort(
direction: ReasoningShortcutDirection, direction: ReasoningShortcutDirection,
) -> Option<ReasoningEffortConfig> { ) -> Option<ReasoningEffortConfig> {
let current_effort = current_effort?; let current_effort = current_effort?;
if choices.is_empty() { if let Some(current_index) = choices.iter().position(|choice| choice == &current_effort) {
return None; 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); let current_rank = current_effort.known_rank()?;
match direction { let ranked_choice = match direction {
ReasoningShortcutDirection::Lower => choices ReasoningShortcutDirection::Lower => choices
.iter() .iter()
.rev() .filter_map(|choice| choice.known_rank().map(|rank| (rank, choice)))
.copied() .filter(|(rank, _)| *rank < current_rank)
.find(|choice| effort_rank(*choice) < current_rank), .max_by_key(|(rank, _)| *rank)
.map(|(_, choice)| choice.clone()),
ReasoningShortcutDirection::Raise => choices ReasoningShortcutDirection::Raise => choices
.iter() .iter()
.copied() .filter_map(|choice| choice.known_rank().map(|rank| (rank, choice)))
.find(|choice| effort_rank(*choice) > current_rank), .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 { let nearest_known_index = choices
match effort { .iter()
ReasoningEffortConfig::None => 0, .enumerate()
ReasoningEffortConfig::Minimal => 1, .filter_map(|(index, choice)| {
ReasoningEffortConfig::Low => 2, choice
ReasoningEffortConfig::Medium => 3, .known_rank()
ReasoningEffortConfig::High => 4, .map(|rank| (rank.abs_diff(current_rank), index))
ReasoningEffortConfig::XHigh => 5, })
.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] #[test]
fn next_reasoning_effort_clamps_at_bounds() { fn next_reasoning_effort_clamps_at_bounds() {
let choices = vec![ let choices = vec![
+2 -2
View File
@@ -79,7 +79,7 @@ impl ChatWidget {
let default_model = session.model.clone(); let default_model = session.model.clone();
self.current_collaboration_mode = self.current_collaboration_mode.with_updates( self.current_collaboration_mode = self.current_collaboration_mode.with_updates(
Some(default_model.clone()), Some(default_model.clone()),
Some(session.reasoning_effort), Some(session.reasoning_effort.clone()),
/*developer_instructions*/ None, /*developer_instructions*/ None,
); );
match session.collaboration_mode.as_deref() { match session.collaboration_mode.as_deref() {
@@ -93,7 +93,7 @@ impl ChatWidget {
Some(&default_model), Some(&default_model),
); );
if let Some(mask) = self.active_collaboration_mask.as_mut() { 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.update_collaboration_mode_indicator();
self.refresh_plan_mode_nudge(); 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. /// so the footer reflects it without waiting for the next mode switch.
/// Passing `None` resets to the Plan-mode preset default. /// Passing `None` resets to the Plan-mode preset default.
pub(crate) fn set_plan_mode_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) { 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() if self.collaboration_modes_enabled()
&& let Some(mask) = self.active_collaboration_mask.as_mut() && let Some(mask) = self.active_collaboration_mask.as_mut()
&& mask.mode == Some(ModeKind::Plan) && mask.mode == Some(ModeKind::Plan)
@@ -182,7 +182,7 @@ impl ChatWidget {
pub(crate) fn set_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) { pub(crate) fn set_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) {
self.current_collaboration_mode = self.current_collaboration_mode.with_updates( self.current_collaboration_mode = self.current_collaboration_mode.with_updates(
/*model*/ None, /*model*/ None,
Some(effort), Some(effort.clone()),
/*developer_instructions*/ None, /*developer_instructions*/ None,
); );
if self.collaboration_modes_enabled() if self.collaboration_modes_enabled()
@@ -471,7 +471,7 @@ impl ChatWidget {
let current_effort = self.current_collaboration_mode.reasoning_effort(); let current_effort = self.current_collaboration_mode.reasoning_effort();
self.active_collaboration_mask self.active_collaboration_mask
.as_ref() .as_ref()
.and_then(|mask| mask.reasoning_effort) .and_then(|mask| mask.reasoning_effort.clone())
.unwrap_or(current_effort) .unwrap_or(current_effort)
} }
@@ -590,7 +590,7 @@ impl ChatWidget {
name: mode_kind.display_name().to_string(), name: mode_kind.display_name().to_string(),
mode: Some(mode_kind), mode: Some(mode_kind),
model: Some(settings.model.clone()), model: Some(settings.model.clone()),
reasoning_effort: Some(settings.reasoning_effort), reasoning_effort: Some(settings.reasoning_effort.clone()),
developer_instructions: Some(settings.developer_instructions), developer_instructions: Some(settings.developer_instructions),
}); });
self.update_collaboration_mode_indicator(); self.update_collaboration_mode_indicator();
@@ -712,7 +712,7 @@ impl ChatWidget {
let previous_model = self.current_model().to_string(); let previous_model = self.current_model().to_string();
let previous_effort = self.effective_reasoning_effort(); let previous_effort = self.effective_reasoning_effort();
if mask.mode == Some(ModeKind::Plan) 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)); mask.reasoning_effort = Some(Some(effort));
} }
@@ -732,13 +732,9 @@ impl ChatWidget {
{ {
let mut message = format!("Model changed to {next_model}"); let mut message = format!("Model changed to {next_model}");
if !next_model.starts_with("codex-auto-") { if !next_model.starts_with("codex-auto-") {
let reasoning_label = match next_effort { let reasoning_label = match next_effort.as_ref() {
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",
Some(effort) => effort.as_str(),
}; };
message.push(' '); message.push(' ');
message.push_str(reasoning_label); message.push_str(reasoning_label);
@@ -8,5 +8,6 @@ expression: popup
2. Medium (default) Balances speed and reasoning depth for everyday tasks 2. Medium (default) Balances speed and reasoning depth for everyday tasks
› 3. High (current) Greater reasoning depth for complex problems › 3. High (current) Greater reasoning depth for complex problems
4. Extra high Extra high 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 Press enter to confirm or esc to go back
@@ -212,7 +212,7 @@ impl ChatWidget {
}); });
let reasoning_effort_override = Some( let reasoning_effort_override = Some(
self.effective_reasoning_effort() self.effective_reasoning_effort()
.or(self.config.model_reasoning_effort) .or_else(|| self.config.model_reasoning_effort.clone())
.or(model_default_reasoning_effort), .or(model_default_reasoning_effort),
); );
let rate_limit_snapshots: Vec<RateLimitSnapshotDisplay> = self let rate_limit_snapshots: Vec<RateLimitSnapshotDisplay> = self
@@ -382,15 +382,11 @@ impl ChatWidget {
} }
pub(super) fn status_line_reasoning_effort_label( pub(super) fn status_line_reasoning_effort_label(
effort: Option<ReasoningEffortConfig>, effort: Option<&ReasoningEffortConfig>,
) -> &'static str { ) -> String {
match effort { match effort {
Some(ReasoningEffortConfig::Minimal) => "minimal", None | Some(ReasoningEffortConfig::None) => "default".to_string(),
Some(ReasoningEffortConfig::Low) => "low", Some(effort) => effort.as_str().to_string(),
Some(ReasoningEffortConfig::Medium) => "medium",
Some(ReasoningEffortConfig::High) => "high",
Some(ReasoningEffortConfig::XHigh) => "xhigh",
None | Some(ReasoningEffortConfig::None) => "default",
} }
} }
} }
@@ -562,7 +562,7 @@ impl ChatWidget {
match item { match item {
StatusLineItem::ModelName => Some(self.model_display_name().to_string()), StatusLineItem::ModelName => Some(self.model_display_name().to_string()),
StatusLineItem::ModelWithReasoning => Some(self.model_with_reasoning_display_name()), 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 => { StatusLineItem::CurrentDir => {
Some(format_directory_display( Some(format_directory_display(
self.status_line_cwd(), self.status_line_cwd(),
@@ -762,15 +762,16 @@ impl ChatWidget {
/*max_chars*/ 32, /*max_chars*/ 32,
)), )),
TerminalTitleItem::Reasoning => Some(Self::truncate_terminal_title_part( TerminalTitleItem::Reasoning => Some(Self::truncate_terminal_title_part(
self.reasoning_display_name().to_string(), self.reasoning_display_name(),
/*max_chars*/ 32, /*max_chars*/ 32,
)), )),
TerminalTitleItem::TaskProgress => self.terminal_title_task_progress(), TerminalTitleItem::TaskProgress => self.terminal_title_task_progress(),
} }
} }
fn reasoning_display_name(&self) -> &'static str { fn reasoning_display_name(&self) -> String {
Self::status_line_reasoning_effort_label(self.effective_reasoning_effort()) let effort = self.effective_reasoning_effort();
Self::status_line_reasoning_effort_label(effort.as_ref())
} }
fn model_with_reasoning_display_name(&self) -> String { fn model_with_reasoning_display_name(&self) -> String {
@@ -2405,13 +2405,54 @@ async fn model_reasoning_selection_popup_snapshot() {
set_chatgpt_auth(&mut chat); set_chatgpt_auth(&mut chat);
chat.set_reasoning_effort(Some(ReasoningEffortConfig::High)); 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); chat.open_reasoning_popup(preset);
let popup = render_bottom_popup(&chat, /*width*/ 80); let popup = render_bottom_popup(&chat, /*width*/ 80);
assert_chatwidget_snapshot!("model_reasoning_selection_popup", popup); 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] #[tokio::test]
async fn model_reasoning_selection_popup_extra_high_warning_snapshot() { async fn model_reasoning_selection_popup_extra_high_warning_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await; 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) // Header box rendered as history (so it appears at the very top)
let header = SessionHeaderHistoryCell::new( let header = SessionHeaderHistoryCell::new(
session.model.clone(), session.model.clone(),
session.reasoning_effort, session.reasoning_effort.clone(),
show_fast_status, show_fast_status,
config.cwd.to_path_buf(), config.cwd.to_path_buf(),
CODEX_CLI_VERSION, CODEX_CLI_VERSION,
@@ -317,15 +317,10 @@ impl SessionHeaderHistoryCell {
formatted formatted
} }
fn reasoning_label(&self) -> Option<&'static str> { fn reasoning_label(&self) -> Option<&str> {
self.reasoning_effort.map(|effort| match effort { self.reasoning_effort
ReasoningEffortConfig::Minimal => "minimal", .as_ref()
ReasoningEffortConfig::Low => "low", .map(ReasoningEffortConfig::as_str)
ReasoningEffortConfig::Medium => "medium",
ReasoningEffortConfig::High => "high",
ReasoningEffortConfig::XHigh => "xhigh",
ReasoningEffortConfig::None => "none",
})
} }
} }
@@ -368,7 +363,7 @@ impl HistoryCell for SessionHeaderHistoryCell {
]; ];
if let Some(reasoning) = reasoning_label { if let Some(reasoning) = reasoning_label {
spans.push(Span::from(" ")); spans.push(Span::from(" "));
spans.push(Span::from(reasoning)); spans.push(Span::from(reasoning.to_owned()));
} }
if self.show_fast_status { if self.show_fast_status {
spans.push(" ".into()); spans.push(" ".into());
+1 -1
View File
@@ -182,7 +182,7 @@ pub(crate) fn spawn_request_summary(item: &ThreadItem) -> Option<SpawnRequestSum
.. ..
} => Some(SpawnRequestSummary { } => Some(SpawnRequestSummary {
model: model.clone(), model: model.clone(),
reasoning_effort: *reasoning_effort, reasoning_effort: reasoning_effort.clone(),
}), }),
_ => None, _ => None,
} }
+1 -1
View File
@@ -287,7 +287,7 @@ impl StatusHistoryCell {
]; ];
if config.model_provider.wire_api == WireApi::Responses { if config.model_provider.wire_api == WireApi::Responses {
let effort_value = reasoning_effort_override 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()) .map(|effort| effort.to_string())
.unwrap_or_else(|| "none".to_string()); .unwrap_or_else(|| "none".to_string());
config_entries.push(("reasoning effort", effort_value)); 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 { if config.model_provider.wire_api == WireApi::Responses {
let reasoning_effort = config let reasoning_effort = config
.model_reasoning_effort .model_reasoning_effort
.map(|effort| effort.to_string()); .as_ref()
.map(std::string::ToString::to_string);
entries.push(( entries.push((
"reasoning effort", "reasoning effort",
reasoning_effort.unwrap_or_else(|| "none".to_string()), reasoning_effort.unwrap_or_else(|| "none".to_string()),