diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 685e355cd..d1ade2182 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -35,10 +35,10 @@ use crate::auth::RefreshTokenError; use crate::chat_completions::AggregateStreamExt; use crate::chat_completions::stream_chat_completions; use crate::client_common::Prompt; +use crate::client_common::Reasoning; use crate::client_common::ResponseEvent; use crate::client_common::ResponseStream; use crate::client_common::ResponsesApiRequest; -use crate::client_common::create_reasoning_param_for_request; use crate::client_common::create_text_param_for_request; use crate::config::Config; use crate::default_client::CodexHttpClient; @@ -199,12 +199,18 @@ impl ModelClient { let auth_manager = self.auth_manager.clone(); let full_instructions = prompt.get_full_instructions(&self.config.model_family); - let tools_json = create_tools_json_for_responses_api(&prompt.tools)?; - let reasoning = create_reasoning_param_for_request( - &self.config.model_family, - self.effort, - self.summary, - ); + let tools_json: Vec = create_tools_json_for_responses_api(&prompt.tools)?; + + let reasoning = if self.config.model_family.supports_reasoning_summaries { + Some(Reasoning { + effort: self + .effort + .or(self.config.model_family.default_reasoning_effort), + summary: Some(self.summary), + }) + } else { + None + }; let include: Vec = if reasoning.is_some() { vec!["reasoning.encrypted_content".to_string()] diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index c402d322c..7d33751e2 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -342,21 +342,6 @@ pub(crate) mod tools { } } -pub(crate) fn create_reasoning_param_for_request( - model_family: &ModelFamily, - effort: Option, - summary: ReasoningSummaryConfig, -) -> Option { - if !model_family.supports_reasoning_summaries { - return None; - } - - Some(Reasoning { - effort, - summary: Some(summary), - }) -} - pub(crate) fn create_text_param_for_request( verbosity: Option, output_schema: &Option, diff --git a/codex-rs/core/src/model_family.rs b/codex-rs/core/src/model_family.rs index 0d904a8a8..b46fae4db 100644 --- a/codex-rs/core/src/model_family.rs +++ b/codex-rs/core/src/model_family.rs @@ -1,3 +1,4 @@ +use codex_protocol::config_types::ReasoningEffort; use codex_protocol::config_types::Verbosity; use crate::config::types::ReasoningSummaryFormat; @@ -31,6 +32,9 @@ pub struct ModelFamily { // `summary` is optional). pub supports_reasoning_summaries: bool, + // The reasoning effort to use for this model family when none is explicitly chosen. + pub default_reasoning_effort: Option, + // Define if we need a special handling of reasoning summary pub reasoning_summary_format: ReasoningSummaryFormat, @@ -84,6 +88,7 @@ macro_rules! model_family { support_verbosity: false, shell_type: ConfigShellToolType::Default, default_verbosity: None, + default_reasoning_effort: None, }; // apply overrides @@ -181,6 +186,7 @@ pub fn find_family_for_model(slug: &str) -> Option { support_verbosity: true, default_verbosity: Some(Verbosity::Low), base_instructions: GPT_5_1_INSTRUCTIONS.to_string(), + default_reasoning_effort: Some(ReasoningEffort::Medium), ) } else if slug.starts_with("gpt-5") { model_family!( @@ -209,5 +215,6 @@ pub fn derive_default_model_family(model: &str) -> ModelFamily { support_verbosity: false, shell_type: ConfigShellToolType::Default, default_verbosity: None, + default_reasoning_effort: None, } } diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 3184ca816..8c740e633 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -21,6 +21,7 @@ use codex_core::protocol::Op; use codex_core::protocol::SessionSource; use codex_otel::otel_event_manager::OtelEventManager; use codex_protocol::ConversationId; +use codex_protocol::config_types::ReasoningEffort; use codex_protocol::config_types::Verbosity; use codex_protocol::models::ReasoningItemContent; use codex_protocol::models::ReasoningItemReasoningSummary; @@ -625,6 +626,121 @@ async fn includes_user_instructions_message_in_request() { assert_message_ends_with(&request_body["input"][1], ""); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn includes_configured_effort_in_request() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + let server = MockServer::start().await; + + let resp_mock = + responses::mount_sse_once_match(&server, path("/v1/responses"), sse_completed("resp1")) + .await; + let TestCodex { codex, .. } = test_codex() + .with_model("gpt-5.1-codex") + .with_config(|config| { + config.model_reasoning_effort = Some(ReasoningEffort::Medium); + }) + .build(&server) + .await?; + + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + let request = resp_mock.single_request(); + let request_body = request.body_json(); + + assert_eq!( + request_body + .get("reasoning") + .and_then(|t| t.get("effort")) + .and_then(|v| v.as_str()), + Some("medium") + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn includes_no_effort_in_request() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + let server = MockServer::start().await; + + let resp_mock = + responses::mount_sse_once_match(&server, path("/v1/responses"), sse_completed("resp1")) + .await; + let TestCodex { codex, .. } = test_codex() + .with_model("gpt-5.1-codex") + .build(&server) + .await?; + + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + let request = resp_mock.single_request(); + let request_body = request.body_json(); + + assert_eq!( + request_body + .get("reasoning") + .and_then(|t| t.get("effort")) + .and_then(|v| v.as_str()), + None + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn includes_default_reasoning_effort_in_request_when_defined_by_model_family() +-> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + let server = MockServer::start().await; + + let resp_mock = + responses::mount_sse_once_match(&server, path("/v1/responses"), sse_completed("resp1")) + .await; + let TestCodex { codex, .. } = test_codex().with_model("gpt-5.1").build(&server).await?; + + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "hello".into(), + }], + }) + .await + .unwrap(); + + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + let request = resp_mock.single_request(); + let request_body = request.body_json(); + + assert_eq!( + request_body + .get("reasoning") + .and_then(|t| t.get("effort")) + .and_then(|v| v.as_str()), + Some("medium") + ); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn includes_default_verbosity_in_request() -> anyhow::Result<()> { skip_if_no_network!(Ok(()));