diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index e00e269222..0b14bc0035 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -67,6 +67,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { supports_search_tool: false, supports_experimental_context: false, use_responses_lite: false, + supports_reasoning_effort_updates: false, guardian: None, node_repl_auto_review_required: false, node_repl_disabled: false, diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index b02958ffa8..2227437618 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -155,6 +155,7 @@ async fn models_client_hits_models_endpoint() { supports_search_tool: false, supports_experimental_context: false, use_responses_lite: false, + supports_reasoning_effort_updates: true, guardian: None, node_repl_auto_review_required: true, node_repl_disabled: true, diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index c3724aee0a..637ce95e7c 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -528,8 +528,10 @@ impl ModelClient { } } - pub(crate) fn reasoning_effort_override_enabled(&self) -> bool { + pub(crate) fn reasoning_effort_override_enabled(&self, model_info: &ModelInfo) -> bool { self.state.reasoning_effort_override_enabled + && self.state.provider.info().is_openai() + && model_info.supports_reasoning_effort_updates } pub(crate) fn with_restored_history(mut self, restored_history: bool) -> Self { @@ -867,8 +869,8 @@ impl ModelClient { responses_metadata: &CodexResponsesMetadata, ) -> Result { let mut input = prompt.get_formatted_input_for_request(model_info); - if !self.state.reasoning_effort_override_enabled { - // Disabling overrides must also recover threads with saved updates. + if !self.reasoning_effort_override_enabled(model_info) { + // Unsupported models and disabled overrides must also accept saved history. // Filter only the request copy; persisted history remains unchanged. input.retain(|item| !matches!(item, ResponseItem::ConfigurationUpdate { .. })); } diff --git a/codex-rs/core/src/session/reasoning_effort.rs b/codex-rs/core/src/session/reasoning_effort.rs index 3ff9e4049f..19e00db3c7 100644 --- a/codex-rs/core/src/session/reasoning_effort.rs +++ b/codex-rs/core/src/session/reasoning_effort.rs @@ -4,6 +4,7 @@ //! baseline while it is retained. //! Successful compaction retires the overrides and allows a fresh request baseline. //! Synchronous Guardian reviewers always use their selected request-level effort. +//! Unsupported models use selected request effort without rewriting saved updates. use super::session::Session; use super::step_context::StepContext; @@ -26,7 +27,7 @@ impl Session { /// Establishes the selected effort in surviving history, independent of replayed settings. pub(crate) async fn record_reasoning_effort_override(&self, step_context: &StepContext) { let settings = &step_context.settings; - let Some(effort) = self.effort_for_configuration_update(settings).await else { + let Some(effort) = self.effort_for_configuration_update(settings) else { return; }; let should_skip = { @@ -99,8 +100,11 @@ impl Session { if !self .services .model_client - .reasoning_effort_override_enabled() + .reasoning_effort_override_enabled(&settings.model_info) { + if usage == RequestEffortUsage::Sampling { + self.state.lock().await.reasoning_effort_pin = ReasoningEffortPin::Unset; + } return selected_effort; } if usage == RequestEffortUsage::Compaction @@ -113,7 +117,7 @@ impl Session { { return Some(pinned); } - let effort = self.effort_for_configuration_update(settings).await; + let effort = self.effort_for_configuration_update(settings); let mut state = self.state.lock().await; let Some(effort) = effort else { if usage == RequestEffortUsage::Sampling { @@ -130,16 +134,14 @@ impl Session { }) } - async fn effort_for_configuration_update( + fn effort_for_configuration_update( &self, settings: &ResolvedStepSettings, ) -> Option { if !self .services .model_client - .reasoning_effort_override_enabled() - || !settings.model_info.use_responses_lite - || !self.provider().await.is_openai() + .reasoning_effort_override_enabled(&settings.model_info) { return None; } diff --git a/codex-rs/core/src/session/reasoning_effort_tests.rs b/codex-rs/core/src/session/reasoning_effort_tests.rs index 2b8ce76afc..bee5f9f2a3 100644 --- a/codex-rs/core/src/session/reasoning_effort_tests.rs +++ b/codex-rs/core/src/session/reasoning_effort_tests.rs @@ -33,7 +33,7 @@ async fn initial_replay_preserves_prewarmed_effort(history: InitialHistory) { ) .await; let mut model_info = Arc::clone(&turn_context.initial_settings.model_info); - Arc::make_mut(&mut model_info).use_responses_lite = true; + Arc::make_mut(&mut model_info).supports_reasoning_effort_updates = true; let mut selected = turn_context.initial_settings.selected().clone(); selected.collaboration_mode.settings.reasoning_effort = Some(ReasoningEffort::Medium); let prewarm_settings = ResolvedStepSettings::new( @@ -87,7 +87,7 @@ async fn compaction_effort_lookup_preserves_pin_for_fallback_models() { let effort = ReasoningEffort::Medium; let model = Arc::make_mut(&mut settings.model_info); model.slug = "fallback".to_string(); - model.use_responses_lite = true; + model.supports_reasoning_effort_updates = true; model.default_reasoning_level = Some(effort.clone()); assert_eq!( @@ -106,3 +106,57 @@ async fn compaction_effort_lookup_preserves_pin_for_fallback_models() { Some(ReasoningEffort::Low) ); } + +#[tokio::test] +async fn unsupported_model_compaction_uses_selected_effort_without_mutating_pin() { + let (session, turn_context, _events) = make_session_and_context_with_auth_and_config_and_rx( + CodexAuth::from_api_key("Test API Key"), + Vec::new(), + |config| { + config + .features + .enable(Feature::ReasoningEffortOverride) + .unwrap(); + config.model_reasoning_effort = Some(ReasoningEffort::High); + }, + ) + .await; + let mut settings = (*turn_context.initial_settings).clone(); + Arc::make_mut(&mut settings.model_info).supports_reasoning_effort_updates = false; + session + .state + .lock() + .await + .reasoning_effort_pin + .pin(&settings.model_info.slug, ReasoningEffort::Low); + + assert_eq!( + session + .reasoning_effort_for_request(&settings, RequestEffortUsage::Compaction) + .await, + Some(ReasoningEffort::High), + ); + assert_eq!( + session + .state + .lock() + .await + .reasoning_effort_pin + .get(&settings.model_info.slug), + Some(ReasoningEffort::Low), + ); + // Sampling retires the old baseline, so a later supported request starts afresh. + assert_eq!( + session + .reasoning_effort_for_request(&settings, RequestEffortUsage::Sampling) + .await, + Some(ReasoningEffort::High), + ); + Arc::make_mut(&mut settings.model_info).supports_reasoning_effort_updates = true; + assert_eq!( + session + .reasoning_effort_for_request(&settings, RequestEffortUsage::Sampling) + .await, + Some(ReasoningEffort::High), + ); +} diff --git a/codex-rs/core/tests/suite/auto_review.rs b/codex-rs/core/tests/suite/auto_review.rs index 7f6b1a6eb2..6f5c9fc7d9 100644 --- a/codex-rs/core/tests/suite/auto_review.rs +++ b/codex-rs/core/tests/suite/auto_review.rs @@ -615,6 +615,7 @@ fn remote_model_with_auto_review_override(slug: &str, review_model: &str) -> Mod supports_search_tool: false, supports_experimental_context: false, use_responses_lite: false, + supports_reasoning_effort_updates: false, node_repl_auto_review_required: false, node_repl_disabled: false, auto_review_model_override: Some(review_model.to_string()), diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 2beb7f554a..cfbcefc9d8 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -1038,6 +1038,7 @@ async fn reasoning_effort_override_remote_v2_compaction_resets_pinned_effort( .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_model_info_override("gpt-5.4", |model| { model.use_responses_lite = true; + model.supports_reasoning_effort_updates = true; }) .with_config(move |config| { config.model_provider.stream_max_retries = Some(0); diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index cebe8ff426..97a39d9a28 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -126,6 +126,7 @@ fn test_model_info( supports_search_tool: false, supports_experimental_context: false, use_responses_lite: false, + supports_reasoning_effort_updates: false, guardian: None, node_repl_auto_review_required: false, node_repl_disabled: false, @@ -1253,6 +1254,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< supports_search_tool: false, supports_experimental_context: false, use_responses_lite: false, + supports_reasoning_effort_updates: false, guardian: None, node_repl_auto_review_required: false, node_repl_disabled: false, diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 3cba8b6eff..4a3cf558c1 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -679,6 +679,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo { supports_search_tool: false, supports_experimental_context: false, use_responses_lite: false, + supports_reasoning_effort_updates: false, guardian: None, node_repl_auto_review_required: false, node_repl_disabled: false, diff --git a/codex-rs/core/tests/suite/reasoning_effort_override.rs b/codex-rs/core/tests/suite/reasoning_effort_override.rs index 0099d94d28..ca317492fe 100644 --- a/codex-rs/core/tests/suite/reasoning_effort_override.rs +++ b/codex-rs/core/tests/suite/reasoning_effort_override.rs @@ -43,6 +43,7 @@ fn override_builder() -> TestCodexBuilder { test_codex() .with_model_info_override("gpt-5.4", |model| { model.use_responses_lite = true; + model.supports_reasoning_effort_updates = true; }) .with_config(|config| { config @@ -354,9 +355,12 @@ async fn reasoning_effort_override_persistent_transitions( Ok(()) } +#[test_case(true; "responses lite")] +#[test_case(false; "ordinary responses")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn reasoning_effort_override_preserves_prefix_and_only_appends_on_change() --> anyhow::Result<()> { +async fn reasoning_effort_override_preserves_prefix_and_only_appends_on_change( + use_responses_lite: bool, +) -> anyhow::Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; let mut mocks = Vec::new(); @@ -366,7 +370,12 @@ async fn reasoning_effort_override_preserves_prefix_and_only_appends_on_change() .await, ); } - let test = override_builder().build_with_auto_env(&server).await?; + let test = override_builder() + .with_model_info_override("gpt-5.4", move |model| { + model.use_responses_lite = use_responses_lite; + }) + .build_with_auto_env(&server) + .await?; test.submit_text_turn("first message").await?; submit_thread_settings( &test.codex, @@ -718,8 +727,12 @@ async fn reasoning_effort_override_websocket_appends_then_replays_after_reconnec Ok(()) } +#[test_case(OverrideUnavailable::FeatureDisabled; "feature disabled")] +#[test_case(OverrideUnavailable::ModelUnsupported; "unsupported model")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn reasoning_effort_override_disabled_filters_websocket_history() -> anyhow::Result<()> { +async fn reasoning_effort_override_unavailable_filters_websocket_history( + unavailable: OverrideUnavailable, +) -> anyhow::Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; let initial_mock = responses::mount_sse_once( @@ -747,11 +760,7 @@ async fn reasoning_effort_override_disabled_filters_websocket_history() -> anyho ]]) .await; let base_url = format!("{}/v1", websocket.uri()); - let mut builder = override_builder().with_config(move |config| { - config - .features - .disable(Feature::ReasoningEffortOverride) - .expect("disable overrides"); + let mut builder = unavailable.builder().with_config(move |config| { config.model_reasoning_effort = Some(ReasoningEffort::High); config.model_provider.base_url = Some(base_url); config.model_provider.supports_websockets = true; @@ -788,12 +797,33 @@ async fn reasoning_effort_override_disabled_filters_websocket_history() -> anyho enum OverrideUnavailable { FeatureDisabled, NonOpenAiProvider, - ResponsesLiteDisabled, + ModelUnsupported, +} + +impl OverrideUnavailable { + fn builder(self) -> TestCodexBuilder { + match self { + Self::FeatureDisabled => override_builder().with_config(|config| { + config + .features + .disable(Feature::ReasoningEffortOverride) + .expect("disable overrides"); + }), + Self::NonOpenAiProvider => override_builder().with_config(|config| { + config.model_provider.name = "unsupported provider".into(); + }), + Self::ModelUnsupported => { + override_builder().with_model_info_override("gpt-5.4", |model| { + model.supports_reasoning_effort_updates = false; + }) + } + } + } } #[test_case(OverrideUnavailable::FeatureDisabled; "feature disabled")] #[test_case(OverrideUnavailable::NonOpenAiProvider; "unsupported provider")] -#[test_case(OverrideUnavailable::ResponsesLiteDisabled; "responses lite disabled")] +#[test_case(OverrideUnavailable::ModelUnsupported; "unsupported model")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn reasoning_effort_override_unavailable_uses_request_effort( unavailable: OverrideUnavailable, @@ -810,20 +840,7 @@ async fn reasoning_effort_override_unavailable_uses_request_effort( responses::sse(vec![responses::ev_completed("second")]), ) .await; - let mut builder = match unavailable { - OverrideUnavailable::FeatureDisabled => override_builder().with_config(|config| { - config - .features - .disable(Feature::ReasoningEffortOverride) - .expect("disable overrides"); - }), - OverrideUnavailable::NonOpenAiProvider => override_builder().with_config(|config| { - config.model_provider.name = "unsupported provider".into(); - }), - OverrideUnavailable::ResponsesLiteDisabled => override_builder() - .with_model_info_override("gpt-5.4", |model| model.use_responses_lite = false), - }; - let test = builder.build_with_auto_env(&server).await?; + let test = unavailable.builder().build_with_auto_env(&server).await?; test.submit_text_turn("first").await?; submit_thread_settings( &test.codex, @@ -846,8 +863,12 @@ async fn reasoning_effort_override_unavailable_uses_request_effort( Ok(()) } +#[test_case(OverrideUnavailable::FeatureDisabled; "feature disabled")] +#[test_case(OverrideUnavailable::ModelUnsupported; "unsupported model")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn reasoning_effort_override_disabled_recovers_saved_history() -> anyhow::Result<()> { +async fn reasoning_effort_override_unavailable_recovers_saved_history( + unavailable: OverrideUnavailable, +) -> anyhow::Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; let reply = |id, text| { @@ -891,13 +912,10 @@ async fn reasoning_effort_override_disabled_recovers_saved_history() -> anyhow:: }) .await; let chatgpt_base_url = format!("{}/backend-api", server.uri()); - let mut builder = override_builder() + let mut builder = unavailable + .builder() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(move |config| { - config - .features - .disable(Feature::ReasoningEffortOverride) - .expect("disable overrides"); config.model_reasoning_effort = Some(ReasoningEffort::High); config.chatgpt_base_url = chatgpt_base_url; }); @@ -984,6 +1002,7 @@ async fn reasoning_effort_override_model_switch_reestablishes_selected_effort( .with_model_info_override("gpt-5.5", |model| { model.comp_hash = None; model.use_responses_lite = true; + model.supports_reasoning_effort_updates = true; }) .with_model("gpt-5.4") .build_with_auto_env(&server) @@ -1021,6 +1040,145 @@ async fn reasoning_effort_override_model_switch_reestablishes_selected_effort( Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn reasoning_effort_override_unsupported_model_round_trip() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + let server = responses::start_mock_server().await; + let test = override_builder() + .with_model_info_override("gpt-6-astra", |model| { + model.comp_hash = None; + model.use_responses_lite = true; + model.supports_reasoning_effort_updates = true; + }) + .with_model_info_override("gpt-5.5", |model| { + model.comp_hash = None; + // Lite alone is not sufficient: unsupported models must still filter updates. + model.use_responses_lite = true; + model.supports_reasoning_effort_updates = false; + }) + .with_model("gpt-6-astra") + .build_with_auto_env(&server) + .await?; + let turns = [ + ("gpt-6-astra", ReasoningEffort::Medium, "Start at medium."), + ( + "gpt-6-astra", + ReasoningEffort::High, + "Raise effort to high.", + ), + ( + "gpt-5.5", + ReasoningEffort::High, + "Switch to the unsupported model.", + ), + ("gpt-5.5", ReasoningEffort::Low, "Lower effort to low."), + ( + "gpt-6-astra", + ReasoningEffort::Low, + "Return to the supported model.", + ), + ("gpt-6-astra", ReasoningEffort::High, "Raise effort again."), + ]; + let mut requests = Vec::new(); + for (index, (model, effort, prompt)) in turns.iter().enumerate() { + let id = format!("turn-{index}"); + let mock = responses::mount_sse_once( + &server, + responses::sse(vec![ + responses::ev_assistant_message(&id, "Acknowledged."), + responses::ev_completed(&id), + ]), + ) + .await; + submit_thread_settings( + &test.codex, + ThreadSettingsOverrides { + model: Some((*model).to_string()), + effort: Some(Some(effort.clone())), + ..Default::default() + }, + ) + .await?; + test.submit_text_turn(prompt).await?; + requests.push(mock.single_request()); + } + let initial_updates = vec![ + effort_update(ReasoningEffort::Medium), + effort_update(ReasoningEffort::High), + ]; + let mut returned_updates = initial_updates.clone(); + returned_updates.push(effort_update(ReasoningEffort::Low)); + let mut final_updates = returned_updates.clone(); + final_updates.push(effort_update(ReasoningEffort::High)); + assert_eq!( + requests + .iter() + .map(|request| { + let body = request.body_json(); + ( + body["model"].clone(), + body["reasoning"]["effort"].clone(), + effort_updates(request), + ) + }) + .collect::>(), + vec![ + ( + Value::from("gpt-6-astra"), + Value::from("medium"), + vec![effort_update(ReasoningEffort::Medium)] + ), + ( + Value::from("gpt-6-astra"), + Value::from("medium"), + initial_updates + ), + (Value::from("gpt-5.5"), Value::from("high"), vec![]), + (Value::from("gpt-5.5"), Value::from("low"), vec![]), + ( + Value::from("gpt-6-astra"), + Value::from("low"), + returned_updates + ), + ( + Value::from("gpt-6-astra"), + Value::from("low"), + final_updates.clone() + ), + ], + ); + for (index, request) in requests.iter().enumerate() { + assert_eq!( + request + .message_input_texts("user") + .into_iter() + .filter(|text| { turns.iter().any(|(_, _, prompt)| text.as_str() == *prompt) }) + .collect::>(), + turns[..=index] + .iter() + .map(|(_, _, prompt)| prompt.to_string()) + .collect::>(), + ); + } + test.codex.shutdown_and_wait().await?; + let saved_updates = test + .codex + .load_history(/*include_archived*/ false) + .await? + .items + .into_iter() + .filter_map(|item| match item { + RolloutItem::ResponseItem(ResponseItemEnvelope { + item: item @ ResponseItem::ConfigurationUpdate { .. }, + .. + }) => Some(serde_json::to_value(item).expect("serialize saved update")), + _ => None, + }) + .collect::>(); + assert_eq!(saved_updates, final_updates); + Ok(()) +} + #[test_case(ReasoningEffort::High; "unchanged effort")] #[test_case(ReasoningEffort::Medium; "changed effort")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -1129,6 +1287,7 @@ async fn reasoning_effort_override_compaction_fallback_uses_each_models_effort() .with_model_info_override("gpt-5.2", |model| { model.comp_hash = Some("fallback".to_string()); model.use_responses_lite = true; + model.supports_reasoning_effort_updates = true; }) .with_model("gpt-5.4") .with_config(move |config| { diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 0568116770..b5a652c5a9 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -584,6 +584,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { supports_search_tool: false, supports_experimental_context: false, use_responses_lite: false, + supports_reasoning_effort_updates: false, guardian: None, node_repl_auto_review_required: false, node_repl_disabled: false, @@ -858,6 +859,7 @@ async fn remote_models_apply_legacy_instructions(auth: CodexAuth) -> Result<()> supports_search_tool: false, supports_experimental_context: false, use_responses_lite: false, + supports_reasoning_effort_updates: false, guardian: None, node_repl_auto_review_required: false, node_repl_disabled: false, @@ -1440,6 +1442,7 @@ fn test_remote_model_with_policy( supports_search_tool: false, supports_experimental_context: false, use_responses_lite: false, + supports_reasoning_effort_updates: false, guardian: None, node_repl_auto_review_required: false, node_repl_disabled: false, diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 2bb9eee369..714b305f5b 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -3008,6 +3008,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re supports_search_tool: false, supports_experimental_context: false, use_responses_lite: false, + supports_reasoning_effort_updates: false, guardian: None, node_repl_auto_review_required: false, node_repl_disabled: false, diff --git a/codex-rs/core/tests/suite/spawn_agent_description.rs b/codex-rs/core/tests/suite/spawn_agent_description.rs index 2df387602f..bc0eaf7580 100644 --- a/codex-rs/core/tests/suite/spawn_agent_description.rs +++ b/codex-rs/core/tests/suite/spawn_agent_description.rs @@ -92,6 +92,7 @@ fn test_model_info( supports_search_tool: false, supports_experimental_context: false, use_responses_lite: false, + supports_reasoning_effort_updates: false, guardian: None, node_repl_auto_review_required: false, node_repl_disabled: false, diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index e29c41d6cd..62479ce0e6 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -1624,6 +1624,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an supports_search_tool: false, supports_experimental_context: false, use_responses_lite: false, + supports_reasoning_effort_updates: false, guardian: None, node_repl_auto_review_required: false, node_repl_disabled: false, diff --git a/codex-rs/models-manager/src/model_info.rs b/codex-rs/models-manager/src/model_info.rs index ece19ad26a..88ade7cfcf 100644 --- a/codex-rs/models-manager/src/model_info.rs +++ b/codex-rs/models-manager/src/model_info.rs @@ -137,6 +137,7 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo { supports_search_tool: false, supports_experimental_context: false, use_responses_lite: false, + supports_reasoning_effort_updates: false, guardian: None, node_repl_auto_review_required: false, node_repl_disabled: false, diff --git a/codex-rs/prompts/src/model_instructions_tests.rs b/codex-rs/prompts/src/model_instructions_tests.rs index 9fa9bb5879..540cbabc79 100644 --- a/codex-rs/prompts/src/model_instructions_tests.rs +++ b/codex-rs/prompts/src/model_instructions_tests.rs @@ -48,6 +48,7 @@ fn test_model(model_messages: Option) -> ModelInfo { supports_search_tool: false, supports_experimental_context: false, use_responses_lite: false, + supports_reasoning_effort_updates: false, guardian: None, node_repl_auto_review_required: false, node_repl_disabled: false, diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index f52a4e601b..41b2a60dd5 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -481,6 +481,10 @@ pub struct ModelInfo { pub supports_experimental_context: bool, #[serde(default)] pub use_responses_lite: bool, + /// Whether the model accepts reasoning-effort `configuration_update` items. + /// Missing metadata keeps effort changes on the ordinary request parameter. + #[serde(default)] + pub supports_reasoning_effort_updates: bool, #[serde(default)] pub node_repl_auto_review_required: bool, #[serde(default)] @@ -989,6 +993,7 @@ mod tests { supports_search_tool: false, supports_experimental_context: false, use_responses_lite: false, + supports_reasoning_effort_updates: false, guardian: None, node_repl_auto_review_required: false, node_repl_disabled: false, @@ -1543,6 +1548,7 @@ mod tests { assert!(!model.supports_search_tool); assert!(!model.supports_experimental_context); assert!(!model.use_responses_lite); + assert!(!model.supports_reasoning_effort_updates); assert!(!model.node_repl_auto_review_required); assert!(!model.node_repl_disabled); assert_eq!(model.comp_hash, None);