diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 97a90c1520f..5f50faf9d0e 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -635,6 +635,8 @@ dependencies = [ "seccompiler", "serde", "serde_json", + "strum 0.27.1", + "strum_macros 0.27.1", "tempfile", "thiserror 2.0.12", "time", diff --git a/codex-rs/config.md b/codex-rs/config.md index 416eeb41440..ffa735ff212 100644 --- a/codex-rs/config.md +++ b/codex-rs/config.md @@ -142,6 +142,34 @@ Users can specify config values at multiple levels. Order of precedence is as fo 3. as an entry in `config.toml`, e.g., `model = "o3"` 4. the default value that comes with Codex CLI (i.e., Codex CLI defaults to `codex-mini-latest`) +## model_reasoning_effort + +If the model name starts with `"o"` (as in `"o3"` or `"o4-mini"`) or `"codex"`, reasoning is enabled by default when using the Responses API. As explained in the [OpenAI Platform documentation](https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning), this can be set to: + +- `"low"` +- `"medium"` (default) +- `"high"` + +To disable reasoning, set `model_reasoning_effort` to `"none"` in your config: + +```toml +model_reasoning_effort = "none" # disable reasoning +``` + +## model_reasoning_summary + +If the model name starts with `"o"` (as in `"o3"` or `"o4-mini"`) or `"codex"`, reasoning is enabled by default when using the Responses API. As explained in the [OpenAI Platform documentation](https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries), this can be set to: + +- `"auto"` (default) +- `"concise"` +- `"detailed"` + +To disable reasoning summaries, set `model_reasoning_summary` to `"none"` in your config: + +```toml +model_reasoning_summary = "none" # disable reasoning summaries +``` + ## sandbox_permissions List of permissions to grant to the sandbox that Codex uses to execute untrusted commands: diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 46872949818..4739ef31eda 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -31,6 +31,8 @@ rand = "0.9" reqwest = { version = "0.12", features = ["json", "stream"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +strum = "0.27.1" +strum_macros = "0.27.1" thiserror = "2.0.12" time = { version = "0.3", features = ["formatting", "local-offset", "macros"] } tokio = { version = "1", features = [ diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 6eb20149a5f..74992fd1787 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -18,12 +18,13 @@ use tracing::warn; use crate::chat_completions::AggregateStreamExt; use crate::chat_completions::stream_chat_completions; -use crate::client_common::Payload; use crate::client_common::Prompt; -use crate::client_common::Reasoning; use crate::client_common::ResponseEvent; use crate::client_common::ResponseStream; -use crate::client_common::Summary; +use crate::client_common::ResponsesApiRequest; +use crate::client_common::create_reasoning_param_for_request; +use crate::config_types::ReasoningEffort as ReasoningEffortConfig; +use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::error::CodexErr; use crate::error::EnvVarError; use crate::error::Result; @@ -41,14 +42,23 @@ pub struct ModelClient { model: String, client: reqwest::Client, provider: ModelProviderInfo, + effort: ReasoningEffortConfig, + summary: ReasoningSummaryConfig, } impl ModelClient { - pub fn new(model: impl ToString, provider: ModelProviderInfo) -> Self { + pub fn new( + model: impl ToString, + provider: ModelProviderInfo, + effort: ReasoningEffortConfig, + summary: ReasoningSummaryConfig, + ) -> Self { Self { model: model.to_string(), client: reqwest::Client::new(), provider, + effort, + summary, } } @@ -98,17 +108,15 @@ impl ModelClient { let full_instructions = prompt.get_full_instructions(); let tools_json = create_tools_json_for_responses_api(prompt, &self.model)?; - let payload = Payload { + let reasoning = create_reasoning_param_for_request(&self.model, self.effort, self.summary); + let payload = ResponsesApiRequest { model: &self.model, instructions: &full_instructions, input: &prompt.input, tools: &tools_json, tool_choice: "auto", parallel_tool_calls: false, - reasoning: Some(Reasoning { - effort: "high", - summary: Some(Summary::Auto), - }), + reasoning, previous_response_id: prompt.prev_id.clone(), store: prompt.store, stream: true, diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 8eb8074b1ef..c4c3874cb28 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -1,3 +1,5 @@ +use crate::config_types::ReasoningEffort as ReasoningEffortConfig; +use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::error::Result; use crate::models::ResponseItem; use futures::Stream; @@ -52,25 +54,59 @@ pub enum ResponseEvent { #[derive(Debug, Serialize)] pub(crate) struct Reasoning { - pub(crate) effort: &'static str, + pub(crate) effort: OpenAiReasoningEffort, #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) summary: Option, + pub(crate) summary: Option, +} + +/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning +#[derive(Debug, Serialize, Default, Clone, Copy)] +#[serde(rename_all = "lowercase")] +pub(crate) enum OpenAiReasoningEffort { + Low, + #[default] + Medium, + High, +} + +impl From for Option { + fn from(effort: ReasoningEffortConfig) -> Self { + match effort { + ReasoningEffortConfig::Low => Some(OpenAiReasoningEffort::Low), + ReasoningEffortConfig::Medium => Some(OpenAiReasoningEffort::Medium), + ReasoningEffortConfig::High => Some(OpenAiReasoningEffort::High), + ReasoningEffortConfig::None => None, + } + } } /// A summary of the reasoning performed by the model. This can be useful for /// debugging and understanding the model's reasoning process. -#[derive(Debug, Serialize)] +/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries +#[derive(Debug, Serialize, Default, Clone, Copy)] #[serde(rename_all = "lowercase")] -pub(crate) enum Summary { +pub(crate) enum OpenAiReasoningSummary { + #[default] Auto, - #[allow(dead_code)] // Will go away once this is configurable. Concise, - #[allow(dead_code)] // Will go away once this is configurable. Detailed, } +impl From for Option { + fn from(summary: ReasoningSummaryConfig) -> Self { + match summary { + ReasoningSummaryConfig::Auto => Some(OpenAiReasoningSummary::Auto), + ReasoningSummaryConfig::Concise => Some(OpenAiReasoningSummary::Concise), + ReasoningSummaryConfig::Detailed => Some(OpenAiReasoningSummary::Detailed), + ReasoningSummaryConfig::None => None, + } + } +} + +/// Request object that is serialized as JSON and POST'ed when using the +/// Responses API. #[derive(Debug, Serialize)] -pub(crate) struct Payload<'a> { +pub(crate) struct ResponsesApiRequest<'a> { pub(crate) model: &'a str, pub(crate) instructions: &'a str, // TODO(mbolin): ResponseItem::Other should not be serialized. Currently, @@ -88,6 +124,40 @@ pub(crate) struct Payload<'a> { pub(crate) stream: bool, } +pub(crate) fn create_reasoning_param_for_request( + model: &str, + effort: ReasoningEffortConfig, + summary: ReasoningSummaryConfig, +) -> Option { + let effort: Option = effort.into(); + let effort = effort?; + + if model_supports_reasoning_summaries(model) { + Some(Reasoning { + effort, + summary: summary.into(), + }) + } else { + None + } +} + +pub fn model_supports_reasoning_summaries(model: &str) -> bool { + // Currently, we hardcode this rule to decide whether enable reasoning. + // We expect reasoning to apply only to OpenAI models, but we do not want + // users to have to mess with their config to disable reasoning for models + // that do not support it, such as `gpt-4.1`. + // + // Though if a user is using Codex with non-OpenAI models that, say, happen + // to start with "o", then they can set `model_reasoning_effort = "none` in + // config.toml to disable reasoning. + // + // Ultimately, this should also be configurable in config.toml, but we + // need to have defaults that "just work." Perhaps we could have a + // "reasoning models pattern" as part of ModelProviderInfo? + model.starts_with("o") || model.starts_with("codex") +} + pub(crate) struct ResponseStream { pub(crate) rx_event: mpsc::Receiver>, } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 01ff459f659..0a03fe60aa8 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -108,6 +108,8 @@ impl Codex { let configure_session = Op::ConfigureSession { provider: config.model_provider.clone(), model: config.model.clone(), + model_reasoning_effort: config.model_reasoning_effort, + model_reasoning_summary: config.model_reasoning_summary, instructions, approval_policy: config.approval_policy, sandbox_policy: config.sandbox_policy.clone(), @@ -554,6 +556,8 @@ async fn submission_loop( Op::ConfigureSession { provider, model, + model_reasoning_effort, + model_reasoning_summary, instructions, approval_policy, sandbox_policy, @@ -575,7 +579,12 @@ async fn submission_loop( return; } - let client = ModelClient::new(model.clone(), provider.clone()); + let client = ModelClient::new( + model.clone(), + provider.clone(), + model_reasoning_effort, + model_reasoning_summary, + ); // abort any current running session and clone its state let retain_zdr_transcript = diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index d948ddb916a..74798129ba1 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -1,6 +1,8 @@ use crate::config_profile::ConfigProfile; use crate::config_types::History; use crate::config_types::McpServerConfig; +use crate::config_types::ReasoningEffort; +use crate::config_types::ReasoningSummary; use crate::config_types::ShellEnvironmentPolicy; use crate::config_types::ShellEnvironmentPolicyToml; use crate::config_types::Tui; @@ -112,6 +114,14 @@ pub struct Config { /// /// When this program is invoked, arg0 will be set to `codex-linux-sandbox`. pub codex_linux_sandbox_exe: Option, + + /// If not "none", the value to use for `reasoning.effort` when making a + /// request using the Responses API. + pub model_reasoning_effort: ReasoningEffort, + + /// If not "none", the value to use for `reasoning.summary` when making a + /// request using the Responses API. + pub model_reasoning_summary: ReasoningSummary, } impl Config { @@ -281,6 +291,9 @@ pub struct ConfigToml { /// When set to `true`, `AgentReasoning` events will be hidden from the /// UI/output. Defaults to `false`. pub hide_agent_reasoning: Option, + + pub model_reasoning_effort: Option, + pub model_reasoning_summary: Option, } fn deserialize_sandbox_permissions<'de, D>( @@ -444,6 +457,8 @@ impl Config { codex_linux_sandbox_exe, hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false), + model_reasoning_effort: cfg.model_reasoning_effort.unwrap_or_default(), + model_reasoning_summary: cfg.model_reasoning_summary.unwrap_or_default(), }; Ok(config) } @@ -786,6 +801,8 @@ disable_response_storage = true tui: Tui::default(), codex_linux_sandbox_exe: None, hide_agent_reasoning: false, + model_reasoning_effort: ReasoningEffort::default(), + model_reasoning_summary: ReasoningSummary::default(), }, o3_profile_config ); @@ -826,6 +843,8 @@ disable_response_storage = true tui: Tui::default(), codex_linux_sandbox_exe: None, hide_agent_reasoning: false, + model_reasoning_effort: ReasoningEffort::default(), + model_reasoning_summary: ReasoningSummary::default(), }; assert_eq!(expected_gpt3_profile_config, gpt3_profile_config); @@ -881,6 +900,8 @@ disable_response_storage = true tui: Tui::default(), codex_linux_sandbox_exe: None, hide_agent_reasoning: false, + model_reasoning_effort: ReasoningEffort::default(), + model_reasoning_summary: ReasoningSummary::default(), }; assert_eq!(expected_zdr_profile_config, zdr_profile_config); diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index d89b09f267e..a7152d14620 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -4,9 +4,11 @@ // definitions that do not contain business logic. use std::collections::HashMap; +use strum_macros::Display; use wildmatch::WildMatchPattern; use serde::Deserialize; +use serde::Serialize; #[derive(Deserialize, Debug, Clone, PartialEq)] pub struct McpServerConfig { @@ -175,3 +177,31 @@ impl From for ShellEnvironmentPolicy { } } } + +/// 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)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum ReasoningEffort { + Low, + #[default] + Medium, + High, + /// Option to disable reasoning. + None, +} + +/// A summary of the reasoning performed by the model. This can be useful for +/// debugging and understanding the model's reasoning process. +/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries +#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum ReasoningSummary { + #[default] + Auto, + Concise, + Detailed, + /// Option to disable reasoning summaries. + None, +} diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 77941a9a51a..1dcf67bd1ca 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -34,3 +34,5 @@ mod rollout; mod safety; mod user_notification; pub mod util; + +pub use client_common::model_supports_reasoning_summaries; diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index fc18f1d8212..737acc77324 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -12,6 +12,8 @@ use serde::Deserialize; use serde::Serialize; use uuid::Uuid; +use crate::config_types::ReasoningEffort as ReasoningEffortConfig; +use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::message_history::HistoryEntry; use crate::model_provider_info::ModelProviderInfo; @@ -37,6 +39,10 @@ pub enum Op { /// If not specified, server will use its default model. model: String, + + model_reasoning_effort: ReasoningEffortConfig, + model_reasoning_summary: ReasoningSummaryConfig, + /// Model instructions instructions: Option, /// When to escalate for approval for execution diff --git a/codex-rs/exec/src/event_processor.rs b/codex-rs/exec/src/event_processor.rs index 5462736b5fc..4cbbd25f0b4 100644 --- a/codex-rs/exec/src/event_processor.rs +++ b/codex-rs/exec/src/event_processor.rs @@ -1,5 +1,7 @@ use codex_common::elapsed::format_elapsed; +use codex_core::WireApi; use codex_core::config::Config; +use codex_core::model_supports_reasoning_summaries; use codex_core::protocol::AgentMessageEvent; use codex_core::protocol::BackgroundEventEvent; use codex_core::protocol::ErrorEvent; @@ -127,16 +129,28 @@ impl EventProcessor { VERSION ); - let entries = vec![ + let mut entries = vec![ ("workdir", config.cwd.display().to_string()), ("model", config.model.clone()), ("provider", config.model_provider_id.clone()), ("approval", format!("{:?}", config.approval_policy)), ("sandbox", format!("{:?}", config.sandbox_policy)), ]; + if config.model_provider.wire_api == WireApi::Responses + && model_supports_reasoning_summaries(&config.model) + { + entries.push(( + "reasoning effort", + config.model_reasoning_effort.to_string(), + )); + entries.push(( + "reasoning summaries", + config.model_reasoning_summary.to_string(), + )); + } for (key, value) in entries { - println!("{} {}", format!("{key}: ").style(self.bold), value); + println!("{} {}", format!("{key}:").style(self.bold), value); } println!("--------"); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index b41c8ac62b0..a1fc672c6be 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -5,7 +5,9 @@ use crate::text_block::TextBlock; use base64::Engine; use codex_ansi_escape::ansi_escape_line; use codex_common::elapsed::format_duration; +use codex_core::WireApi; use codex_core::config::Config; +use codex_core::model_supports_reasoning_summaries; use codex_core::protocol::FileChange; use codex_core::protocol::SessionConfiguredEvent; use image::DynamicImage; @@ -147,13 +149,25 @@ impl HistoryCell { ]), ]; - let entries = vec![ + let mut entries = vec![ ("workdir", config.cwd.display().to_string()), ("model", config.model.clone()), ("provider", config.model_provider_id.clone()), ("approval", format!("{:?}", config.approval_policy)), ("sandbox", format!("{:?}", config.sandbox_policy)), ]; + if config.model_provider.wire_api == WireApi::Responses + && model_supports_reasoning_summaries(&config.model) + { + entries.push(( + "reasoning effort", + config.model_reasoning_effort.to_string(), + )); + entries.push(( + "reasoning summaries", + config.model_reasoning_summary.to_string(), + )); + } for (key, value) in entries { lines.push(Line::from(vec![format!("{key}: ").bold(), value.into()])); }