From 56a8470aa0b0c5b9f174f449a697fe6ccc6d9462 Mon Sep 17 00:00:00 2001 From: felixxia-oai Date: Sat, 5 Sep 2026 21:44:36 +0000 Subject: [PATCH] Record reasoning effort changes in conversation history behind a flag (#43110) ## What changed Add the disabled-by-default `reasoning_effort_override` feature for OpenAI models with `use_responses_lite` enabled. Append a trusted `configuration_update` after accepted input when no effort is established in surviving history or the resolved effort changes. Preserve the existing history prefix and continue sending the request-level reasoning effort. Share effort normalization through `ModelInfo::resolve_reasoning_effort` so requests and history updates resolve `ultra` consistently and translate `persistent` to `disabled`. Exclude other custom effort values from history updates, and compare only against harness-authored configuration items. ## Testing Add integration coverage for effort transitions, deduplication, history prefix and cache-key preservation, alias normalization, and feature/provider/model gating. Add unit coverage for model-specific `ultra` resolution, fallbacks, and `persistent` translation. GitOrigin-RevId: c3fe7050058076454d8ea20054a618bca9d48c7c --- codex-rs/core/config.schema.json | 6 + codex-rs/core/src/client.rs | 40 +-- codex-rs/core/src/session/mod.rs | 1 + codex-rs/core/src/session/reasoning_effort.rs | 87 +++++ codex-rs/core/src/session/turn.rs | 4 + codex-rs/core/tests/suite/mod.rs | 1 + .../tests/suite/reasoning_effort_override.rs | 316 ++++++++++++++++++ codex-rs/features/src/lib.rs | 8 + codex-rs/protocol/src/openai_models.rs | 3 +- .../src/openai_models/reasoning_effort.rs | 44 +++ .../openai_models/reasoning_effort_tests.rs | 52 +++ 11 files changed, 523 insertions(+), 39 deletions(-) create mode 100644 codex-rs/core/src/session/reasoning_effort.rs create mode 100644 codex-rs/core/tests/suite/reasoning_effort_override.rs create mode 100644 codex-rs/protocol/src/openai_models/reasoning_effort.rs create mode 100644 codex-rs/protocol/src/openai_models/reasoning_effort_tests.rs diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 095a2bcc59..2d5d268a67 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -879,6 +879,9 @@ "realtime_conversation": { "type": "boolean" }, + "reasoning_effort_override": { + "type": "boolean" + }, "recommended_plugins": { "type": "boolean" }, @@ -6271,6 +6274,9 @@ "realtime_conversation": { "type": "boolean" }, + "reasoning_effort_override": { + "type": "boolean" + }, "recommended_plugins": { "type": "boolean" }, diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 75ed0747ae..a813022548 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -183,42 +183,6 @@ pub(crate) struct CompactConversationRequestSettings { pub(crate) service_tier: Option, } -fn reasoning_effort_for_request( - model_info: &ModelInfo, - effort: ReasoningEffortConfig, -) -> ReasoningEffortConfig { - match effort { - ReasoningEffortConfig::Ultra => model_info - .multi_agent_reasoning_effort - .as_ref() - .filter(|effort| { - *effort != &ReasoningEffortConfig::Ultra - && model_info - .supported_reasoning_levels - .iter() - .any(|preset| &preset.effort == *effort) - }) - .cloned() - .or_else(|| { - let supported_reasoning_levels = &model_info.supported_reasoning_levels; - supported_reasoning_levels - .iter() - .find(|preset| preset.effort == ReasoningEffortConfig::Max) - .or_else(|| { - supported_reasoning_levels - .iter() - .rev() - .find(|preset| preset.effort != ReasoningEffortConfig::Ultra) - }) - .map(|preset| preset.effort.clone()) - }) - .unwrap_or(ReasoningEffortConfig::Medium), - // Keep "persistent" in local settings; the Responses API calls it "disabled". - ReasoningEffortConfig::Persistent => ReasoningEffortConfig::Custom("disabled".to_string()), - effort => effort, - } -} - fn session_telemetry_for_request( session_telemetry: &SessionTelemetry, request: &ResponsesApiRequest, @@ -806,7 +770,7 @@ impl ModelClient { model: model_info.slug.clone(), raw_memories, reasoning: effort - .map(|effort| reasoning_effort_for_request(model_info, effort)) + .map(|effort| model_info.resolve_reasoning_effort(effort)) .map(|effort| Reasoning { effort: Some(effort), summary: None, @@ -912,7 +876,7 @@ impl ModelClient { Reasoning { effort: effort .or_else(|| model_info.default_reasoning_level.clone()) - .map(|effort| reasoning_effort_for_request(model_info, effort)), + .map(|effort| model_info.resolve_reasoning_effort(effort)), summary: (model_info.supports_reasoning_summary_parameter && summary != ReasoningSummaryConfig::None) .then_some(summary), diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index e4730cbf7b..712fe63e23 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -230,6 +230,7 @@ mod mcp_refresh; mod mcp_runtime; pub(crate) mod multi_agents; mod realtime_history; +mod reasoning_effort; mod retained_context; mod review; mod rollout_budget; diff --git a/codex-rs/core/src/session/reasoning_effort.rs b/codex-rs/core/src/session/reasoning_effort.rs new file mode 100644 index 0000000000..c11d010fa7 --- /dev/null +++ b/codex-rs/core/src/session/reasoning_effort.rs @@ -0,0 +1,87 @@ +//! Gated reasoning-effort updates appended after accepted input. +//! +//! Only trusted harness items establish an effort. Updates append to surviving +//! history without replacing the request-level reasoning effort. + +use super::session::Session; +use super::step_context::StepContext; +use super::step_settings::ResolvedStepSettings; +use codex_features::Feature; +use codex_history::CodexHarnessMetadata; +use codex_history::ResponseItemEnvelope; +use codex_protocol::models::ConfigurationReasoning; +use codex_protocol::models::ResponseItem; +use codex_protocol::openai_models::ReasoningEffort; + +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 { + return; + }; + let should_skip = { + let state = self.state.lock().await; + let established_effort = + state + .history + .annotated_items() + .iter() + .rev() + .find_map(|envelope| { + if !envelope + .metadata + .as_ref() + .is_some_and(|metadata| metadata.harness_authored_configuration) + { + return None; + } + match &envelope.item { + ResponseItem::ConfigurationUpdate { reasoning } => { + Some(&reasoning.effort) + } + _ => None, + } + }); + established_effort == Some(&effort) + }; + if should_skip { + return; + } + + self.record_annotated_conversation_items( + step_context.turn.as_ref(), + vec![ResponseItemEnvelope { + item: ResponseItem::ConfigurationUpdate { + reasoning: ConfigurationReasoning { effort }, + }, + metadata: Some(CodexHarnessMetadata { + harness_authored_configuration: true, + ..Default::default() + }), + }], + ) + .await; + } + + async fn effort_for_configuration_update( + &self, + settings: &ResolvedStepSettings, + ) -> Option { + if !self.enabled(Feature::ReasoningEffortOverride) + || !settings.model_info.use_responses_lite + || !self.provider().await.is_openai() + { + return None; + } + let effort = settings + .model_info + .resolve_reasoning_effort(settings.effective_reasoning_effort()?); + // Persistent normalizes to "disabled". Keep unknown custom values out of + // durable updates so injected items stay bounded to known backend modes. + if matches!(&effort, ReasoningEffort::Custom(value) if value != "disabled") { + return None; + } + Some(effort) + } +} diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index fe99a18a2b..26ff799a44 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -413,6 +413,10 @@ pub(crate) async fn run_turn( .record_step_world_state_if_changed(&world_state, step_context.as_ref()) .await?; + // Keep the override after accepted input so ordinary turn rollback removes it too. + sess.record_reasoning_effort_override(step_context.as_ref()) + .await; + // Construct the input that we will send to the model. let sampling_request_input: Vec = async { sess.clone_history() diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index a31883290b..2040f6ba3b 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -128,6 +128,7 @@ mod quota_exceeded; mod realtime_conversation; mod realtime_initial_items; mod realtime_sideband_endpoint; +mod reasoning_effort_override; mod remote_env; mod remote_models; mod request_compression; diff --git a/codex-rs/core/tests/suite/reasoning_effort_override.rs b/codex-rs/core/tests/suite/reasoning_effort_override.rs new file mode 100644 index 0000000000..8fa7be8dcf --- /dev/null +++ b/codex-rs/core/tests/suite/reasoning_effort_override.rs @@ -0,0 +1,316 @@ +//! Trusted reasoning-effort updates follow surviving history and the next turn's selected settings. + +use codex_features::Feature; +use codex_protocol::openai_models::ReasoningEffort; +use codex_protocol::openai_models::ReasoningEffortPreset; +use codex_protocol::protocol::ThreadSettingsOverrides; +use core_test_support::responses; +use core_test_support::responses::ResponsesRequest; +use core_test_support::skip_if_no_network; +use core_test_support::submit_thread_settings; +use core_test_support::test_codex::TestCodexBuilder; +use core_test_support::test_codex::test_codex; +use pretty_assertions::assert_eq; +use serde_json::Value; +use test_case::test_case; + +fn override_builder() -> TestCodexBuilder { + test_codex() + .with_model_info_override("gpt-5.4", |model| { + model.use_responses_lite = true; + }) + .with_config(|config| { + config + .features + .enable(Feature::ReasoningEffortOverride) + .expect("enable reasoning effort overrides"); + config.model_reasoning_effort = Some(ReasoningEffort::Medium); + }) +} + +fn effort_updates(request: &ResponsesRequest) -> Vec { + request + .input() + .into_iter() + .filter(|item| item["type"] == "configuration_update") + .collect() +} + +fn effort_update(effort: ReasoningEffort) -> Value { + serde_json::json!({ + "type": "configuration_update", + "reasoning": {"effort": effort}, + }) +} + +fn message(role: &str, text: &str) -> Value { + serde_json::json!({ + "type": "message", + "role": role, + "content": [{"type": "input_text", "text": text}], + }) +} + +#[test_case(ReasoningEffort::High; "high to persistent and back")] +#[test_case(ReasoningEffort::Persistent; "persistent to high and back")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn reasoning_effort_override_persistent_transitions( + initial_effort: ReasoningEffort, +) -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + let server = responses::start_mock_server().await; + let mut mocks = Vec::new(); + for id in ["first", "changed", "unchanged", "restored"] { + mocks.push( + responses::mount_sse_once(&server, responses::sse(vec![responses::ev_completed(id)])) + .await, + ); + } + let high = effort_update(ReasoningEffort::High); + let disabled = effort_update(ReasoningEffort::Custom("disabled".to_string())); + let (changed_effort, initial_update, changed_update) = + if initial_effort == ReasoningEffort::Persistent { + (ReasoningEffort::High, disabled, high) + } else { + (ReasoningEffort::Persistent, high, disabled) + }; + let test = override_builder().build_with_auto_env(&server).await?; + for effort in [ + initial_effort.clone(), + changed_effort.clone(), + changed_effort, + initial_effort, + ] { + submit_thread_settings( + &test.codex, + ThreadSettingsOverrides { + effort: Some(Some(effort)), + ..Default::default() + }, + ) + .await?; + test.submit_text_turn("continue").await?; + } + assert_eq!( + mocks + .iter() + .map(|mock| effort_updates(&mock.single_request())) + .collect::>(), + vec![ + vec![initial_update.clone()], + vec![initial_update.clone(), changed_update.clone()], + vec![initial_update.clone(), changed_update.clone()], + vec![initial_update.clone(), changed_update, initial_update], + ], + ); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn reasoning_effort_override_preserves_prefix_and_only_appends_on_change() +-> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + let server = responses::start_mock_server().await; + let mut mocks = Vec::new(); + for id in ["first", "changed", "unchanged", "lowered"] { + mocks.push( + responses::mount_sse_once(&server, responses::sse(vec![responses::ev_completed(id)])) + .await, + ); + } + let test = override_builder().build_with_auto_env(&server).await?; + test.submit_text_turn("first message").await?; + submit_thread_settings( + &test.codex, + ThreadSettingsOverrides { + effort: Some(Some(ReasoningEffort::High)), + ..Default::default() + }, + ) + .await?; + test.submit_text_turn("second message").await?; + test.submit_text_turn("third message").await?; + submit_thread_settings( + &test.codex, + ThreadSettingsOverrides { + effort: Some(Some(ReasoningEffort::Low)), + ..Default::default() + }, + ) + .await?; + test.submit_text_turn("fourth message").await?; + + let requests = mocks + .iter() + .map(responses::ResponseMock::single_request) + .collect::>(); + let inputs = requests + .iter() + .map(|request| { + responses::strip_response_item_ids_from_json(responses::strip_metadata_from_json( + Value::Array(request.input()), + )) + .as_array() + .expect("input array") + .clone() + }) + .collect::>(); + assert_eq!( + requests + .iter() + .map(|request| request.body_json()["reasoning"]["effort"].clone()) + .collect::>(), + vec![ + Value::from("medium"), + Value::from("high"), + Value::from("high"), + Value::from("low"), + ], + ); + let cache_keys = requests + .iter() + .map(|request| request.body_json()["prompt_cache_key"].clone()) + .collect::>(); + assert_eq!(cache_keys, vec![cache_keys[0].clone(); requests.len()]); + assert_eq!( + inputs[0][inputs[0].len() - 2..], + [ + message("user", "first message"), + effort_update(ReasoningEffort::Medium) + ], + ); + let mut expected = inputs[0].clone(); + expected.extend([ + message("user", "second message"), + effort_update(ReasoningEffort::High), + ]); + assert_eq!(inputs[1], expected); + expected.push(message("user", "third message")); + assert_eq!(inputs[2], expected); + expected.extend([ + message("user", "fourth message"), + effort_update(ReasoningEffort::Low), + ]); + assert_eq!(inputs[3], expected); + Ok(()) +} + +#[test_case(ReasoningEffort::High; "model maps ultra to high")] +#[test_case(ReasoningEffort::Max; "model maps ultra to max")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn reasoning_effort_override_normalizes_ultra_before_comparing_updates( + resolved_effort: ReasoningEffort, +) -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + let server = responses::start_mock_server().await; + let first = responses::mount_sse_once( + &server, + responses::sse(vec![responses::ev_completed("first")]), + ) + .await; + let second = responses::mount_sse_once( + &server, + responses::sse(vec![responses::ev_completed("second")]), + ) + .await; + let model_effort = resolved_effort.clone(); + let test = override_builder() + .with_model_info_override("gpt-5.4", move |model| { + model.multi_agent_reasoning_effort = Some(model_effort.clone()); + model.supported_reasoning_levels = vec![ReasoningEffortPreset { + effort: model_effort, + description: "Model effort".to_string(), + }]; + }) + .with_config(|config| config.model_reasoning_effort = Some(ReasoningEffort::Ultra)) + .build_with_auto_env(&server) + .await?; + test.submit_text_turn("ultra selection").await?; + submit_thread_settings( + &test.codex, + ThreadSettingsOverrides { + effort: Some(Some(resolved_effort.clone())), + ..Default::default() + }, + ) + .await?; + test.submit_text_turn("equivalent explicit effort").await?; + let requests = [first.single_request(), second.single_request()]; + assert_eq!( + requests.each_ref().map(effort_updates), + [ + vec![effort_update(resolved_effort.clone())], + vec![effort_update(resolved_effort.clone())], + ] + ); + assert_eq!( + requests.map(|request| request.body_json()["reasoning"]["effort"].clone()), + [ + serde_json::to_value(&resolved_effort)?, + serde_json::to_value(&resolved_effort)? + ] + ); + Ok(()) +} + +#[derive(Clone, Copy)] +enum OverrideUnavailable { + FeatureDisabled, + NonOpenAiProvider, + ResponsesLiteDisabled, +} + +#[test_case(OverrideUnavailable::FeatureDisabled; "feature disabled")] +#[test_case(OverrideUnavailable::NonOpenAiProvider; "unsupported provider")] +#[test_case(OverrideUnavailable::ResponsesLiteDisabled; "responses lite disabled")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn reasoning_effort_override_unavailable_uses_request_effort( + unavailable: OverrideUnavailable, +) -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + let server = responses::start_mock_server().await; + let first = responses::mount_sse_once( + &server, + responses::sse(vec![responses::ev_completed("first")]), + ) + .await; + let second = responses::mount_sse_once( + &server, + 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?; + test.submit_text_turn("first").await?; + submit_thread_settings( + &test.codex, + ThreadSettingsOverrides { + effort: Some(Some(ReasoningEffort::High)), + ..Default::default() + }, + ) + .await?; + test.submit_text_turn("second").await?; + let requests = [first.single_request(), second.single_request()]; + assert_eq!( + requests.each_ref().map(effort_updates), + [Vec::::new(), Vec::new()] + ); + assert_eq!( + requests.map(|request| request.body_json()["reasoning"]["effort"].clone()), + [Value::from("medium"), Value::from("high")], + ); + Ok(()) +} diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 83d16523af..5b5206047c 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -324,6 +324,8 @@ pub enum Feature { ContextManagement, /// Track and report a shared token budget across a session's agent threads. RolloutBudget, + /// Append trusted response configuration items when the selected reasoning effort changes. + ReasoningEffortOverride, /// Add current-time reminders to model-visible context. CurrentTimeReminder, /// Route MCP tool approval prompts through the MCP elicitation request path. @@ -1607,6 +1609,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::UnderDevelopment, default_enabled: false, }, + FeatureSpec { + id: Feature::ReasoningEffortOverride, + key: "reasoning_effort_override", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::CurrentTimeReminder, key: "current_time_reminder", diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index 649b8e2125..f5336ddc5a 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -41,6 +41,8 @@ pub use guardian::GuardianScope; #[path = "openai_models/guardian_v2.rs"] mod guardian_v2; +#[path = "openai_models/reasoning_effort.rs"] +mod reasoning_effort; pub use guardian_v2::GuardianV2ModelConfig; pub use guardian_v2::GuardianV2TranscriptModelConfig; @@ -1020,7 +1022,6 @@ mod tests { multi_agent_reasoning_effort: None, } } - fn personality_variables() -> ModelInstructionsVariables { ModelInstructionsVariables { personality_default: Some("default".to_string()), diff --git a/codex-rs/protocol/src/openai_models/reasoning_effort.rs b/codex-rs/protocol/src/openai_models/reasoning_effort.rs new file mode 100644 index 0000000000..b4709202c3 --- /dev/null +++ b/codex-rs/protocol/src/openai_models/reasoning_effort.rs @@ -0,0 +1,44 @@ +//! Model-owned effort normalization shared by native requests and configuration update items. +//! +//! UI aliases resolve through the same model defaults and fallbacks in both paths. + +use super::ModelInfo; +use super::ReasoningEffort; + +impl ModelInfo { + /// Resolves a selected effort to the value sent in an ordinary inference request. + pub fn resolve_reasoning_effort(&self, effort: ReasoningEffort) -> ReasoningEffort { + match effort { + ReasoningEffort::Ultra => self + .multi_agent_reasoning_effort + .as_ref() + .filter(|effort| { + *effort != &ReasoningEffort::Ultra + && self + .supported_reasoning_levels + .iter() + .any(|preset| &preset.effort == *effort) + }) + .cloned() + .or_else(|| { + self.supported_reasoning_levels + .iter() + .find(|preset| preset.effort == ReasoningEffort::Max) + .or_else(|| { + self.supported_reasoning_levels + .iter() + .rev() + .find(|preset| preset.effort != ReasoningEffort::Ultra) + }) + .map(|preset| preset.effort.clone()) + }) + .unwrap_or(ReasoningEffort::Medium), + // Keep "persistent" in local settings; the Responses API calls it "disabled". + ReasoningEffort::Persistent => ReasoningEffort::Custom("disabled".to_string()), + effort => effort, + } + } +} +#[cfg(test)] +#[path = "reasoning_effort_tests.rs"] +mod tests; diff --git a/codex-rs/protocol/src/openai_models/reasoning_effort_tests.rs b/codex-rs/protocol/src/openai_models/reasoning_effort_tests.rs new file mode 100644 index 0000000000..e36fe3a256 --- /dev/null +++ b/codex-rs/protocol/src/openai_models/reasoning_effort_tests.rs @@ -0,0 +1,52 @@ +use super::super::ReasoningEffortPreset; +use super::super::tests::test_model; +use super::ReasoningEffort; +use pretty_assertions::assert_eq; + +#[test] +fn resolves_ultra_using_model_override_and_supported_fallbacks() { + use ReasoningEffort::High; + use ReasoningEffort::Low; + use ReasoningEffort::Max; + use ReasoningEffort::Medium; + use ReasoningEffort::Ultra; + use ReasoningEffort::XHigh; + + for (supported, multi_agent_effort, expected) in [ + (vec![High, Max, Ultra], Some(High), High), + (vec![High, Max, Ultra], None, Max), + (vec![High, XHigh, Ultra], None, XHigh), + (vec![High, XHigh, Ultra], Some(Ultra), XHigh), + (vec![High, XHigh, Ultra], Some(Low), XHigh), + (vec![], None, Medium), + ] { + let mut model = test_model(/*spec*/ None); + model.supported_reasoning_levels = supported + .into_iter() + .map(|effort| ReasoningEffortPreset { + effort, + description: String::new(), + }) + .collect(); + model.multi_agent_reasoning_effort = multi_agent_effort; + assert_eq!(model.resolve_reasoning_effort(Ultra), expected); + } +} + +#[test] +fn preserves_native_efforts_and_translates_persistent_mode() { + let model = test_model(/*spec*/ None); + for effort in [ + ReasoningEffort::Low, + ReasoningEffort::Medium, + ReasoningEffort::High, + ReasoningEffort::Max, + ReasoningEffort::Custom("future-effort".to_string()), + ] { + assert_eq!(model.resolve_reasoning_effort(effort.clone()), effort); + } + assert_eq!( + model.resolve_reasoning_effort(ReasoningEffort::Persistent), + ReasoningEffort::Custom("disabled".to_string()), + ); +}