diff --git a/codex-rs/config.md b/codex-rs/config.md index 416eeb4144..ffa735ff21 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/src/client.rs b/codex-rs/core/src/client.rs index 6eb20149a5..74992fd178 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 8eb8074b1e..a35501603a 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,36 @@ 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?; + + // 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? + if model.starts_with("o") || model.starts_with("codex") { + Some(Reasoning { + effort, + summary: summary.into(), + }) + } else { + None + } +} + 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 01ff459f65..0a03fe60aa 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 d948ddb916..58c557f76e 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,9 @@ pub struct Config { /// /// When this program is invoked, arg0 will be set to `codex-linux-sandbox`. pub codex_linux_sandbox_exe: Option, + + pub model_reasoning_effort: ReasoningEffort, + pub model_reasoning_summary: ReasoningSummary, } impl Config { @@ -281,6 +286,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 +452,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 +796,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 +838,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 +895,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 d89b09f267..7cfb530328 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -7,6 +7,7 @@ use std::collections::HashMap; use wildmatch::WildMatchPattern; use serde::Deserialize; +use serde::Serialize; #[derive(Deserialize, Debug, Clone, PartialEq)] pub struct McpServerConfig { @@ -175,3 +176,29 @@ 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)] +#[serde(rename_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)] +#[serde(rename_all = "lowercase")] +pub enum ReasoningSummary { + #[default] + Auto, + Concise, + Detailed, + /// Option to disable reasoning summaries. + None, +} diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index fc18f1d821..737acc7732 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