mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Support model-defined reasoning efforts (#26444)
## Summary - accept non-empty model-defined reasoning effort values while preserving built-in effort behavior - propagate the non-Copy effort type through core, app-server, TUI, telemetry, and persistence call sites - preserve string wire encoding and expose an open-string schema for clients - update model selection and shortcut behavior for model-advertised effort values ## Root cause `ReasoningEffort` gained a string-backed custom variant, so it could no longer implement `Copy` or rely on derived closed-enum serialization. Existing consumers still moved effort values from shared references and assumed a fixed built-in value set. ## Validation - `just fmt` - Local tests and compilation were not run per request; relying on CI.
This commit is contained in:
+2
-9
@@ -1971,15 +1971,8 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningItemContent": {
|
||||
|
||||
@@ -2777,15 +2777,8 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningSummary": {
|
||||
|
||||
+2
-9
@@ -13525,15 +13525,8 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningEffortOption": {
|
||||
|
||||
+2
-9
@@ -10047,15 +10047,8 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningEffortOption": {
|
||||
|
||||
@@ -667,15 +667,8 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningSummary": {
|
||||
|
||||
+2
-9
@@ -462,15 +462,8 @@
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"TextElement": {
|
||||
|
||||
@@ -462,15 +462,8 @@
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"TextElement": {
|
||||
|
||||
@@ -185,15 +185,8 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningEffortOption": {
|
||||
|
||||
@@ -606,15 +606,8 @@
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"TextElement": {
|
||||
|
||||
@@ -718,15 +718,8 @@
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"SandboxPolicy": {
|
||||
|
||||
@@ -632,15 +632,8 @@
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"SessionSource": {
|
||||
|
||||
+2
-9
@@ -632,15 +632,8 @@
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"SessionSource": {
|
||||
|
||||
@@ -632,15 +632,8 @@
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"SessionSource": {
|
||||
|
||||
@@ -718,15 +718,8 @@
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"SandboxPolicy": {
|
||||
|
||||
@@ -632,15 +632,8 @@
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"SessionSource": {
|
||||
|
||||
+2
-9
@@ -124,15 +124,8 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningSummary": {
|
||||
|
||||
@@ -718,15 +718,8 @@
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"SandboxPolicy": {
|
||||
|
||||
+2
-9
@@ -632,15 +632,8 @@
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"SessionSource": {
|
||||
|
||||
@@ -632,15 +632,8 @@
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"SessionSource": {
|
||||
|
||||
+2
-9
@@ -606,15 +606,8 @@
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"TextElement": {
|
||||
|
||||
@@ -154,15 +154,8 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningSummary": {
|
||||
|
||||
@@ -606,15 +606,8 @@
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"TextElement": {
|
||||
|
||||
@@ -606,15 +606,8 @@
|
||||
]
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"TextElement": {
|
||||
|
||||
@@ -5,4 +5,4 @@
|
||||
/**
|
||||
* See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
|
||||
*/
|
||||
export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
||||
export type ReasoningEffort = string;
|
||||
|
||||
@@ -621,7 +621,7 @@ impl ThreadHistoryBuilder {
|
||||
receiver_thread_ids: Vec::new(),
|
||||
prompt: Some(payload.prompt.clone()),
|
||||
model: Some(payload.model.clone()),
|
||||
reasoning_effort: Some(payload.reasoning_effort),
|
||||
reasoning_effort: Some(payload.reasoning_effort.clone()),
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
self.upsert_item_in_current_turn(item);
|
||||
@@ -656,7 +656,7 @@ impl ThreadHistoryBuilder {
|
||||
receiver_thread_ids,
|
||||
prompt: Some(payload.prompt.clone()),
|
||||
model: Some(payload.model.clone()),
|
||||
reasoning_effort: Some(payload.reasoning_effort),
|
||||
reasoning_effort: Some(payload.reasoning_effort.clone()),
|
||||
agents_states,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ Example with notification opt-out:
|
||||
- `fs/watch` — subscribe this connection to filesystem change notifications for an absolute file or directory path and caller-provided `watchId`; returns the canonicalized `path`.
|
||||
- `fs/unwatch` — stop sending notifications for a prior `fs/watch`; returns `{}`.
|
||||
- `fs/changed` — notification emitted when watched paths change, including the `watchId` and `changedPaths`.
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, `serviceTiers`, optional `defaultServiceTier`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with model-advertised string reasoning effort options, `additionalSpeedTiers`, `serviceTiers`, optional `defaultServiceTier`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
|
||||
- `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider.
|
||||
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. Pass `threadId` when showing feature state for an existing loaded thread so `enabled` is computed from that thread's refreshed config, including project-local config for the thread's cwd; if omitted, the server uses its default config resolution context. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
|
||||
- `permissionProfile/list` — beta; list available permission profile ids with optional display `description` text, using cursor pagination. Pass `cwd` when the caller needs project-local `[permissions.<id>]` entries to be included in the current catalog view.
|
||||
|
||||
@@ -62,10 +62,10 @@ fn reasoning_efforts_from_preset(
|
||||
efforts: Vec<ReasoningEffortPreset>,
|
||||
) -> Vec<ReasoningEffortOption> {
|
||||
efforts
|
||||
.iter()
|
||||
.into_iter()
|
||||
.map(|preset| ReasoningEffortOption {
|
||||
reasoning_effort: preset.effort,
|
||||
description: preset.description.to_string(),
|
||||
description: preset.description,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ fn merge_persisted_resume_metadata(
|
||||
typesafe_overrides.model = persisted_metadata.model.clone();
|
||||
typesafe_overrides.model_provider = Some(persisted_metadata.model_provider.clone());
|
||||
|
||||
if let Some(reasoning_effort) = persisted_metadata.reasoning_effort {
|
||||
if let Some(reasoning_effort) = persisted_metadata.reasoning_effort.as_ref() {
|
||||
request_overrides.get_or_insert_with(HashMap::new).insert(
|
||||
"model_reasoning_effort".to_string(),
|
||||
serde_json::Value::String(reasoning_effort.to_string()),
|
||||
|
||||
@@ -203,7 +203,7 @@ pub(crate) fn thread_settings_from_config_snapshot(
|
||||
model: config_snapshot.model.clone(),
|
||||
model_provider: config_snapshot.model_provider_id.clone(),
|
||||
service_tier: config_snapshot.service_tier.clone(),
|
||||
effort: config_snapshot.reasoning_effort,
|
||||
effort: config_snapshot.reasoning_effort.clone(),
|
||||
summary: config_snapshot.reasoning_summary,
|
||||
collaboration_mode: config_snapshot.collaboration_mode.clone(),
|
||||
personality: config_snapshot.personality,
|
||||
|
||||
@@ -629,7 +629,7 @@ impl TurnRequestProcessor {
|
||||
profile_workspace_roots: profile_workspace_roots.clone(),
|
||||
windows_sandbox_level: None,
|
||||
model: model.clone(),
|
||||
effort,
|
||||
effort: effort.clone(),
|
||||
summary,
|
||||
service_tier: service_tier.clone(),
|
||||
collaboration_mode: collaboration_mode.clone(),
|
||||
|
||||
@@ -18,7 +18,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
|
||||
slug: preset.id.clone(),
|
||||
display_name: preset.display_name.clone(),
|
||||
description: Some(preset.description.clone()),
|
||||
default_reasoning_level: Some(preset.default_reasoning_effort),
|
||||
default_reasoning_level: Some(preset.default_reasoning_effort.clone()),
|
||||
supported_reasoning_levels: preset.supported_reasoning_efforts.clone(),
|
||||
shell_type: ConfigShellToolType::ShellCommand,
|
||||
visibility: if preset.show_in_picker {
|
||||
|
||||
@@ -48,11 +48,11 @@ fn model_from_preset(preset: &ModelPreset) -> Model {
|
||||
.supported_reasoning_efforts
|
||||
.iter()
|
||||
.map(|preset| ReasoningEffortOption {
|
||||
reasoning_effort: preset.effort,
|
||||
reasoning_effort: preset.effort.clone(),
|
||||
description: preset.description.clone(),
|
||||
})
|
||||
.collect(),
|
||||
default_reasoning_effort: preset.default_reasoning_effort,
|
||||
default_reasoning_effort: preset.default_reasoning_effort.clone(),
|
||||
input_modalities: preset.input_modalities.clone(),
|
||||
// `write_models_cache()` round-trips through a simplified ModelInfo fixture that does not
|
||||
// preserve personality placeholders in base instructions, so app-server list results from
|
||||
@@ -164,10 +164,10 @@ async fn list_models_uses_chatgpt_remote_catalog_as_source_of_truth() -> Result<
|
||||
"slug": "chatgpt-remote-only",
|
||||
"display_name": "ChatGPT Remote Only",
|
||||
"description": "Remote-only model for app-server model/list coverage",
|
||||
"default_reasoning_level": "medium",
|
||||
"default_reasoning_level": "max",
|
||||
"supported_reasoning_levels": [
|
||||
{"effort": "low", "description": "low"},
|
||||
{"effort": "medium", "description": "medium"}
|
||||
{"effort": "max", "description": "Maximum"}
|
||||
],
|
||||
"shell_type": "shell_command",
|
||||
"visibility": "list",
|
||||
|
||||
@@ -2503,15 +2503,8 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningEffort": {
|
||||
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
|
||||
"enum": [
|
||||
"none",
|
||||
"minimal",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh"
|
||||
],
|
||||
"description": "A non-empty reasoning effort value advertised by the model.",
|
||||
"minLength": 1,
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningSummary": {
|
||||
|
||||
@@ -721,7 +721,7 @@ impl ModelClient {
|
||||
) -> Option<Reasoning> {
|
||||
if model_info.supports_reasoning_summaries {
|
||||
Some(Reasoning {
|
||||
effort: effort.or(model_info.default_reasoning_level),
|
||||
effort: effort.or_else(|| model_info.default_reasoning_level.clone()),
|
||||
summary: if summary == ReasoningSummaryConfig::None {
|
||||
None
|
||||
} else {
|
||||
@@ -1267,7 +1267,7 @@ impl ModelClientSession {
|
||||
&client_setup.api_provider,
|
||||
prompt,
|
||||
model_info,
|
||||
effort,
|
||||
effort.clone(),
|
||||
summary,
|
||||
service_tier.clone(),
|
||||
)?;
|
||||
@@ -1376,7 +1376,7 @@ impl ModelClientSession {
|
||||
&client_setup.api_provider,
|
||||
prompt,
|
||||
model_info,
|
||||
effort,
|
||||
effort.clone(),
|
||||
summary,
|
||||
service_tier.clone(),
|
||||
)?;
|
||||
@@ -1595,7 +1595,7 @@ impl ModelClientSession {
|
||||
prompt,
|
||||
model_info,
|
||||
session_telemetry,
|
||||
effort,
|
||||
effort.clone(),
|
||||
summary,
|
||||
service_tier.clone(),
|
||||
turn_metadata_header,
|
||||
|
||||
@@ -573,7 +573,7 @@ async fn drain_to_completed(
|
||||
prompt,
|
||||
&turn_context.model_info,
|
||||
&turn_context.session_telemetry,
|
||||
turn_context.reasoning_effort,
|
||||
turn_context.reasoning_effort.clone(),
|
||||
turn_context.reasoning_summary,
|
||||
turn_context.config.service_tier.clone(),
|
||||
turn_metadata_header,
|
||||
|
||||
@@ -242,7 +242,7 @@ async fn run_remote_compact_task_inner_impl(
|
||||
&prompt,
|
||||
&turn_context.model_info,
|
||||
CompactConversationRequestSettings {
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: turn_context.reasoning_summary,
|
||||
service_tier: if sess.services.auth_manager.auth_mode() == Some(AuthMode::ApiKey) {
|
||||
None
|
||||
|
||||
@@ -339,7 +339,7 @@ async fn run_remote_compaction_request_v2(
|
||||
prompt,
|
||||
&turn_context.model_info,
|
||||
&turn_context.session_telemetry,
|
||||
turn_context.reasoning_effort,
|
||||
turn_context.reasoning_effort.clone(),
|
||||
turn_context.reasoning_summary,
|
||||
turn_context.config.service_tier.clone(),
|
||||
turn_metadata_header,
|
||||
|
||||
@@ -225,7 +225,7 @@ impl ConfigDocument {
|
||||
);
|
||||
mutated |= self.write_optional_value(
|
||||
&["model_reasoning_effort"],
|
||||
effort.map(|effort| value(effort.to_string())),
|
||||
effort.as_ref().map(|effort| value(effort.to_string())),
|
||||
);
|
||||
mutated
|
||||
}),
|
||||
|
||||
@@ -702,7 +702,7 @@ pub(super) async fn run_guardian_review_session(
|
||||
.supported_reasoning_efforts
|
||||
.iter()
|
||||
.any(|effort| effort.effort == codex_protocol::openai_models::ReasoningEffort::Low),
|
||||
Some(preset.default_reasoning_effort),
|
||||
Some(preset.default_reasoning_effort.clone()),
|
||||
);
|
||||
(review_model_id.to_string(), reasoning_effort)
|
||||
} else {
|
||||
@@ -712,7 +712,8 @@ pub(super) async fn run_guardian_review_session(
|
||||
.iter()
|
||||
.any(|preset| preset.effort == codex_protocol::openai_models::ReasoningEffort::Low),
|
||||
turn.reasoning_effort
|
||||
.or(turn.model_info.default_reasoning_level),
|
||||
.clone()
|
||||
.or_else(|| turn.model_info.default_reasoning_level.clone()),
|
||||
);
|
||||
(
|
||||
model_override
|
||||
@@ -725,7 +726,7 @@ pub(super) async fn run_guardian_review_session(
|
||||
turn.config.as_ref(),
|
||||
live_network_config.clone(),
|
||||
guardian_model.as_str(),
|
||||
guardian_reasoning_effort,
|
||||
guardian_reasoning_effort.clone(),
|
||||
);
|
||||
let guardian_config = match guardian_config {
|
||||
Ok(config) => config,
|
||||
|
||||
@@ -167,7 +167,7 @@ impl GuardianReviewSessionReuseKey {
|
||||
model_context_window: spawn_config.model_context_window,
|
||||
model_auto_compact_token_limit: spawn_config.model_auto_compact_token_limit,
|
||||
model_auto_compact_token_limit_scope: spawn_config.model_auto_compact_token_limit_scope,
|
||||
model_reasoning_effort: spawn_config.model_reasoning_effort,
|
||||
model_reasoning_effort: spawn_config.model_reasoning_effort.clone(),
|
||||
model_reasoning_summary: spawn_config.model_reasoning_summary,
|
||||
permissions: spawn_config.permissions.clone(),
|
||||
developer_instructions: spawn_config.developer_instructions.clone(),
|
||||
@@ -658,7 +658,8 @@ async fn run_review_on_session(
|
||||
let guardian_reasoning_effort = if model_info.supports_reasoning_summaries {
|
||||
params
|
||||
.reasoning_effort
|
||||
.or(model_info.default_reasoning_level)
|
||||
.clone()
|
||||
.or_else(|| model_info.default_reasoning_level.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -742,7 +743,7 @@ async fn run_review_on_session(
|
||||
mode: codex_protocol::config_types::ModeKind::Default,
|
||||
settings: codex_protocol::config_types::Settings {
|
||||
model: params.model.clone(),
|
||||
reasoning_effort: params.reasoning_effort,
|
||||
reasoning_effort: params.reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
}),
|
||||
@@ -1107,7 +1108,7 @@ mod tests {
|
||||
async fn test_review_params() -> GuardianReviewSessionParams {
|
||||
let (session, turn) = crate::session::tests::make_session_and_context().await;
|
||||
let model = turn.model_info.slug.clone();
|
||||
let reasoning_effort = turn.reasoning_effort;
|
||||
let reasoning_effort = turn.reasoning_effort.clone();
|
||||
let reasoning_summary = turn.reasoning_summary;
|
||||
let personality = turn.personality;
|
||||
#[allow(deprecated)]
|
||||
@@ -1116,7 +1117,7 @@ mod tests {
|
||||
turn.config.as_ref(),
|
||||
/*live_network_config*/ None,
|
||||
model.as_str(),
|
||||
reasoning_effort,
|
||||
reasoning_effort.clone(),
|
||||
)
|
||||
.expect("guardian config");
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ fn save_config_resolved_fields(
|
||||
) -> anyhow::Result<()> {
|
||||
lock_config.web_search = Some(config.web_search_mode.value());
|
||||
lock_config.model_provider = Some(config.model_provider_id.clone());
|
||||
lock_config.plan_mode_reasoning_effort = config.plan_mode_reasoning_effort;
|
||||
lock_config.plan_mode_reasoning_effort = config.plan_mode_reasoning_effort.clone();
|
||||
lock_config.model_verbosity = config.model_verbosity;
|
||||
lock_config.include_permissions_instructions = Some(config.include_permissions_instructions);
|
||||
lock_config.include_apps_instructions = Some(config.include_apps_instructions);
|
||||
|
||||
@@ -579,7 +579,7 @@ impl Codex {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
model: model.clone(),
|
||||
reasoning_effort: config.model_reasoning_effort,
|
||||
reasoning_effort: config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -74,7 +74,7 @@ pub(super) async fn spawn_review_thread(
|
||||
let auth_manager_for_context = auth_manager.clone();
|
||||
let provider_for_context = provider.clone();
|
||||
let session_telemetry_for_context = session_telemetry.clone();
|
||||
let reasoning_effort = per_turn_config.model_reasoning_effort;
|
||||
let reasoning_effort = per_turn_config.model_reasoning_effort.clone();
|
||||
let reasoning_summary = per_turn_config
|
||||
.model_reasoning_summary
|
||||
.unwrap_or(model_info.default_reasoning_summary);
|
||||
|
||||
@@ -74,7 +74,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
let rollout_items = vec![RolloutItem::TurnContext(previous_context_item)];
|
||||
@@ -113,7 +113,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
let turn_id = previous_context_item
|
||||
@@ -963,7 +963,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
let previous_turn_id = previous_context_item
|
||||
@@ -1042,7 +1042,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
}))
|
||||
.expect("serialize expected reference context item")
|
||||
@@ -1071,7 +1071,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
let previous_turn_id = previous_context_item
|
||||
@@ -1190,7 +1190,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
|
||||
@@ -1308,7 +1308,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
let previous_turn_id = previous_context_item
|
||||
@@ -1467,7 +1467,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
let previous_turn_id = previous_context_item
|
||||
|
||||
@@ -2559,7 +2559,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
multi_agent_version: None,
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
effort: turn_context.reasoning_effort.clone(),
|
||||
summary: codex_protocol::config_types::ReasoningSummary::Auto,
|
||||
};
|
||||
let turn_id = previous_context_item
|
||||
@@ -3146,7 +3146,7 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
let model = get_model_offline_for_tests(config.model.as_deref());
|
||||
let model_info =
|
||||
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config());
|
||||
let reasoning_effort = config.model_reasoning_effort;
|
||||
let reasoning_effort = config.model_reasoning_effort.clone();
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
@@ -3254,7 +3254,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
let model = get_model_offline_for_tests(config.model.as_deref());
|
||||
let model_info =
|
||||
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config());
|
||||
let reasoning_effort = config.model_reasoning_effort;
|
||||
let reasoning_effort = config.model_reasoning_effort.clone();
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
@@ -3785,7 +3785,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
let model = get_model_offline_for_tests(config.model.as_deref());
|
||||
let model_info =
|
||||
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config());
|
||||
let reasoning_effort = config.model_reasoning_effort;
|
||||
let reasoning_effort = config.model_reasoning_effort.clone();
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
@@ -4614,7 +4614,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
model,
|
||||
reasoning_effort: config.model_reasoning_effort,
|
||||
reasoning_effort: config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
@@ -4716,7 +4716,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
let model = get_model_offline_for_tests(config.model.as_deref());
|
||||
let model_info =
|
||||
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config());
|
||||
let reasoning_effort = config.model_reasoning_effort;
|
||||
let reasoning_effort = config.model_reasoning_effort.clone();
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
@@ -4959,7 +4959,7 @@ async fn make_session_with_config_and_rx(
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
model,
|
||||
reasoning_effort: config.model_reasoning_effort,
|
||||
reasoning_effort: config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
@@ -5064,7 +5064,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
model,
|
||||
reasoning_effort: config.model_reasoning_effort,
|
||||
reasoning_effort: config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
};
|
||||
@@ -5999,7 +5999,7 @@ async fn user_turn_updates_approvals_reviewer() {
|
||||
mode: codex_protocol::config_types::ModeKind::Default,
|
||||
settings: codex_protocol::config_types::Settings {
|
||||
model: turn_context.model_info.slug.clone(),
|
||||
reasoning_effort: config.model_reasoning_effort,
|
||||
reasoning_effort: config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
}),
|
||||
@@ -6806,7 +6806,7 @@ where
|
||||
let model = get_model_offline_for_tests(config.model.as_deref());
|
||||
let model_info =
|
||||
construct_model_info_offline_for_tests(model.as_str(), &config.to_models_manager_config());
|
||||
let reasoning_effort = config.model_reasoning_effort;
|
||||
let reasoning_effort = config.model_reasoning_effort.clone();
|
||||
let collaboration_mode = CollaborationMode {
|
||||
mode: ModeKind::Default,
|
||||
settings: Settings {
|
||||
|
||||
@@ -698,7 +698,7 @@ async fn track_turn_resolved_config_analytics(
|
||||
permission_profile: turn_context.permission_profile(),
|
||||
#[allow(deprecated)]
|
||||
permission_profile_cwd: turn_context.cwd.to_path_buf(),
|
||||
reasoning_effort: turn_context.reasoning_effort,
|
||||
reasoning_effort: turn_context.reasoning_effort.clone(),
|
||||
reasoning_summary: Some(turn_context.reasoning_summary),
|
||||
service_tier: turn_context
|
||||
.config
|
||||
@@ -1799,7 +1799,7 @@ async fn try_run_sampling_request(
|
||||
prompt,
|
||||
&turn_context.model_info,
|
||||
&turn_context.session_telemetry,
|
||||
turn_context.reasoning_effort,
|
||||
turn_context.reasoning_effort.clone(),
|
||||
turn_context.reasoning_summary,
|
||||
turn_context.config.service_tier.clone(),
|
||||
turn_metadata_header,
|
||||
|
||||
@@ -136,7 +136,8 @@ impl TurnContext {
|
||||
pub(crate) fn effective_reasoning_effort(&self) -> Option<ReasoningEffortConfig> {
|
||||
if self.model_info.supports_reasoning_summaries {
|
||||
self.reasoning_effort
|
||||
.or(self.model_info.default_reasoning_level)
|
||||
.clone()
|
||||
.or_else(|| self.model_info.default_reasoning_level.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -196,28 +197,29 @@ impl TurnContext {
|
||||
let supported_reasoning_levels = model_info
|
||||
.supported_reasoning_levels
|
||||
.iter()
|
||||
.map(|preset| preset.effort)
|
||||
.map(|preset| preset.effort.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let reasoning_effort = if let Some(current_reasoning_effort) = self.reasoning_effort {
|
||||
let reasoning_effort = if let Some(current_reasoning_effort) = self.reasoning_effort.clone()
|
||||
{
|
||||
if supported_reasoning_levels.contains(¤t_reasoning_effort) {
|
||||
Some(current_reasoning_effort)
|
||||
} else {
|
||||
supported_reasoning_levels
|
||||
.get(supported_reasoning_levels.len().saturating_sub(1) / 2)
|
||||
.copied()
|
||||
.or(model_info.default_reasoning_level)
|
||||
.cloned()
|
||||
.or_else(|| model_info.default_reasoning_level.clone())
|
||||
}
|
||||
} else {
|
||||
supported_reasoning_levels
|
||||
.get(supported_reasoning_levels.len().saturating_sub(1) / 2)
|
||||
.copied()
|
||||
.or(model_info.default_reasoning_level)
|
||||
.cloned()
|
||||
.or_else(|| model_info.default_reasoning_level.clone())
|
||||
};
|
||||
config.model_reasoning_effort = reasoning_effort;
|
||||
config.model_reasoning_effort = reasoning_effort.clone();
|
||||
|
||||
let collaboration_mode = self.collaboration_mode.with_updates(
|
||||
Some(model.clone()),
|
||||
Some(reasoning_effort),
|
||||
Some(reasoning_effort.clone()),
|
||||
/*developer_instructions*/ None,
|
||||
);
|
||||
let features = self.features.clone();
|
||||
@@ -363,7 +365,7 @@ impl TurnContext {
|
||||
collaboration_mode: Some(self.collaboration_mode.clone()),
|
||||
multi_agent_version: Some(self.multi_agent_version),
|
||||
realtime_active: Some(self.realtime_active),
|
||||
effort: self.reasoning_effort,
|
||||
effort: self.reasoning_effort.clone(),
|
||||
summary: ReasoningSummaryConfig::Auto,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +271,7 @@ async fn schedule_startup_prewarm_inner(
|
||||
&startup_prompt,
|
||||
&startup_turn_context.model_info,
|
||||
&startup_turn_context.session_telemetry,
|
||||
startup_turn_context.reasoning_effort,
|
||||
startup_turn_context.reasoning_effort.clone(),
|
||||
startup_turn_context.reasoning_summary,
|
||||
startup_turn_context.config.service_tier.clone(),
|
||||
startup_turn_metadata_header.as_deref(),
|
||||
|
||||
@@ -83,7 +83,7 @@ async fn handle_spawn_agent(
|
||||
sender_thread_id: session.thread_id,
|
||||
prompt: prompt.clone(),
|
||||
model: args.model.clone().unwrap_or_default(),
|
||||
reasoning_effort: args.reasoning_effort.unwrap_or_default(),
|
||||
reasoning_effort: args.reasoning_effort.clone().unwrap_or_default(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
@@ -94,14 +94,18 @@ async fn handle_spawn_agent(
|
||||
config.service_tier = Some(service_tier.clone());
|
||||
}
|
||||
if args.fork_context {
|
||||
reject_full_fork_spawn_overrides(role_name, args.model.as_deref(), args.reasoning_effort)?;
|
||||
reject_full_fork_spawn_overrides(
|
||||
role_name,
|
||||
args.model.as_deref(),
|
||||
args.reasoning_effort.clone(),
|
||||
)?;
|
||||
} else {
|
||||
apply_requested_spawn_agent_model_overrides(
|
||||
&session,
|
||||
turn.as_ref(),
|
||||
&mut config,
|
||||
args.model.as_deref(),
|
||||
args.reasoning_effort,
|
||||
args.reasoning_effort.clone(),
|
||||
)
|
||||
.await?;
|
||||
apply_role_to_config(&mut config, role_name)
|
||||
@@ -174,7 +178,7 @@ async fn handle_spawn_agent(
|
||||
.unwrap_or_else(|| args.model.clone().unwrap_or_default());
|
||||
let effective_reasoning_effort = agent_snapshot
|
||||
.as_ref()
|
||||
.and_then(|snapshot| snapshot.reasoning_effort)
|
||||
.and_then(|snapshot| snapshot.reasoning_effort.clone())
|
||||
.unwrap_or(args.reasoning_effort.unwrap_or_default());
|
||||
let nickname = new_agent_nickname.clone();
|
||||
session
|
||||
|
||||
@@ -224,7 +224,8 @@ fn build_agent_shared_config(turn: &TurnContext) -> Result<Config, FunctionCallE
|
||||
config.model_provider = turn.provider.info().clone();
|
||||
config.model_reasoning_effort = turn
|
||||
.reasoning_effort
|
||||
.or(turn.model_info.default_reasoning_level);
|
||||
.clone()
|
||||
.or_else(|| turn.model_info.default_reasoning_level.clone());
|
||||
config.model_reasoning_summary = Some(turn.reasoning_summary);
|
||||
config.developer_instructions = turn.developer_instructions.clone();
|
||||
config.compact_prompt = turn.compact_prompt.clone();
|
||||
@@ -304,7 +305,7 @@ pub(crate) async fn apply_requested_spawn_agent_model_overrides(
|
||||
validate_spawn_agent_reasoning_effort(
|
||||
&selected_model_name,
|
||||
&selected_model_info.supported_reasoning_levels,
|
||||
reasoning_effort,
|
||||
&reasoning_effort,
|
||||
)?;
|
||||
config.model_reasoning_effort = Some(reasoning_effort);
|
||||
} else {
|
||||
@@ -318,7 +319,7 @@ pub(crate) async fn apply_requested_spawn_agent_model_overrides(
|
||||
validate_spawn_agent_reasoning_effort(
|
||||
&turn.model_info.slug,
|
||||
&turn.model_info.supported_reasoning_levels,
|
||||
reasoning_effort,
|
||||
&reasoning_effort,
|
||||
)?;
|
||||
config.model_reasoning_effort = Some(reasoning_effort);
|
||||
}
|
||||
@@ -404,11 +405,11 @@ fn find_spawn_agent_model_name(
|
||||
fn validate_spawn_agent_reasoning_effort(
|
||||
model: &str,
|
||||
supported_reasoning_levels: &[ReasoningEffortPreset],
|
||||
requested_reasoning_effort: ReasoningEffort,
|
||||
requested_reasoning_effort: &ReasoningEffort,
|
||||
) -> Result<(), FunctionCallError> {
|
||||
if supported_reasoning_levels
|
||||
.iter()
|
||||
.any(|preset| preset.effort == requested_reasoning_effort)
|
||||
.any(|preset| &preset.effort == requested_reasoning_effort)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const SPAWN_AGENT_MODEL_OVERRIDE_DESCRIPTION: &str =
|
||||
const SPAWN_AGENT_SERVICE_TIER_OVERRIDE_DESCRIPTION: &str =
|
||||
"Service tier override for the new agent. Omit unless explicitly requested.";
|
||||
const MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT_DESCRIPTION: usize = 5;
|
||||
const MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION: usize = 64;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SpawnAgentToolOptions {
|
||||
@@ -762,13 +763,20 @@ fn spawn_agent_models_description(models: &[ModelPreset]) -> String {
|
||||
let model_descriptions = visible_models
|
||||
.into_iter()
|
||||
.map(|model| {
|
||||
let default_reasoning_effort = model.default_reasoning_effort;
|
||||
let default_reasoning_effort = &model.default_reasoning_effort;
|
||||
let efforts = model
|
||||
.supported_reasoning_efforts
|
||||
.iter()
|
||||
.map(|preset| {
|
||||
let effort = preset.effort;
|
||||
if effort == default_reasoning_effort {
|
||||
let effort = preset.effort.as_str();
|
||||
let effort = match effort
|
||||
.char_indices()
|
||||
.nth(MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION)
|
||||
{
|
||||
Some((index, _)) => &effort[..index],
|
||||
None => effort,
|
||||
};
|
||||
if &preset.effort == default_reasoning_effort {
|
||||
format!("{effort} (default)")
|
||||
} else {
|
||||
effort.to_string()
|
||||
|
||||
@@ -186,6 +186,27 @@ fn spawn_agent_tool_caps_visible_model_summaries() {
|
||||
assert!(!description.contains("`sixth-model`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_agent_tool_caps_reasoning_effort_value_length() {
|
||||
let mut model = model_preset("visible", /*show_in_picker*/ true);
|
||||
let custom_effort = ReasoningEffort::Custom(
|
||||
"é".repeat(MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION + 1),
|
||||
);
|
||||
model.default_reasoning_effort = custom_effort.clone();
|
||||
model.supported_reasoning_efforts = vec![ReasoningEffortPreset {
|
||||
effort: custom_effort,
|
||||
description: "Model-defined".to_string(),
|
||||
}];
|
||||
|
||||
assert_eq!(
|
||||
spawn_agent_models_description(&[model]),
|
||||
format!(
|
||||
"Available model overrides (optional; inherited parent model is preferred):\n- `visible-model`: visible description Reasoning efforts: {} (default). Service tiers: priority.",
|
||||
"é".repeat(MAX_REASONING_EFFORT_CHARS_IN_SPAWN_AGENT_DESCRIPTION)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_agent_tool_hides_service_tier_with_spawn_metadata() {
|
||||
let tool = create_spawn_agent_tool_v2(SpawnAgentToolOptions {
|
||||
|
||||
@@ -4374,7 +4374,7 @@ async fn build_agent_spawn_config_uses_turn_context_values() {
|
||||
expected.base_instructions = Some(base_instructions.text);
|
||||
expected.model = Some(turn.model_info.slug.clone());
|
||||
expected.model_provider = turn.provider.info().clone();
|
||||
expected.model_reasoning_effort = turn.reasoning_effort;
|
||||
expected.model_reasoning_effort = turn.reasoning_effort.clone();
|
||||
expected.model_reasoning_summary = Some(turn.reasoning_summary);
|
||||
expected.developer_instructions = turn.developer_instructions.clone();
|
||||
expected.compact_prompt = turn.compact_prompt.clone();
|
||||
@@ -4431,7 +4431,7 @@ async fn build_agent_resume_config_clears_base_instructions() {
|
||||
expected.base_instructions = None;
|
||||
expected.model = Some(turn.model_info.slug.clone());
|
||||
expected.model_provider = turn.provider.info().clone();
|
||||
expected.model_reasoning_effort = turn.reasoning_effort;
|
||||
expected.model_reasoning_effort = turn.reasoning_effort.clone();
|
||||
expected.model_reasoning_summary = Some(turn.reasoning_summary);
|
||||
expected.developer_instructions = turn.developer_instructions.clone();
|
||||
expected.compact_prompt = turn.compact_prompt.clone();
|
||||
|
||||
@@ -75,7 +75,7 @@ async fn handle_spawn_agent(
|
||||
sender_thread_id: session.thread_id,
|
||||
prompt: prompt.clone(),
|
||||
model: args.model.clone().unwrap_or_default(),
|
||||
reasoning_effort: args.reasoning_effort.unwrap_or_default(),
|
||||
reasoning_effort: args.reasoning_effort.clone().unwrap_or_default(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
@@ -86,14 +86,18 @@ async fn handle_spawn_agent(
|
||||
config.service_tier = Some(service_tier.clone());
|
||||
}
|
||||
if matches!(fork_mode, Some(SpawnAgentForkMode::FullHistory)) {
|
||||
reject_full_fork_spawn_overrides(role_name, args.model.as_deref(), args.reasoning_effort)?;
|
||||
reject_full_fork_spawn_overrides(
|
||||
role_name,
|
||||
args.model.as_deref(),
|
||||
args.reasoning_effort.clone(),
|
||||
)?;
|
||||
} else {
|
||||
apply_requested_spawn_agent_model_overrides(
|
||||
&session,
|
||||
turn.as_ref(),
|
||||
&mut config,
|
||||
args.model.as_deref(),
|
||||
args.reasoning_effort,
|
||||
args.reasoning_effort.clone(),
|
||||
)
|
||||
.await?;
|
||||
apply_role_to_config(&mut config, role_name)
|
||||
@@ -188,7 +192,7 @@ async fn handle_spawn_agent(
|
||||
.unwrap_or_else(|| args.model.clone().unwrap_or_default());
|
||||
let effective_reasoning_effort = agent_snapshot
|
||||
.as_ref()
|
||||
.and_then(|snapshot| snapshot.reasoning_effort)
|
||||
.and_then(|snapshot| snapshot.reasoning_effort.clone())
|
||||
.unwrap_or(args.reasoning_effort.unwrap_or_default());
|
||||
let nickname = new_agent_nickname.clone();
|
||||
session
|
||||
|
||||
@@ -74,7 +74,7 @@ async fn responses_stream_includes_subagent_header_on_review() {
|
||||
let mut config = load_default_config_for_test(&codex_home).await;
|
||||
config.model_provider_id = provider.name.clone();
|
||||
config.model_provider = provider.clone();
|
||||
let effort = config.model_reasoning_effort;
|
||||
let effort = config.model_reasoning_effort.clone();
|
||||
let summary = config.model_reasoning_summary;
|
||||
let model = codex_core::test_support::get_model_offline(config.model.as_deref());
|
||||
config.model = Some(model.clone());
|
||||
@@ -202,7 +202,7 @@ async fn responses_stream_includes_subagent_header_on_other() {
|
||||
let mut config = load_default_config_for_test(&codex_home).await;
|
||||
config.model_provider_id = provider.name.clone();
|
||||
config.model_provider = provider.clone();
|
||||
let effort = config.model_reasoning_effort;
|
||||
let effort = config.model_reasoning_effort.clone();
|
||||
let summary = config.model_reasoning_summary;
|
||||
let model = codex_core::test_support::get_model_offline(config.model.as_deref());
|
||||
config.model = Some(model.clone());
|
||||
@@ -318,7 +318,7 @@ async fn responses_respects_model_info_overrides_from_config() {
|
||||
config.model_provider = provider.clone();
|
||||
config.model_supports_reasoning_summaries = Some(true);
|
||||
config.model_reasoning_summary = Some(ReasoningSummary::Detailed);
|
||||
let effort = config.model_reasoning_effort;
|
||||
let effort = config.model_reasoning_effort.clone();
|
||||
let summary = config.model_reasoning_summary;
|
||||
let model = config.model.clone().expect("model configured");
|
||||
let config = Arc::new(config);
|
||||
|
||||
@@ -876,7 +876,7 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth
|
||||
let mut config = load_default_config_for_test(&codex_home).await;
|
||||
config.model_provider_id = provider.name.clone();
|
||||
config.model_provider = provider.clone();
|
||||
let effort = config.model_reasoning_effort;
|
||||
let effort = config.model_reasoning_effort.clone();
|
||||
let summary = config.model_reasoning_summary;
|
||||
let model = codex_core::test_support::get_model_offline(config.model.as_deref());
|
||||
config.model = Some(model.clone());
|
||||
@@ -2332,7 +2332,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
|
||||
let mut config = load_default_config_for_test(&codex_home).await;
|
||||
config.model_provider_id = provider.name.clone();
|
||||
config.model_provider = provider.clone();
|
||||
let effort = config.model_reasoning_effort;
|
||||
let effort = config.model_reasoning_effort.clone();
|
||||
let summary = config.model_reasoning_summary;
|
||||
let model = codex_core::test_support::get_model_offline(config.model.as_deref());
|
||||
config.model = Some(model.clone());
|
||||
|
||||
@@ -343,7 +343,7 @@ async fn responses_websocket_request_prewarm_reuses_connection() {
|
||||
&prompt,
|
||||
&harness.model_info,
|
||||
&harness.session_telemetry,
|
||||
harness.effort,
|
||||
harness.effort.clone(),
|
||||
harness.summary,
|
||||
/*service_tier*/ None,
|
||||
/*turn_metadata_header*/ None,
|
||||
@@ -397,7 +397,7 @@ async fn responses_websocket_request_prewarm_traces_logical_request() {
|
||||
&prompt,
|
||||
&harness.model_info,
|
||||
&harness.session_telemetry,
|
||||
harness.effort,
|
||||
harness.effort.clone(),
|
||||
harness.summary,
|
||||
/*service_tier*/ None,
|
||||
/*turn_metadata_header*/ None,
|
||||
@@ -442,7 +442,7 @@ async fn responses_websocket_request_prewarm_traces_logical_request() {
|
||||
&prompt,
|
||||
&harness.model_info,
|
||||
&harness.session_telemetry,
|
||||
harness.effort,
|
||||
harness.effort.clone(),
|
||||
harness.summary,
|
||||
/*service_tier*/ None,
|
||||
/*turn_metadata_header*/ None,
|
||||
@@ -534,7 +534,7 @@ async fn responses_websocket_preconnect_is_reused_even_with_header_changes() {
|
||||
&prompt,
|
||||
&harness.model_info,
|
||||
&harness.session_telemetry,
|
||||
harness.effort,
|
||||
harness.effort.clone(),
|
||||
harness.summary,
|
||||
/*service_tier*/ None,
|
||||
/*turn_metadata_header*/ None,
|
||||
@@ -573,7 +573,7 @@ async fn responses_websocket_request_prewarm_is_reused_even_with_header_changes(
|
||||
&prompt,
|
||||
&harness.model_info,
|
||||
&harness.session_telemetry,
|
||||
harness.effort,
|
||||
harness.effort.clone(),
|
||||
harness.summary,
|
||||
/*service_tier*/ None,
|
||||
/*turn_metadata_header*/ None,
|
||||
@@ -585,7 +585,7 @@ async fn responses_websocket_request_prewarm_is_reused_even_with_header_changes(
|
||||
&prompt,
|
||||
&harness.model_info,
|
||||
&harness.session_telemetry,
|
||||
harness.effort,
|
||||
harness.effort.clone(),
|
||||
harness.summary,
|
||||
/*service_tier*/ None,
|
||||
/*turn_metadata_header*/ None,
|
||||
@@ -639,7 +639,7 @@ async fn responses_websocket_prewarm_uses_v2_when_provider_supports_websockets()
|
||||
&prompt,
|
||||
&harness.model_info,
|
||||
&harness.session_telemetry,
|
||||
harness.effort,
|
||||
harness.effort.clone(),
|
||||
harness.summary,
|
||||
/*service_tier*/ None,
|
||||
/*turn_metadata_header*/ None,
|
||||
@@ -988,7 +988,7 @@ async fn responses_websocket_emits_reasoning_included_event() {
|
||||
&prompt,
|
||||
&harness.model_info,
|
||||
&harness.session_telemetry,
|
||||
harness.effort,
|
||||
harness.effort.clone(),
|
||||
harness.summary,
|
||||
/*service_tier*/ None,
|
||||
/*turn_metadata_header*/ None,
|
||||
@@ -1062,7 +1062,7 @@ async fn responses_websocket_emits_rate_limit_events() {
|
||||
&prompt,
|
||||
&harness.model_info,
|
||||
&harness.session_telemetry,
|
||||
harness.effort,
|
||||
harness.effort.clone(),
|
||||
harness.summary,
|
||||
/*service_tier*/ None,
|
||||
/*turn_metadata_header*/ None,
|
||||
@@ -1718,7 +1718,7 @@ async fn responses_websocket_v2_after_error_uses_full_create_without_previous_re
|
||||
&prompt_two,
|
||||
&harness.model_info,
|
||||
&harness.session_telemetry,
|
||||
harness.effort,
|
||||
harness.effort.clone(),
|
||||
harness.summary,
|
||||
/*service_tier*/ None,
|
||||
/*turn_metadata_header*/ None,
|
||||
@@ -1806,7 +1806,7 @@ async fn responses_websocket_v2_surfaces_terminal_error_without_close_handshake(
|
||||
&prompt_two,
|
||||
&harness.model_info,
|
||||
&harness.session_telemetry,
|
||||
harness.effort,
|
||||
harness.effort.clone(),
|
||||
harness.summary,
|
||||
/*service_tier*/ None,
|
||||
/*turn_metadata_header*/ None,
|
||||
@@ -2073,7 +2073,7 @@ async fn stream_until_complete_with_request_metadata(
|
||||
prompt,
|
||||
&harness.model_info,
|
||||
&harness.session_telemetry,
|
||||
harness.effort,
|
||||
harness.effort.clone(),
|
||||
harness.summary,
|
||||
service_tier.map(|service_tier| service_tier.request_value().to_string()),
|
||||
turn_metadata_header,
|
||||
|
||||
@@ -58,7 +58,7 @@ fn read_only_user_turn(test: &TestCodex, items: Vec<UserInput>, model: String) -
|
||||
mode: codex_protocol::config_types::ModeKind::Default,
|
||||
settings: codex_protocol::config_types::Settings {
|
||||
model,
|
||||
reasoning_effort: test.config.model_reasoning_effort,
|
||||
reasoning_effort: test.config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -135,7 +135,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
|
||||
mode: codex_protocol::config_types::ModeKind::Default,
|
||||
settings: codex_protocol::config_types::Settings {
|
||||
model: test.session_configured.model.clone(),
|
||||
reasoning_effort: test.config.model_reasoning_effort,
|
||||
reasoning_effort: test.config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
}),
|
||||
@@ -172,7 +172,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
|
||||
mode: codex_protocol::config_types::ModeKind::Default,
|
||||
settings: codex_protocol::config_types::Settings {
|
||||
model: test.session_configured.model.clone(),
|
||||
reasoning_effort: test.config.model_reasoning_effort,
|
||||
reasoning_effort: test.config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
}),
|
||||
@@ -261,7 +261,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
|
||||
mode: codex_protocol::config_types::ModeKind::Default,
|
||||
settings: codex_protocol::config_types::Settings {
|
||||
model: test.session_configured.model.clone(),
|
||||
reasoning_effort: test.config.model_reasoning_effort,
|
||||
reasoning_effort: test.config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
}),
|
||||
@@ -295,7 +295,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
|
||||
mode: codex_protocol::config_types::ModeKind::Default,
|
||||
settings: codex_protocol::config_types::Settings {
|
||||
model: test.session_configured.model.clone(),
|
||||
reasoning_effort: test.config.model_reasoning_effort,
|
||||
reasoning_effort: test.config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
}),
|
||||
@@ -422,7 +422,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
|
||||
mode: codex_protocol::config_types::ModeKind::Default,
|
||||
settings: codex_protocol::config_types::Settings {
|
||||
model: resumed.session_configured.model.clone(),
|
||||
reasoning_effort: resumed.config.model_reasoning_effort,
|
||||
reasoning_effort: resumed.config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -79,7 +79,7 @@ fn read_only_text_turn_with_personality(
|
||||
mode: codex_protocol::config_types::ModeKind::Default,
|
||||
settings: codex_protocol::config_types::Settings {
|
||||
model,
|
||||
reasoning_effort: test.config.model_reasoning_effort,
|
||||
reasoning_effort: test.config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -870,7 +870,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
|
||||
let default_approval_policy = config.permissions.approval_policy.value();
|
||||
let default_sandbox_policy = &config.legacy_sandbox_policy();
|
||||
let default_model = session_configured.model;
|
||||
let default_effort = config.model_reasoning_effort;
|
||||
let default_effort = config.model_reasoning_effort.clone();
|
||||
let default_summary = config.model_reasoning_summary;
|
||||
|
||||
codex
|
||||
@@ -892,7 +892,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
|
||||
mode: codex_protocol::config_types::ModeKind::Default,
|
||||
settings: codex_protocol::config_types::Settings {
|
||||
model: default_model.clone(),
|
||||
reasoning_effort: default_effort,
|
||||
reasoning_effort: default_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
}),
|
||||
@@ -1013,7 +1013,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
|
||||
let default_approval_policy = config.permissions.approval_policy.value();
|
||||
let default_sandbox_policy = &config.legacy_sandbox_policy();
|
||||
let default_model = session_configured.model;
|
||||
let default_effort = config.model_reasoning_effort;
|
||||
let default_effort = config.model_reasoning_effort.clone();
|
||||
let default_summary = config.model_reasoning_summary;
|
||||
|
||||
codex
|
||||
|
||||
@@ -318,7 +318,7 @@ async fn remote_models_use_context_window_when_config_override_is_absent() -> Re
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<()> {
|
||||
async fn remote_models_long_model_slug_is_sent_with_custom_reasoning() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
@@ -331,15 +331,16 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<(
|
||||
/*priority*/ 1_000,
|
||||
TruncationPolicyConfig::bytes(/*limit*/ 10_000),
|
||||
);
|
||||
remote_model.default_reasoning_level = Some(ReasoningEffort::High);
|
||||
let custom_reasoning_effort = ReasoningEffort::Custom("max".to_string());
|
||||
remote_model.default_reasoning_level = Some(custom_reasoning_effort.clone());
|
||||
remote_model.supported_reasoning_levels = vec![
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::Medium,
|
||||
description: ReasoningEffort::Medium.to_string(),
|
||||
},
|
||||
ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::High,
|
||||
description: ReasoningEffort::High.to_string(),
|
||||
effort: custom_reasoning_effort.clone(),
|
||||
description: custom_reasoning_effort.to_string(),
|
||||
},
|
||||
];
|
||||
remote_model.supports_reasoning_summaries = true;
|
||||
@@ -393,7 +394,7 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<(
|
||||
.and_then(|reasoning| reasoning.get("summary"))
|
||||
.and_then(|value| value.as_str());
|
||||
assert_eq!(body["model"].as_str(), Some(requested_model));
|
||||
assert_eq!(reasoning_effort, Some("high"));
|
||||
assert_eq!(reasoning_effort, Some("max"));
|
||||
assert_eq!(reasoning_summary, Some("detailed"));
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -41,7 +41,7 @@ fn resume_history(
|
||||
collaboration_mode: None,
|
||||
multi_agent_version: None,
|
||||
realtime_active: None,
|
||||
effort: config.model_reasoning_effort,
|
||||
effort: config.model_reasoning_effort.clone(),
|
||||
summary: config
|
||||
.model_reasoning_summary
|
||||
.unwrap_or(ReasoningSummary::Auto),
|
||||
|
||||
@@ -55,7 +55,7 @@ fn disabled_text_turn(test: &TestCodex, text: &str) -> Op {
|
||||
mode: codex_protocol::config_types::ModeKind::Default,
|
||||
settings: codex_protocol::config_types::Settings {
|
||||
model: REQUESTED_MODEL.to_string(),
|
||||
reasoning_effort: test.config.model_reasoning_effort,
|
||||
reasoning_effort: test.config.model_reasoning_effort.clone(),
|
||||
developer_instructions: None,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -68,7 +68,7 @@ Each response yields:
|
||||
- `data` - ordered list of models. A model includes:
|
||||
- `id`, `model`, `displayName`, `description`
|
||||
- `supportedReasoningEfforts` - array of objects with:
|
||||
- `reasoningEffort` - one of `none|minimal|low|medium|high|xhigh`
|
||||
- `reasoningEffort` - a string value advertised by the model; common values are `none|minimal|low|medium|high|xhigh`
|
||||
- `description` - human-friendly label for the effort
|
||||
- `defaultReasoningEffort` - suggested effort for the UI
|
||||
- `inputModalities` - accepted input types for the model
|
||||
|
||||
@@ -446,7 +446,8 @@ fn config_summary_entries(
|
||||
"reasoning effort",
|
||||
config
|
||||
.model_reasoning_effort
|
||||
.map(|effort| effort.to_string())
|
||||
.as_ref()
|
||||
.map(std::string::ToString::to_string)
|
||||
.unwrap_or_else(|| "none".to_string()),
|
||||
));
|
||||
entries.push((
|
||||
|
||||
@@ -694,7 +694,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
|
||||
let default_cwd = config.cwd.to_path_buf();
|
||||
let default_approval_policy = config.permissions.approval_policy.value();
|
||||
let default_effort = config.model_reasoning_effort;
|
||||
let default_effort = config.model_reasoning_effort.clone();
|
||||
|
||||
let (initial_operation, prompt_summary) = match (command.as_ref(), prompt, images) {
|
||||
(Some(ExecCommand::Review(review_cli)), _, _) => {
|
||||
@@ -1169,7 +1169,7 @@ fn session_configured_from_thread_start_response(
|
||||
config.permissions.effective_permission_profile(),
|
||||
response.active_permission_profile.clone().map(Into::into),
|
||||
response.cwd.clone(),
|
||||
response.reasoning_effort,
|
||||
response.reasoning_effort.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1192,7 +1192,7 @@ fn session_configured_from_thread_resume_response(
|
||||
config.permissions.effective_permission_profile(),
|
||||
response.active_permission_profile.clone().map(Into::into),
|
||||
response.cwd.clone(),
|
||||
response.reasoning_effort,
|
||||
response.reasoning_effort.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ impl MemoryStartupContext {
|
||||
prompt,
|
||||
&context.model_info,
|
||||
&context.session_telemetry,
|
||||
context.reasoning_effort,
|
||||
context.reasoning_effort.clone(),
|
||||
context.reasoning_summary,
|
||||
context.service_tier.clone(),
|
||||
context.turn_metadata_header.as_deref(),
|
||||
|
||||
@@ -458,7 +458,7 @@ impl SessionTelemetry {
|
||||
auth.env_provider_key_name = self.metadata.auth_env.provider_env_key_name.as_deref(),
|
||||
auth.env_provider_key_present = self.metadata.auth_env.provider_env_key_present,
|
||||
auth.env_refresh_token_url_override_present = self.metadata.auth_env.refresh_token_url_override_present,
|
||||
reasoning_effort = reasoning_effort.map(|e| e.to_string()),
|
||||
reasoning_effort = reasoning_effort.as_ref().map(ToString::to_string),
|
||||
reasoning_summary = %reasoning_summary,
|
||||
context_window = context_window,
|
||||
auto_compact_token_limit = auto_compact_token_limit,
|
||||
|
||||
@@ -59,4 +59,5 @@ tempfile = { workspace = true }
|
||||
[package.metadata.cargo-shear]
|
||||
# Required because:
|
||||
# `icu_provider`: contains a required `sync` feature for `icu_decimal`
|
||||
ignored = ["icu_provider"]
|
||||
# `strum`: is referenced by generated `EnumIter` derive implementations
|
||||
ignored = ["icu_provider", "strum"]
|
||||
|
||||
@@ -635,7 +635,7 @@ impl CollaborationMode {
|
||||
}
|
||||
|
||||
pub fn reasoning_effort(&self) -> Option<ReasoningEffort> {
|
||||
self.settings_ref().reasoning_effort
|
||||
self.settings_ref().reasoning_effort.clone()
|
||||
}
|
||||
|
||||
/// Updates the collaboration mode with new model and/or effort values.
|
||||
@@ -654,7 +654,7 @@ impl CollaborationMode {
|
||||
let settings = self.settings_ref();
|
||||
let updated_settings = Settings {
|
||||
model: model.unwrap_or_else(|| settings.model.clone()),
|
||||
reasoning_effort: effort.unwrap_or(settings.reasoning_effort),
|
||||
reasoning_effort: effort.unwrap_or_else(|| settings.reasoning_effort.clone()),
|
||||
developer_instructions: developer_instructions
|
||||
.unwrap_or_else(|| settings.developer_instructions.clone()),
|
||||
};
|
||||
@@ -676,7 +676,10 @@ impl CollaborationMode {
|
||||
mode: mask.mode.unwrap_or(self.mode),
|
||||
settings: Settings {
|
||||
model: mask.model.clone().unwrap_or_else(|| settings.model.clone()),
|
||||
reasoning_effort: mask.reasoning_effort.unwrap_or(settings.reasoning_effort),
|
||||
reasoning_effort: mask
|
||||
.reasoning_effort
|
||||
.clone()
|
||||
.unwrap_or_else(|| settings.reasoning_effort.clone()),
|
||||
developer_instructions: mask
|
||||
.developer_instructions
|
||||
.clone()
|
||||
|
||||
@@ -4,14 +4,22 @@
|
||||
//! are used to preserve compatibility when older payloads omit newly introduced attributes.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use schemars::r#gen::SchemaGenerator;
|
||||
use schemars::schema::InstanceType;
|
||||
use schemars::schema::Metadata;
|
||||
use schemars::schema::Schema;
|
||||
use schemars::schema::SchemaObject;
|
||||
use schemars::schema::StringValidation;
|
||||
use serde::Deserialize;
|
||||
use serde::Deserializer;
|
||||
use serde::Serialize;
|
||||
use serde::Serializer;
|
||||
use serde::de::DeserializeOwned;
|
||||
use strum::IntoEnumIterator;
|
||||
use serde::de::Error;
|
||||
use strum_macros::Display;
|
||||
use strum_macros::EnumIter;
|
||||
use tracing::warn;
|
||||
@@ -28,23 +36,8 @@ const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}";
|
||||
pub const SPEED_TIER_FAST: &str = "fast";
|
||||
|
||||
/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
|
||||
#[derive(
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Default,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Display,
|
||||
JsonSchema,
|
||||
TS,
|
||||
EnumIter,
|
||||
Hash,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, TS, Hash)]
|
||||
#[ts(type = "string")]
|
||||
pub enum ReasoningEffort {
|
||||
None,
|
||||
Minimal,
|
||||
@@ -53,14 +46,113 @@ pub enum ReasoningEffort {
|
||||
Medium,
|
||||
High,
|
||||
XHigh,
|
||||
/// A model-defined effort value that this client does not know yet.
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl ReasoningEffort {
|
||||
/// Returns the exact value used on the wire.
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::None => "none",
|
||||
Self::Minimal => "minimal",
|
||||
Self::Low => "low",
|
||||
Self::Medium => "medium",
|
||||
Self::High => "high",
|
||||
Self::XHigh => "xhigh",
|
||||
Self::Custom(effort) => effort,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the built-in effort values in ascending order.
|
||||
pub fn known_values() -> impl DoubleEndedIterator<Item = Self> + ExactSizeIterator {
|
||||
[
|
||||
Self::None,
|
||||
Self::Minimal,
|
||||
Self::Low,
|
||||
Self::Medium,
|
||||
Self::High,
|
||||
Self::XHigh,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
/// Returns the built-in ordering rank, or `None` for model-defined values.
|
||||
pub const fn known_rank(&self) -> Option<usize> {
|
||||
match self {
|
||||
Self::None => Some(0),
|
||||
Self::Minimal => Some(1),
|
||||
Self::Low => Some(2),
|
||||
Self::Medium => Some(3),
|
||||
Self::High => Some(4),
|
||||
Self::XHigh => Some(5),
|
||||
Self::Custom(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ReasoningEffort {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for ReasoningEffort {
|
||||
fn schema_name() -> String {
|
||||
"ReasoningEffort".to_string()
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
description: Some(
|
||||
"A non-empty reasoning effort value advertised by the model.".to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
})),
|
||||
string: Some(Box::new(StringValidation {
|
||||
min_length: Some(1),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ReasoningEffort {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ReasoningEffort {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let effort = String::deserialize(deserializer)?;
|
||||
effort.parse().map_err(D::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ReasoningEffort {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
serde_json::from_value(serde_json::Value::String(s.to_string()))
|
||||
.map_err(|_| format!("invalid reasoning_effort: {s}"))
|
||||
match s {
|
||||
"none" => Ok(Self::None),
|
||||
"minimal" => Ok(Self::Minimal),
|
||||
"low" => Ok(Self::Low),
|
||||
"medium" => Ok(Self::Medium),
|
||||
"high" => Ok(Self::High),
|
||||
"xhigh" => Ok(Self::XHigh),
|
||||
"" => Err("reasoning_effort must not be empty".to_string()),
|
||||
effort => Ok(Self::Custom(effort.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,39 +671,43 @@ fn reasoning_effort_mapping_from_presets(
|
||||
}
|
||||
|
||||
// Map every canonical effort to the closest supported effort for the new model.
|
||||
let supported: Vec<ReasoningEffort> = presets.iter().map(|p| p.effort).collect();
|
||||
let supported: Vec<ReasoningEffort> = presets.iter().map(|p| p.effort.clone()).collect();
|
||||
let mut map = HashMap::new();
|
||||
for effort in ReasoningEffort::iter() {
|
||||
let nearest = nearest_effort(effort, &supported);
|
||||
for effort in ReasoningEffort::known_values() {
|
||||
let nearest = nearest_effort(&effort, &supported);
|
||||
map.insert(effort, nearest);
|
||||
}
|
||||
Some(map)
|
||||
}
|
||||
|
||||
fn effort_rank(effort: ReasoningEffort) -> i32 {
|
||||
match effort {
|
||||
ReasoningEffort::None => 0,
|
||||
ReasoningEffort::Minimal => 1,
|
||||
ReasoningEffort::Low => 2,
|
||||
ReasoningEffort::Medium => 3,
|
||||
ReasoningEffort::High => 4,
|
||||
ReasoningEffort::XHigh => 5,
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_effort(target: ReasoningEffort, supported: &[ReasoningEffort]) -> ReasoningEffort {
|
||||
let target_rank = effort_rank(target);
|
||||
fn nearest_effort(target: &ReasoningEffort, supported: &[ReasoningEffort]) -> ReasoningEffort {
|
||||
let Some(target_rank) = target.known_rank() else {
|
||||
return supported
|
||||
.iter()
|
||||
.find(|candidate| *candidate == target)
|
||||
.unwrap_or(target)
|
||||
.clone();
|
||||
};
|
||||
supported
|
||||
.iter()
|
||||
.copied()
|
||||
.min_by_key(|candidate| (effort_rank(*candidate) - target_rank).abs())
|
||||
.filter_map(|candidate| {
|
||||
candidate
|
||||
.known_rank()
|
||||
.map(|rank| (rank.abs_diff(target_rank), candidate))
|
||||
})
|
||||
.min_by_key(|(distance, _)| *distance)
|
||||
.map(|(_, effort)| effort)
|
||||
.or_else(|| supported.first())
|
||||
.unwrap_or(target)
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::from_str;
|
||||
use serde_json::to_string;
|
||||
|
||||
fn test_model(spec: Option<ModelMessages>) -> ModelInfo {
|
||||
ModelInfo {
|
||||
@@ -663,16 +759,58 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_effort_from_str_accepts_known_values() {
|
||||
assert_eq!("high".parse(), Ok(ReasoningEffort::High));
|
||||
assert_eq!("minimal".parse(), Ok(ReasoningEffort::Minimal));
|
||||
fn reasoning_effort_accepts_known_and_custom_values() {
|
||||
let custom = ReasoningEffort::Custom("max".to_string());
|
||||
let deserialized = from_str::<ReasoningEffort>(r#""max""#)
|
||||
.expect("custom reasoning effort should deserialize");
|
||||
let serialized = to_string(&custom).expect("custom reasoning effort should serialize");
|
||||
|
||||
assert_eq!(
|
||||
(
|
||||
"high".parse(),
|
||||
"max".parse(),
|
||||
deserialized,
|
||||
serialized,
|
||||
custom.to_string(),
|
||||
),
|
||||
(
|
||||
Ok(ReasoningEffort::High),
|
||||
Ok(custom.clone()),
|
||||
custom,
|
||||
r#""max""#.to_string(),
|
||||
"max".to_string(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_effort_from_str_rejects_unknown_values() {
|
||||
fn reasoning_effort_rejects_empty_values() {
|
||||
assert_eq!(
|
||||
"unsupported".parse::<ReasoningEffort>(),
|
||||
Err("invalid reasoning_effort: unsupported".to_string())
|
||||
"".parse::<ReasoningEffort>(),
|
||||
Err("reasoning_effort must not be empty".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_effort_json_schema_is_an_open_string() {
|
||||
let mut effort_generator = SchemaGenerator::default();
|
||||
|
||||
assert_eq!(
|
||||
ReasoningEffort::json_schema(&mut effort_generator),
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
description: Some(
|
||||
"A non-empty reasoning effort value advertised by the model.".to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
})),
|
||||
string: Some(Box::new(StringValidation {
|
||||
min_length: Some(1),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ fn apply_turn_context(metadata: &mut ThreadMetadata, turn_ctx: &TurnContextItem)
|
||||
metadata.cwd = turn_ctx.cwd.clone();
|
||||
}
|
||||
metadata.model = Some(turn_ctx.model.clone());
|
||||
metadata.reasoning_effort = turn_ctx.effort;
|
||||
metadata.reasoning_effort = turn_ctx.effort.clone();
|
||||
metadata.sandbox_policy =
|
||||
serde_json::to_string(&turn_ctx.permission_profile()).unwrap_or_default();
|
||||
metadata.approval_mode = enum_to_string(&turn_ctx.approval_policy);
|
||||
|
||||
@@ -584,13 +584,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_row_ignores_unknown_reasoning_effort_values() {
|
||||
fn thread_row_preserves_model_defined_reasoning_effort_values() {
|
||||
let metadata = ThreadMetadata::try_from(thread_row(Some("future")))
|
||||
.expect("thread metadata should parse");
|
||||
|
||||
assert_eq!(
|
||||
metadata,
|
||||
expected_thread_metadata(/*reasoning_effort*/ None)
|
||||
expected_thread_metadata(Some(ReasoningEffort::Custom("future".to_string())))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,7 +369,7 @@ fn stored_thread_from_state(
|
||||
.and_then(|metadata| metadata.model_provider.clone())
|
||||
.unwrap_or_else(|| "test".to_string()),
|
||||
model: metadata.and_then(|metadata| metadata.model.clone()),
|
||||
reasoning_effort: metadata.and_then(|metadata| metadata.reasoning_effort),
|
||||
reasoning_effort: metadata.and_then(|metadata| metadata.reasoning_effort.clone()),
|
||||
created_at: metadata
|
||||
.and_then(|metadata| metadata.created_at)
|
||||
.unwrap_or_else(Utc::now),
|
||||
|
||||
@@ -232,7 +232,7 @@ impl ThreadMetadataSync {
|
||||
update.cwd = Some(turn_ctx.cwd.clone());
|
||||
}
|
||||
update.model = Some(turn_ctx.model.clone());
|
||||
update.reasoning_effort = turn_ctx.effort;
|
||||
update.reasoning_effort = turn_ctx.effort.clone();
|
||||
update.approval_mode = Some(turn_ctx.approval_policy);
|
||||
update.permission_profile = Some(turn_ctx.permission_profile());
|
||||
}
|
||||
|
||||
@@ -712,21 +712,17 @@ impl App {
|
||||
.add_info_message("Reset local memories.".to_string(), /*hint*/ None);
|
||||
}
|
||||
|
||||
pub(super) fn reasoning_label(reasoning_effort: Option<ReasoningEffortConfig>) -> &'static str {
|
||||
pub(super) fn reasoning_label(reasoning_effort: Option<&ReasoningEffortConfig>) -> String {
|
||||
match reasoning_effort {
|
||||
Some(ReasoningEffortConfig::Minimal) => "minimal",
|
||||
Some(ReasoningEffortConfig::Low) => "low",
|
||||
Some(ReasoningEffortConfig::Medium) => "medium",
|
||||
Some(ReasoningEffortConfig::High) => "high",
|
||||
Some(ReasoningEffortConfig::XHigh) => "xhigh",
|
||||
None | Some(ReasoningEffortConfig::None) => "default",
|
||||
None | Some(ReasoningEffortConfig::None) => "default".to_string(),
|
||||
Some(reasoning_effort) => reasoning_effort.as_str().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn reasoning_label_for(
|
||||
model: &str,
|
||||
reasoning_effort: Option<ReasoningEffortConfig>,
|
||||
) -> Option<&'static str> {
|
||||
reasoning_effort: Option<&ReasoningEffortConfig>,
|
||||
) -> Option<String> {
|
||||
(!model.starts_with("codex-auto-")).then(|| Self::reasoning_label(reasoning_effort))
|
||||
}
|
||||
|
||||
@@ -737,7 +733,7 @@ impl App {
|
||||
pub(super) fn on_update_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) {
|
||||
// TODO(aibrahim): Remove this and don't use config as a state object.
|
||||
// Instead, explicitly pass the stored collaboration mode's effort into new sessions.
|
||||
self.config.model_reasoning_effort = effort;
|
||||
self.config.model_reasoning_effort = effort.clone();
|
||||
self.chat_widget.set_reasoning_effort(effort);
|
||||
}
|
||||
|
||||
|
||||
@@ -750,7 +750,7 @@ impl App {
|
||||
self.chat_widget.on_connectors_loaded(result, is_final);
|
||||
}
|
||||
AppEvent::UpdateReasoningEffort(effort) => {
|
||||
self.on_update_reasoning_effort(effort);
|
||||
self.on_update_reasoning_effort(effort.clone());
|
||||
self.sync_active_thread_reasoning_setting(app_server, effort)
|
||||
.await;
|
||||
}
|
||||
@@ -1300,19 +1300,23 @@ impl App {
|
||||
AppEvent::PersistModelSelection { model, effort } => {
|
||||
match crate::config_update::write_config_batch(
|
||||
app_server.request_handle(),
|
||||
crate::config_update::build_model_selection_edits(model.as_str(), effort),
|
||||
crate::config_update::build_model_selection_edits(
|
||||
model.as_str(),
|
||||
effort.as_ref(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let effort_label = effort
|
||||
.map(|selected_effort| selected_effort.to_string())
|
||||
.as_ref()
|
||||
.map(std::string::ToString::to_string)
|
||||
.unwrap_or_else(|| "default".to_string());
|
||||
tracing::info!("Selected model: {model}, Selected effort: {effort_label}");
|
||||
let mut message = format!("Model changed to {model}");
|
||||
if let Some(label) = Self::reasoning_label_for(&model, effort) {
|
||||
if let Some(label) = Self::reasoning_label_for(&model, effort.as_ref()) {
|
||||
message.push(' ');
|
||||
message.push_str(label);
|
||||
message.push_str(&label);
|
||||
}
|
||||
self.chat_widget.add_info_message(message, /*hint*/ None);
|
||||
}
|
||||
@@ -1607,7 +1611,7 @@ impl App {
|
||||
self.chat_widget.set_rate_limit_switch_prompt_hidden(hidden);
|
||||
}
|
||||
AppEvent::UpdatePlanModeReasoningEffort(effort) => {
|
||||
self.config.plan_mode_reasoning_effort = effort;
|
||||
self.config.plan_mode_reasoning_effort = effort.clone();
|
||||
self.chat_widget.set_plan_mode_reasoning_effort(effort);
|
||||
self.sync_active_thread_plan_mode_reasoning_setting(app_server)
|
||||
.await;
|
||||
|
||||
@@ -152,9 +152,11 @@ pub(super) fn apply_accepted_model_migration(
|
||||
});
|
||||
|
||||
config.model = Some(target_model.clone());
|
||||
config.model_reasoning_effort = Some(target_default_effort);
|
||||
config.model_reasoning_effort = Some(target_default_effort.clone());
|
||||
app_event_tx.send(AppEvent::UpdateModel(target_model.clone()));
|
||||
app_event_tx.send(AppEvent::UpdateReasoningEffort(Some(target_default_effort)));
|
||||
app_event_tx.send(AppEvent::UpdateReasoningEffort(Some(
|
||||
target_default_effort.clone(),
|
||||
)));
|
||||
app_event_tx.send(AppEvent::PersistModelSelection {
|
||||
model: target_model,
|
||||
effort: Some(target_default_effort),
|
||||
@@ -290,7 +292,7 @@ pub(super) async fn handle_model_migration_prompt_if_needed(
|
||||
app_event_tx,
|
||||
model.to_string(),
|
||||
target_model.clone(),
|
||||
target_preset.default_reasoning_effort,
|
||||
target_preset.default_reasoning_effort.clone(),
|
||||
);
|
||||
}
|
||||
ModelMigrationOutcome::Rejected => {
|
||||
|
||||
@@ -5241,7 +5241,7 @@ async fn override_turn_context_sends_thread_settings_update() {
|
||||
.expect("thread/start should succeed");
|
||||
let thread_id = started.session.thread_id;
|
||||
let initial_model = started.session.model.clone();
|
||||
let initial_effort = started.session.reasoning_effort;
|
||||
let initial_effort = started.session.reasoning_effort.clone();
|
||||
app.enqueue_primary_thread_session(started.session, started.turns)
|
||||
.await
|
||||
.expect("primary thread should be registered");
|
||||
@@ -5466,7 +5466,7 @@ async fn inactive_thread_settings_notification_updates_cached_collaboration_mode
|
||||
model: "gpt-plan".to_string(),
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
effort: collaboration_mode.settings.reasoning_effort,
|
||||
effort: collaboration_mode.settings.reasoning_effort.clone(),
|
||||
summary: None,
|
||||
collaboration_mode: collaboration_mode.clone(),
|
||||
personality: Some(Personality::Pragmatic),
|
||||
|
||||
@@ -606,7 +606,7 @@ impl App {
|
||||
permissions_override,
|
||||
config.permissions.user_visible_workspace_roots(),
|
||||
model.to_string(),
|
||||
*effort,
|
||||
effort.clone(),
|
||||
*summary,
|
||||
service_tier.clone(),
|
||||
collaboration_mode.clone(),
|
||||
|
||||
@@ -124,7 +124,7 @@ impl App {
|
||||
.as_ref()
|
||||
.map(|profile| profile.id.clone()),
|
||||
model: model.clone(),
|
||||
effort: effort.unwrap_or_default(),
|
||||
effort: effort.clone().unwrap_or_default(),
|
||||
summary: *summary,
|
||||
service_tier: service_tier.clone(),
|
||||
collaboration_mode: collaboration_mode.clone(),
|
||||
@@ -172,7 +172,7 @@ impl App {
|
||||
fn apply_thread_settings_to_session(session: &mut ThreadSessionState, settings: &ThreadSettings) {
|
||||
if settings.collaboration_mode.mode == ModeKind::Default {
|
||||
session.model = settings.model.clone();
|
||||
session.reasoning_effort = settings.effort;
|
||||
session.reasoning_effort = settings.effort.clone();
|
||||
}
|
||||
session.model_provider_id = settings.model_provider.clone();
|
||||
session.service_tier = settings.service_tier.clone();
|
||||
@@ -190,7 +190,7 @@ fn apply_thread_settings_to_session(session: &mut ThreadSessionState, settings:
|
||||
.settings
|
||||
.model
|
||||
.clone_from(&settings.model);
|
||||
collaboration_mode.settings.reasoning_effort = settings.effort;
|
||||
collaboration_mode.settings.reasoning_effort = settings.effort.clone();
|
||||
session.collaboration_mode = Some(Box::new(collaboration_mode));
|
||||
}
|
||||
|
||||
|
||||
@@ -1273,7 +1273,8 @@ fn config_request_overrides_from_config(
|
||||
"model_reasoning_effort",
|
||||
config
|
||||
.model_reasoning_effort
|
||||
.map(|effort| effort.to_string()),
|
||||
.as_ref()
|
||||
.map(std::string::ToString::to_string),
|
||||
);
|
||||
insert(
|
||||
"model_reasoning_summary",
|
||||
@@ -1586,7 +1587,7 @@ async fn thread_session_state_from_thread_start_response(
|
||||
response.cwd.clone(),
|
||||
response.runtime_workspace_roots.clone(),
|
||||
response.instruction_sources.clone(),
|
||||
response.reasoning_effort,
|
||||
response.reasoning_effort.clone(),
|
||||
config,
|
||||
)
|
||||
.await
|
||||
@@ -1627,7 +1628,7 @@ async fn thread_session_state_from_thread_resume_response(
|
||||
response.cwd.clone(),
|
||||
response.runtime_workspace_roots.clone(),
|
||||
response.instruction_sources.clone(),
|
||||
response.reasoning_effort,
|
||||
response.reasoning_effort.clone(),
|
||||
config,
|
||||
)
|
||||
.await
|
||||
@@ -1659,7 +1660,7 @@ async fn thread_session_state_from_thread_fork_response(
|
||||
response.cwd.clone(),
|
||||
response.runtime_workspace_roots.clone(),
|
||||
response.instruction_sources.clone(),
|
||||
response.reasoning_effort,
|
||||
response.reasoning_effort.clone(),
|
||||
config,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -166,7 +166,7 @@ impl ChatWidget {
|
||||
mut collaboration_mode: CollaborationModeMask,
|
||||
) {
|
||||
if collaboration_mode.mode == Some(ModeKind::Plan)
|
||||
&& let Some(effort) = self.config.plan_mode_reasoning_effort
|
||||
&& let Some(effort) = self.config.plan_mode_reasoning_effort.clone()
|
||||
{
|
||||
collaboration_mode.reasoning_effort = Some(Some(effort));
|
||||
}
|
||||
|
||||
@@ -100,11 +100,11 @@ impl ChatWidget {
|
||||
let model = preset.model.clone();
|
||||
let should_prompt_plan_mode_scope = self.should_prompt_plan_mode_reasoning_scope(
|
||||
model.as_str(),
|
||||
Some(preset.default_reasoning_effort),
|
||||
Some(preset.default_reasoning_effort.clone()),
|
||||
);
|
||||
let actions = Self::model_selection_actions(
|
||||
model.clone(),
|
||||
Some(preset.default_reasoning_effort),
|
||||
Some(preset.default_reasoning_effort.clone()),
|
||||
should_prompt_plan_mode_scope,
|
||||
);
|
||||
SelectionItem {
|
||||
@@ -222,16 +222,16 @@ impl ChatWidget {
|
||||
if should_prompt_plan_mode_scope {
|
||||
tx.send(AppEvent::OpenPlanReasoningScopePrompt {
|
||||
model: model_for_action.clone(),
|
||||
effort: effort_for_action,
|
||||
effort: effort_for_action.clone(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
tx.send(AppEvent::UpdateModel(model_for_action.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(effort_for_action));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(effort_for_action.clone()));
|
||||
tx.send(AppEvent::PersistModelSelection {
|
||||
model: model_for_action.clone(),
|
||||
effort: effort_for_action,
|
||||
effort: effort_for_action.clone(),
|
||||
});
|
||||
})]
|
||||
}
|
||||
@@ -261,30 +261,34 @@ impl ChatWidget {
|
||||
model: String,
|
||||
effort: Option<ReasoningEffortConfig>,
|
||||
) {
|
||||
let reasoning_phrase = match effort {
|
||||
let reasoning_phrase = match effort.as_ref() {
|
||||
Some(ReasoningEffortConfig::None) => "no reasoning".to_string(),
|
||||
Some(selected_effort) => {
|
||||
format!(
|
||||
"{} reasoning",
|
||||
Self::reasoning_effort_label(selected_effort).to_lowercase()
|
||||
Self::reasoning_effort_sentence_label(selected_effort)
|
||||
)
|
||||
}
|
||||
None => "the selected reasoning".to_string(),
|
||||
};
|
||||
let plan_only_description = format!("Always use {reasoning_phrase} in Plan mode.");
|
||||
let plan_reasoning_source = if let Some(plan_override) =
|
||||
self.config.plan_mode_reasoning_effort
|
||||
self.config.plan_mode_reasoning_effort.as_ref()
|
||||
{
|
||||
format!(
|
||||
"user-chosen Plan override ({})",
|
||||
Self::reasoning_effort_label(plan_override).to_lowercase()
|
||||
Self::reasoning_effort_sentence_label(plan_override)
|
||||
)
|
||||
} else if let Some(plan_mask) = collaboration_modes::plan_mask(self.model_catalog.as_ref())
|
||||
{
|
||||
match plan_mask.reasoning_effort.flatten() {
|
||||
match plan_mask
|
||||
.reasoning_effort
|
||||
.as_ref()
|
||||
.and_then(|effort| effort.as_ref())
|
||||
{
|
||||
Some(plan_effort) => format!(
|
||||
"built-in Plan default ({})",
|
||||
Self::reasoning_effort_label(plan_effort).to_lowercase()
|
||||
Self::reasoning_effort_sentence_label(plan_effort)
|
||||
),
|
||||
None => "built-in Plan default (no reasoning)".to_string(),
|
||||
}
|
||||
@@ -298,20 +302,21 @@ impl ChatWidget {
|
||||
|
||||
let plan_only_actions: Vec<SelectionAction> = vec![Box::new({
|
||||
let model = model.clone();
|
||||
let effort = effort.clone();
|
||||
move |tx| {
|
||||
tx.send(AppEvent::UpdateModel(model.clone()));
|
||||
tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort));
|
||||
tx.send(AppEvent::PersistPlanModeReasoningEffort(effort));
|
||||
tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort.clone()));
|
||||
tx.send(AppEvent::PersistPlanModeReasoningEffort(effort.clone()));
|
||||
}
|
||||
})];
|
||||
let all_modes_actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::UpdateModel(model.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(effort));
|
||||
tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort));
|
||||
tx.send(AppEvent::PersistPlanModeReasoningEffort(effort));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(effort.clone()));
|
||||
tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort.clone()));
|
||||
tx.send(AppEvent::PersistPlanModeReasoningEffort(effort.clone()));
|
||||
tx.send(AppEvent::PersistModelSelection {
|
||||
model: model.clone(),
|
||||
effort,
|
||||
effort: effort.clone(),
|
||||
});
|
||||
})];
|
||||
|
||||
@@ -344,7 +349,7 @@ impl ChatWidget {
|
||||
|
||||
/// Open a popup to choose the reasoning effort (stage 2) for the given model.
|
||||
pub(crate) fn open_reasoning_popup(&mut self, preset: ModelPreset) {
|
||||
let default_effort: ReasoningEffortConfig = preset.default_reasoning_effort;
|
||||
let default_effort = preset.default_reasoning_effort;
|
||||
let supported = preset.supported_reasoning_efforts;
|
||||
let in_plan_mode =
|
||||
self.collaboration_modes_enabled() && self.active_mode_kind() == ModeKind::Plan;
|
||||
@@ -362,7 +367,7 @@ impl ChatWidget {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let warning_text = warn_effort.map(|effort| {
|
||||
let warning_text = warn_effort.as_ref().map(|effort| {
|
||||
let effort_label = Self::reasoning_effort_label(effort);
|
||||
format!("⚠ {effort_label} reasoning effort can quickly consume Plus plan rate limits.")
|
||||
});
|
||||
@@ -370,30 +375,25 @@ impl ChatWidget {
|
||||
|| preset.model.starts_with("gpt-5.1-codex-max")
|
||||
|| preset.model.starts_with("gpt-5.2");
|
||||
|
||||
struct EffortChoice {
|
||||
stored: Option<ReasoningEffortConfig>,
|
||||
display: ReasoningEffortConfig,
|
||||
}
|
||||
let mut choices: Vec<EffortChoice> = Vec::new();
|
||||
for effort in ReasoningEffortConfig::iter() {
|
||||
if supported.iter().any(|option| option.effort == effort) {
|
||||
choices.push(EffortChoice {
|
||||
stored: Some(effort),
|
||||
display: effort,
|
||||
});
|
||||
}
|
||||
}
|
||||
let mut choices: Vec<ReasoningEffortConfig> = ReasoningEffortConfig::known_values()
|
||||
.filter(|effort| supported.iter().any(|option| option.effort == *effort))
|
||||
.collect();
|
||||
choices.extend(
|
||||
supported
|
||||
.iter()
|
||||
.filter(|option| option.effort.known_rank().is_none())
|
||||
.map(|option| option.effort.clone()),
|
||||
);
|
||||
if choices.is_empty() {
|
||||
choices.push(EffortChoice {
|
||||
stored: Some(default_effort),
|
||||
display: default_effort,
|
||||
});
|
||||
choices.push(default_effort.clone());
|
||||
}
|
||||
|
||||
if choices.len() == 1 {
|
||||
let selected_effort = choices.first().and_then(|c| c.stored);
|
||||
let selected_effort = choices.first().cloned();
|
||||
let selected_model = preset.model;
|
||||
if self.should_prompt_plan_mode_reasoning_scope(&selected_model, selected_effort) {
|
||||
if self
|
||||
.should_prompt_plan_mode_reasoning_scope(&selected_model, selected_effort.clone())
|
||||
{
|
||||
self.app_event_tx
|
||||
.send(AppEvent::OpenPlanReasoningScopePrompt {
|
||||
model: selected_model,
|
||||
@@ -405,12 +405,10 @@ impl ChatWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
let default_choice: Option<ReasoningEffortConfig> = choices
|
||||
.iter()
|
||||
.any(|choice| choice.stored == Some(default_effort))
|
||||
.then_some(Some(default_effort))
|
||||
.flatten()
|
||||
.or_else(|| choices.iter().find_map(|choice| choice.stored))
|
||||
let default_choice = choices
|
||||
.contains(&default_effort)
|
||||
.then(|| default_effort.clone())
|
||||
.or_else(|| choices.first().cloned())
|
||||
.or(Some(default_effort));
|
||||
|
||||
let model_slug = preset.model.to_string();
|
||||
@@ -419,40 +417,33 @@ impl ChatWidget {
|
||||
if in_plan_mode {
|
||||
self.config
|
||||
.plan_mode_reasoning_effort
|
||||
.or(self.effective_reasoning_effort())
|
||||
.clone()
|
||||
.or_else(|| self.effective_reasoning_effort())
|
||||
} else {
|
||||
self.effective_reasoning_effort()
|
||||
}
|
||||
} else {
|
||||
default_choice
|
||||
default_choice.clone()
|
||||
};
|
||||
let selection_choice = highlight_choice.or(default_choice);
|
||||
let selection_choice = highlight_choice.clone().or_else(|| default_choice.clone());
|
||||
let initial_selected_idx = choices
|
||||
.iter()
|
||||
.position(|choice| choice.stored == selection_choice)
|
||||
.or_else(|| {
|
||||
selection_choice
|
||||
.and_then(|effort| choices.iter().position(|choice| choice.display == effort))
|
||||
});
|
||||
.position(|choice| Some(choice) == selection_choice.as_ref());
|
||||
let mut items: Vec<SelectionItem> = Vec::new();
|
||||
for choice in choices.iter() {
|
||||
let effort = choice.display;
|
||||
let mut effort_label = Self::reasoning_effort_label(effort).to_string();
|
||||
if choice.stored == default_choice {
|
||||
let effort = choice.clone();
|
||||
let mut effort_label = Self::reasoning_effort_label(&effort);
|
||||
if Some(choice) == default_choice.as_ref() {
|
||||
effort_label.push_str(" (default)");
|
||||
}
|
||||
|
||||
let description = choice
|
||||
.stored
|
||||
.and_then(|effort| {
|
||||
supported
|
||||
.iter()
|
||||
.find(|option| option.effort == effort)
|
||||
.map(|option| option.description.to_string())
|
||||
})
|
||||
let description = supported
|
||||
.iter()
|
||||
.find(|option| option.effort == effort)
|
||||
.map(|option| option.description.to_string())
|
||||
.filter(|text| !text.is_empty());
|
||||
|
||||
let show_warning = warn_for_model && warn_effort == Some(effort);
|
||||
let show_warning = warn_for_model && warn_effort.as_ref() == Some(&effort);
|
||||
let selected_description = if show_warning {
|
||||
warning_text.as_ref().map(|warning_message| {
|
||||
description.as_ref().map_or_else(
|
||||
@@ -465,21 +456,23 @@ impl ChatWidget {
|
||||
};
|
||||
|
||||
let model_for_action = model_slug.clone();
|
||||
let choice_effort = choice.stored;
|
||||
let should_prompt_plan_mode_scope =
|
||||
self.should_prompt_plan_mode_reasoning_scope(model_slug.as_str(), choice_effort);
|
||||
let choice_effort = Some(effort);
|
||||
let should_prompt_plan_mode_scope = self.should_prompt_plan_mode_reasoning_scope(
|
||||
model_slug.as_str(),
|
||||
choice_effort.clone(),
|
||||
);
|
||||
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||
if should_prompt_plan_mode_scope {
|
||||
tx.send(AppEvent::OpenPlanReasoningScopePrompt {
|
||||
model: model_for_action.clone(),
|
||||
effort: choice_effort,
|
||||
effort: choice_effort.clone(),
|
||||
});
|
||||
} else {
|
||||
tx.send(AppEvent::UpdateModel(model_for_action.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(choice_effort));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(choice_effort.clone()));
|
||||
tx.send(AppEvent::PersistModelSelection {
|
||||
model: model_for_action.clone(),
|
||||
effort: choice_effort,
|
||||
effort: choice_effort.clone(),
|
||||
});
|
||||
}
|
||||
})];
|
||||
@@ -488,7 +481,7 @@ impl ChatWidget {
|
||||
name: effort_label,
|
||||
description,
|
||||
selected_description,
|
||||
is_current: is_current_model && choice.stored == highlight_choice,
|
||||
is_current: is_current_model && Some(choice) == highlight_choice.as_ref(),
|
||||
actions,
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
@@ -509,14 +502,22 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn reasoning_effort_label(effort: ReasoningEffortConfig) -> &'static str {
|
||||
pub(super) fn reasoning_effort_label(effort: &ReasoningEffortConfig) -> String {
|
||||
match effort {
|
||||
ReasoningEffortConfig::None => "None",
|
||||
ReasoningEffortConfig::Minimal => "Minimal",
|
||||
ReasoningEffortConfig::Low => "Low",
|
||||
ReasoningEffortConfig::Medium => "Medium",
|
||||
ReasoningEffortConfig::High => "High",
|
||||
ReasoningEffortConfig::XHigh => "Extra high",
|
||||
ReasoningEffortConfig::None => "None".to_string(),
|
||||
ReasoningEffortConfig::Minimal => "Minimal".to_string(),
|
||||
ReasoningEffortConfig::Low => "Low".to_string(),
|
||||
ReasoningEffortConfig::Medium => "Medium".to_string(),
|
||||
ReasoningEffortConfig::High => "High".to_string(),
|
||||
ReasoningEffortConfig::XHigh => "Extra high".to_string(),
|
||||
ReasoningEffortConfig::Custom(value) => value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn reasoning_effort_sentence_label(effort: &ReasoningEffortConfig) -> String {
|
||||
match effort {
|
||||
ReasoningEffortConfig::Custom(value) => value.clone(),
|
||||
effort => Self::reasoning_effort_label(effort).to_lowercase(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,7 +532,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn apply_model_and_effort(&self, model: String, effort: Option<ReasoningEffortConfig>) {
|
||||
self.apply_model_and_effort_without_persist(model.clone(), effort);
|
||||
self.apply_model_and_effort_without_persist(model.clone(), effort.clone());
|
||||
self.app_event_tx
|
||||
.send(AppEvent::PersistModelSelection { model, effort });
|
||||
}
|
||||
|
||||
@@ -343,14 +343,16 @@ impl ChatWidget {
|
||||
/*active_permission_profile*/ None,
|
||||
/*windows_sandbox_level*/ None,
|
||||
Some(switch_model_for_events.clone()),
|
||||
Some(Some(default_effort)),
|
||||
Some(Some(default_effort.clone())),
|
||||
/*summary*/ None,
|
||||
/*service_tier*/ None,
|
||||
/*collaboration_mode*/ None,
|
||||
/*personality*/ None,
|
||||
)));
|
||||
tx.send(AppEvent::UpdateModel(switch_model_for_events.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(Some(default_effort)));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(Some(
|
||||
default_effort.clone(),
|
||||
)));
|
||||
})];
|
||||
|
||||
let keep_actions: Vec<SelectionAction> = Vec::new();
|
||||
|
||||
@@ -8,15 +8,14 @@
|
||||
//! The shortcut state machine is deliberately narrow: it only handles key
|
||||
//! presses when no modal or popup owns input, it anchors unset reasoning to the
|
||||
//! current model preset's default, and it walks only efforts advertised by the
|
||||
//! active model. Unsupported current efforts are not normalized eagerly; the
|
||||
//! next shortcut moves to the nearest supported effort in the requested
|
||||
//! direction.
|
||||
//! active model. Unsupported known efforts move to the nearest advertised known
|
||||
//! effort in the requested direction. Unknown efforts anchor to the model
|
||||
//! default before stepping through the advertised order.
|
||||
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use crossterm::event::KeyEvent;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use super::ChatWidget;
|
||||
use crate::app_event::AppEvent;
|
||||
@@ -30,8 +29,8 @@ pub(super) enum ReasoningShortcutDirection {
|
||||
}
|
||||
|
||||
impl ReasoningShortcutDirection {
|
||||
fn bound_message(self, effort: ReasoningEffortConfig) -> String {
|
||||
let label = ChatWidget::reasoning_effort_label(effort).to_lowercase();
|
||||
fn bound_message(self, effort: &ReasoningEffortConfig) -> String {
|
||||
let label = ChatWidget::reasoning_effort_sentence_label(effort);
|
||||
match self {
|
||||
Self::Lower => format!("Reasoning is already at the lowest level ({label})."),
|
||||
Self::Raise => format!("Reasoning is already at the highest level ({label})."),
|
||||
@@ -90,12 +89,23 @@ impl ChatWidget {
|
||||
};
|
||||
|
||||
let choices = reasoning_choices(&preset);
|
||||
let current_effort = self
|
||||
let configured_effort = self
|
||||
.effective_reasoning_effort()
|
||||
.unwrap_or(preset.default_reasoning_effort);
|
||||
let Some(next_effort) = next_reasoning_effort(&choices, Some(current_effort), direction)
|
||||
.unwrap_or_else(|| preset.default_reasoning_effort.clone());
|
||||
let current_effort = if choices.contains(&configured_effort) {
|
||||
configured_effort
|
||||
} else if choices.contains(&preset.default_reasoning_effort) {
|
||||
preset.default_reasoning_effort
|
||||
} else {
|
||||
choices
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or(preset.default_reasoning_effort)
|
||||
};
|
||||
let Some(next_effort) =
|
||||
next_reasoning_effort(&choices, Some(current_effort.clone()), direction)
|
||||
else {
|
||||
self.add_info_message(direction.bound_message(current_effort), /*hint*/ None);
|
||||
self.add_info_message(direction.bound_message(¤t_effort), /*hint*/ None);
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -120,18 +130,23 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn reasoning_choices(preset: &ModelPreset) -> Vec<ReasoningEffortConfig> {
|
||||
let mut choices = Vec::new();
|
||||
for effort in ReasoningEffortConfig::iter() {
|
||||
if preset
|
||||
let mut choices: Vec<ReasoningEffortConfig> = ReasoningEffortConfig::known_values()
|
||||
.filter(|effort| {
|
||||
preset
|
||||
.supported_reasoning_efforts
|
||||
.iter()
|
||||
.any(|option| option.effort == *effort)
|
||||
})
|
||||
.collect();
|
||||
choices.extend(
|
||||
preset
|
||||
.supported_reasoning_efforts
|
||||
.iter()
|
||||
.any(|option| option.effort == effort)
|
||||
{
|
||||
choices.push(effort);
|
||||
}
|
||||
}
|
||||
.filter(|option| option.effort.known_rank().is_none())
|
||||
.map(|option| option.effort.clone()),
|
||||
);
|
||||
if choices.is_empty() {
|
||||
choices.push(preset.default_reasoning_effort);
|
||||
choices.push(preset.default_reasoning_effort.clone());
|
||||
}
|
||||
choices
|
||||
}
|
||||
@@ -142,32 +157,51 @@ fn next_reasoning_effort(
|
||||
direction: ReasoningShortcutDirection,
|
||||
) -> Option<ReasoningEffortConfig> {
|
||||
let current_effort = current_effort?;
|
||||
if choices.is_empty() {
|
||||
return None;
|
||||
if let Some(current_index) = choices.iter().position(|choice| choice == ¤t_effort) {
|
||||
return match direction {
|
||||
ReasoningShortcutDirection::Lower => current_index
|
||||
.checked_sub(1)
|
||||
.and_then(|index| choices.get(index))
|
||||
.cloned(),
|
||||
ReasoningShortcutDirection::Raise => choices.get(current_index + 1).cloned(),
|
||||
};
|
||||
}
|
||||
|
||||
let current_rank = effort_rank(current_effort);
|
||||
match direction {
|
||||
let current_rank = current_effort.known_rank()?;
|
||||
let ranked_choice = match direction {
|
||||
ReasoningShortcutDirection::Lower => choices
|
||||
.iter()
|
||||
.rev()
|
||||
.copied()
|
||||
.find(|choice| effort_rank(*choice) < current_rank),
|
||||
.filter_map(|choice| choice.known_rank().map(|rank| (rank, choice)))
|
||||
.filter(|(rank, _)| *rank < current_rank)
|
||||
.max_by_key(|(rank, _)| *rank)
|
||||
.map(|(_, choice)| choice.clone()),
|
||||
ReasoningShortcutDirection::Raise => choices
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|choice| effort_rank(*choice) > current_rank),
|
||||
.filter_map(|choice| choice.known_rank().map(|rank| (rank, choice)))
|
||||
.filter(|(rank, _)| *rank > current_rank)
|
||||
.min_by_key(|(rank, _)| *rank)
|
||||
.map(|(_, choice)| choice.clone()),
|
||||
};
|
||||
if let Some(ranked_choice) = ranked_choice {
|
||||
return Some(ranked_choice);
|
||||
}
|
||||
}
|
||||
|
||||
fn effort_rank(effort: ReasoningEffortConfig) -> i32 {
|
||||
match effort {
|
||||
ReasoningEffortConfig::None => 0,
|
||||
ReasoningEffortConfig::Minimal => 1,
|
||||
ReasoningEffortConfig::Low => 2,
|
||||
ReasoningEffortConfig::Medium => 3,
|
||||
ReasoningEffortConfig::High => 4,
|
||||
ReasoningEffortConfig::XHigh => 5,
|
||||
let nearest_known_index = choices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, choice)| {
|
||||
choice
|
||||
.known_rank()
|
||||
.map(|rank| (rank.abs_diff(current_rank), index))
|
||||
})
|
||||
.min_by_key(|(distance, _)| *distance)
|
||||
.map(|(_, index)| index)?;
|
||||
match direction {
|
||||
ReasoningShortcutDirection::Lower => nearest_known_index
|
||||
.checked_sub(1)
|
||||
.and_then(|index| choices.get(index))
|
||||
.cloned(),
|
||||
ReasoningShortcutDirection::Raise => choices.get(nearest_known_index + 1).cloned(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,6 +269,21 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_reasoning_effort_reaches_custom_level_from_nearest_known_anchor() {
|
||||
let custom_effort = ReasoningEffortConfig::Custom("max".to_string());
|
||||
let choices = vec![ReasoningEffortConfig::Medium, custom_effort.clone()];
|
||||
|
||||
assert_eq!(
|
||||
next_reasoning_effort(
|
||||
&choices,
|
||||
Some(ReasoningEffortConfig::High),
|
||||
ReasoningShortcutDirection::Raise,
|
||||
),
|
||||
Some(custom_effort)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_reasoning_effort_clamps_at_bounds() {
|
||||
let choices = vec![
|
||||
|
||||
@@ -79,7 +79,7 @@ impl ChatWidget {
|
||||
let default_model = session.model.clone();
|
||||
self.current_collaboration_mode = self.current_collaboration_mode.with_updates(
|
||||
Some(default_model.clone()),
|
||||
Some(session.reasoning_effort),
|
||||
Some(session.reasoning_effort.clone()),
|
||||
/*developer_instructions*/ None,
|
||||
);
|
||||
match session.collaboration_mode.as_deref() {
|
||||
@@ -93,7 +93,7 @@ impl ChatWidget {
|
||||
Some(&default_model),
|
||||
);
|
||||
if let Some(mask) = self.active_collaboration_mask.as_mut() {
|
||||
mask.reasoning_effort = Some(session.reasoning_effort);
|
||||
mask.reasoning_effort = Some(session.reasoning_effort.clone());
|
||||
}
|
||||
self.update_collaboration_mode_indicator();
|
||||
self.refresh_plan_mode_nudge();
|
||||
|
||||
@@ -159,7 +159,7 @@ impl ChatWidget {
|
||||
/// so the footer reflects it without waiting for the next mode switch.
|
||||
/// Passing `None` resets to the Plan-mode preset default.
|
||||
pub(crate) fn set_plan_mode_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) {
|
||||
self.config.plan_mode_reasoning_effort = effort;
|
||||
self.config.plan_mode_reasoning_effort = effort.clone();
|
||||
if self.collaboration_modes_enabled()
|
||||
&& let Some(mask) = self.active_collaboration_mask.as_mut()
|
||||
&& mask.mode == Some(ModeKind::Plan)
|
||||
@@ -182,7 +182,7 @@ impl ChatWidget {
|
||||
pub(crate) fn set_reasoning_effort(&mut self, effort: Option<ReasoningEffortConfig>) {
|
||||
self.current_collaboration_mode = self.current_collaboration_mode.with_updates(
|
||||
/*model*/ None,
|
||||
Some(effort),
|
||||
Some(effort.clone()),
|
||||
/*developer_instructions*/ None,
|
||||
);
|
||||
if self.collaboration_modes_enabled()
|
||||
@@ -471,7 +471,7 @@ impl ChatWidget {
|
||||
let current_effort = self.current_collaboration_mode.reasoning_effort();
|
||||
self.active_collaboration_mask
|
||||
.as_ref()
|
||||
.and_then(|mask| mask.reasoning_effort)
|
||||
.and_then(|mask| mask.reasoning_effort.clone())
|
||||
.unwrap_or(current_effort)
|
||||
}
|
||||
|
||||
@@ -590,7 +590,7 @@ impl ChatWidget {
|
||||
name: mode_kind.display_name().to_string(),
|
||||
mode: Some(mode_kind),
|
||||
model: Some(settings.model.clone()),
|
||||
reasoning_effort: Some(settings.reasoning_effort),
|
||||
reasoning_effort: Some(settings.reasoning_effort.clone()),
|
||||
developer_instructions: Some(settings.developer_instructions),
|
||||
});
|
||||
self.update_collaboration_mode_indicator();
|
||||
@@ -712,7 +712,7 @@ impl ChatWidget {
|
||||
let previous_model = self.current_model().to_string();
|
||||
let previous_effort = self.effective_reasoning_effort();
|
||||
if mask.mode == Some(ModeKind::Plan)
|
||||
&& let Some(effort) = self.config.plan_mode_reasoning_effort
|
||||
&& let Some(effort) = self.config.plan_mode_reasoning_effort.clone()
|
||||
{
|
||||
mask.reasoning_effort = Some(Some(effort));
|
||||
}
|
||||
@@ -732,13 +732,9 @@ impl ChatWidget {
|
||||
{
|
||||
let mut message = format!("Model changed to {next_model}");
|
||||
if !next_model.starts_with("codex-auto-") {
|
||||
let reasoning_label = match next_effort {
|
||||
Some(ReasoningEffortConfig::Minimal) => "minimal",
|
||||
Some(ReasoningEffortConfig::Low) => "low",
|
||||
Some(ReasoningEffortConfig::Medium) => "medium",
|
||||
Some(ReasoningEffortConfig::High) => "high",
|
||||
Some(ReasoningEffortConfig::XHigh) => "xhigh",
|
||||
let reasoning_label = match next_effort.as_ref() {
|
||||
None | Some(ReasoningEffortConfig::None) => "default",
|
||||
Some(effort) => effort.as_str(),
|
||||
};
|
||||
message.push(' ');
|
||||
message.push_str(reasoning_label);
|
||||
|
||||
+1
@@ -8,5 +8,6 @@ expression: popup
|
||||
2. Medium (default) Balances speed and reasoning depth for everyday tasks
|
||||
› 3. High (current) Greater reasoning depth for complex problems
|
||||
4. Extra high Extra high reasoning depth for complex problems
|
||||
5. max Maximum available reasoning
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
|
||||
@@ -212,7 +212,7 @@ impl ChatWidget {
|
||||
});
|
||||
let reasoning_effort_override = Some(
|
||||
self.effective_reasoning_effort()
|
||||
.or(self.config.model_reasoning_effort)
|
||||
.or_else(|| self.config.model_reasoning_effort.clone())
|
||||
.or(model_default_reasoning_effort),
|
||||
);
|
||||
let rate_limit_snapshots: Vec<RateLimitSnapshotDisplay> = self
|
||||
@@ -382,15 +382,11 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
pub(super) fn status_line_reasoning_effort_label(
|
||||
effort: Option<ReasoningEffortConfig>,
|
||||
) -> &'static str {
|
||||
effort: Option<&ReasoningEffortConfig>,
|
||||
) -> String {
|
||||
match effort {
|
||||
Some(ReasoningEffortConfig::Minimal) => "minimal",
|
||||
Some(ReasoningEffortConfig::Low) => "low",
|
||||
Some(ReasoningEffortConfig::Medium) => "medium",
|
||||
Some(ReasoningEffortConfig::High) => "high",
|
||||
Some(ReasoningEffortConfig::XHigh) => "xhigh",
|
||||
None | Some(ReasoningEffortConfig::None) => "default",
|
||||
None | Some(ReasoningEffortConfig::None) => "default".to_string(),
|
||||
Some(effort) => effort.as_str().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,7 +562,7 @@ impl ChatWidget {
|
||||
match item {
|
||||
StatusLineItem::ModelName => Some(self.model_display_name().to_string()),
|
||||
StatusLineItem::ModelWithReasoning => Some(self.model_with_reasoning_display_name()),
|
||||
StatusLineItem::Reasoning => Some(self.reasoning_display_name().to_string()),
|
||||
StatusLineItem::Reasoning => Some(self.reasoning_display_name()),
|
||||
StatusLineItem::CurrentDir => {
|
||||
Some(format_directory_display(
|
||||
self.status_line_cwd(),
|
||||
@@ -762,15 +762,16 @@ impl ChatWidget {
|
||||
/*max_chars*/ 32,
|
||||
)),
|
||||
TerminalTitleItem::Reasoning => Some(Self::truncate_terminal_title_part(
|
||||
self.reasoning_display_name().to_string(),
|
||||
self.reasoning_display_name(),
|
||||
/*max_chars*/ 32,
|
||||
)),
|
||||
TerminalTitleItem::TaskProgress => self.terminal_title_task_progress(),
|
||||
}
|
||||
}
|
||||
|
||||
fn reasoning_display_name(&self) -> &'static str {
|
||||
Self::status_line_reasoning_effort_label(self.effective_reasoning_effort())
|
||||
fn reasoning_display_name(&self) -> String {
|
||||
let effort = self.effective_reasoning_effort();
|
||||
Self::status_line_reasoning_effort_label(effort.as_ref())
|
||||
}
|
||||
|
||||
fn model_with_reasoning_display_name(&self) -> String {
|
||||
|
||||
@@ -2405,13 +2405,54 @@ async fn model_reasoning_selection_popup_snapshot() {
|
||||
set_chatgpt_auth(&mut chat);
|
||||
chat.set_reasoning_effort(Some(ReasoningEffortConfig::High));
|
||||
|
||||
let preset = get_available_model(&chat, "gpt-5.4");
|
||||
let mut preset = get_available_model(&chat, "gpt-5.4");
|
||||
preset
|
||||
.supported_reasoning_efforts
|
||||
.push(ReasoningEffortPreset {
|
||||
effort: ReasoningEffortConfig::Custom("max".to_string()),
|
||||
description: "Maximum available reasoning".to_string(),
|
||||
});
|
||||
chat.open_reasoning_popup(preset);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert_chatwidget_snapshot!("model_reasoning_selection_popup", popup);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn model_reasoning_selection_popup_applies_custom_effort() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.4")).await;
|
||||
let custom_effort = ReasoningEffortConfig::Custom("max".to_string());
|
||||
chat.set_reasoning_effort(Some(ReasoningEffortConfig::XHigh));
|
||||
|
||||
let mut preset = get_available_model(&chat, "gpt-5.4");
|
||||
preset
|
||||
.supported_reasoning_efforts
|
||||
.push(ReasoningEffortPreset {
|
||||
effort: custom_effort.clone(),
|
||||
description: "Maximum available reasoning".to_string(),
|
||||
});
|
||||
chat.open_reasoning_popup(preset);
|
||||
while rx.try_recv().is_ok() {}
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
let selected_effort_events = std::iter::from_fn(|| rx.try_recv().ok())
|
||||
.filter_map(|event| match event {
|
||||
AppEvent::UpdateReasoningEffort(effort) => Some((None, effort)),
|
||||
AppEvent::PersistModelSelection { model, effort } => Some((Some(model), effort)),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
selected_effort_events,
|
||||
vec![
|
||||
(None, Some(custom_effort.clone())),
|
||||
(Some("gpt-5.4".to_string()), Some(custom_effort)),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn model_reasoning_selection_popup_extra_high_warning_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2")).await;
|
||||
|
||||
@@ -149,7 +149,7 @@ pub(crate) fn new_session_info(
|
||||
// Header box rendered as history (so it appears at the very top)
|
||||
let header = SessionHeaderHistoryCell::new(
|
||||
session.model.clone(),
|
||||
session.reasoning_effort,
|
||||
session.reasoning_effort.clone(),
|
||||
show_fast_status,
|
||||
config.cwd.to_path_buf(),
|
||||
CODEX_CLI_VERSION,
|
||||
@@ -317,15 +317,10 @@ impl SessionHeaderHistoryCell {
|
||||
formatted
|
||||
}
|
||||
|
||||
fn reasoning_label(&self) -> Option<&'static str> {
|
||||
self.reasoning_effort.map(|effort| match effort {
|
||||
ReasoningEffortConfig::Minimal => "minimal",
|
||||
ReasoningEffortConfig::Low => "low",
|
||||
ReasoningEffortConfig::Medium => "medium",
|
||||
ReasoningEffortConfig::High => "high",
|
||||
ReasoningEffortConfig::XHigh => "xhigh",
|
||||
ReasoningEffortConfig::None => "none",
|
||||
})
|
||||
fn reasoning_label(&self) -> Option<&str> {
|
||||
self.reasoning_effort
|
||||
.as_ref()
|
||||
.map(ReasoningEffortConfig::as_str)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +363,7 @@ impl HistoryCell for SessionHeaderHistoryCell {
|
||||
];
|
||||
if let Some(reasoning) = reasoning_label {
|
||||
spans.push(Span::from(" "));
|
||||
spans.push(Span::from(reasoning));
|
||||
spans.push(Span::from(reasoning.to_owned()));
|
||||
}
|
||||
if self.show_fast_status {
|
||||
spans.push(" ".into());
|
||||
|
||||
@@ -182,7 +182,7 @@ pub(crate) fn spawn_request_summary(item: &ThreadItem) -> Option<SpawnRequestSum
|
||||
..
|
||||
} => Some(SpawnRequestSummary {
|
||||
model: model.clone(),
|
||||
reasoning_effort: *reasoning_effort,
|
||||
reasoning_effort: reasoning_effort.clone(),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ impl StatusHistoryCell {
|
||||
];
|
||||
if config.model_provider.wire_api == WireApi::Responses {
|
||||
let effort_value = reasoning_effort_override
|
||||
.unwrap_or(config.model_reasoning_effort)
|
||||
.unwrap_or_else(|| config.model_reasoning_effort.clone())
|
||||
.map(|effort| effort.to_string())
|
||||
.unwrap_or_else(|| "none".to_string());
|
||||
config_entries.push(("reasoning effort", effort_value));
|
||||
|
||||
@@ -25,7 +25,8 @@ pub fn create_config_summary_entries(config: &Config, model: &str) -> Vec<(&'sta
|
||||
if config.model_provider.wire_api == WireApi::Responses {
|
||||
let reasoning_effort = config
|
||||
.model_reasoning_effort
|
||||
.map(|effort| effort.to_string());
|
||||
.as_ref()
|
||||
.map(std::string::ToString::to_string);
|
||||
entries.push((
|
||||
"reasoning effort",
|
||||
reasoning_effort.unwrap_or_else(|| "none".to_string()),
|
||||
|
||||
Reference in New Issue
Block a user