From 9b2055586d450fc94ad4a530aebdffe17f303d9b Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Thu, 4 Dec 2025 11:57:58 -0800 Subject: [PATCH] remove `model_family` from `config (#7571) - Remove `model_family` from `config` - Make sure to still override config elements related to `model_family` like supporting reasoning --- codex-rs/common/src/config_summary.rs | 13 +- codex-rs/core/src/client.rs | 39 +++--- codex-rs/core/src/codex.rs | 56 +++++--- codex-rs/core/src/config/mod.rs | 35 ++--- .../core/src/openai_models/model_family.rs | 13 ++ .../core/src/openai_models/models_manager.rs | 6 +- codex-rs/core/src/sandboxing/assessment.rs | 8 +- codex-rs/core/src/truncate.rs | 4 +- .../core/tests/chat_completions_payload.rs | 9 +- codex-rs/core/tests/chat_completions_sse.rs | 10 +- codex-rs/core/tests/common/test_codex.rs | 2 + codex-rs/core/tests/responses_headers.rs | 126 +++++++++++++++++- codex-rs/core/tests/suite/client.rs | 10 +- codex-rs/core/tests/suite/prompt_caching.rs | 15 ++- .../core/tests/suite/shell_serialization.rs | 69 ++++------ codex-rs/tui/src/app.rs | 6 +- codex-rs/tui/src/chatwidget.rs | 5 +- codex-rs/tui/src/history_cell.rs | 71 +++++++--- 18 files changed, 339 insertions(+), 158 deletions(-) diff --git a/codex-rs/common/src/config_summary.rs b/codex-rs/common/src/config_summary.rs index 8fc1bb26f..32b837f1f 100644 --- a/codex-rs/common/src/config_summary.rs +++ b/codex-rs/common/src/config_summary.rs @@ -12,15 +12,14 @@ pub fn create_config_summary_entries(config: &Config) -> Vec<(&'static str, Stri ("approval", config.approval_policy.to_string()), ("sandbox", summarize_sandbox_policy(&config.sandbox_policy)), ]; - if config.model_provider.wire_api == WireApi::Responses - && config.model_family.supports_reasoning_summaries - { + if config.model_provider.wire_api == WireApi::Responses { let reasoning_effort = config .model_reasoning_effort - .or(config.model_family.default_reasoning_effort) - .map(|effort| effort.to_string()) - .unwrap_or_else(|| "none".to_string()); - entries.push(("reasoning effort", reasoning_effort)); + .map(|effort| effort.to_string()); + entries.push(( + "reasoning effort", + reasoning_effort.unwrap_or_else(|| "none".to_string()), + )); entries.push(( "reasoning summaries", config.model_reasoning_summary.to_string(), diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index a3c990cdb..4c3cf737b 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -57,6 +57,7 @@ use crate::tools::spec::create_tools_json_for_responses_api; pub struct ModelClient { config: Arc, auth_manager: Option>, + model_family: ModelFamily, otel_event_manager: OtelEventManager, provider: ModelProviderInfo, conversation_id: ConversationId, @@ -70,6 +71,7 @@ impl ModelClient { pub fn new( config: Arc, auth_manager: Option>, + model_family: ModelFamily, otel_event_manager: OtelEventManager, provider: ModelProviderInfo, effort: Option, @@ -80,6 +82,7 @@ impl ModelClient { Self { config, auth_manager, + model_family, otel_event_manager, provider, conversation_id, @@ -90,16 +93,18 @@ impl ModelClient { } pub fn get_model_context_window(&self) -> Option { - let pct = self.config.model_family.effective_context_window_percent; + let model_family = self.get_model_family(); + let effective_context_window_percent = model_family.effective_context_window_percent; self.config .model_context_window - .or_else(|| get_model_info(&self.config.model_family).map(|info| info.context_window)) - .map(|w| w.saturating_mul(pct) / 100) + .or_else(|| get_model_info(&model_family).map(|info| info.context_window)) + .map(|w| w.saturating_mul(effective_context_window_percent) / 100) } pub fn get_auto_compact_token_limit(&self) -> Option { + let model_family = self.get_model_family(); self.config.model_auto_compact_token_limit.or_else(|| { - get_model_info(&self.config.model_family).and_then(|info| info.auto_compact_token_limit) + get_model_info(&model_family).and_then(|info| info.auto_compact_token_limit) }) } @@ -149,9 +154,8 @@ impl ModelClient { } let auth_manager = self.auth_manager.clone(); - let instructions = prompt - .get_full_instructions(&self.config.model_family) - .into_owned(); + let model_family = self.get_model_family(); + let instructions = prompt.get_full_instructions(&model_family).into_owned(); let tools_json = create_tools_json_for_chat_completions_api(&prompt.tools)?; let api_prompt = build_api_prompt(prompt, instructions, tools_json); let conversation_id = self.conversation_id.to_string(); @@ -204,16 +208,13 @@ impl ModelClient { } let auth_manager = self.auth_manager.clone(); - let instructions = prompt - .get_full_instructions(&self.config.model_family) - .into_owned(); + let model_family = self.get_model_family(); + let instructions = prompt.get_full_instructions(&model_family).into_owned(); let tools_json: Vec = create_tools_json_for_responses_api(&prompt.tools)?; - let reasoning = if self.config.model_family.supports_reasoning_summaries { + let reasoning = if model_family.supports_reasoning_summaries { Some(Reasoning { - effort: self - .effort - .or(self.config.model_family.default_reasoning_effort), + effort: self.effort.or(model_family.default_reasoning_effort), summary: Some(self.summary), }) } else { @@ -226,15 +227,15 @@ impl ModelClient { vec![] }; - let verbosity = if self.config.model_family.support_verbosity { + let verbosity = if model_family.support_verbosity { self.config .model_verbosity - .or(self.config.model_family.default_verbosity) + .or(model_family.default_verbosity) } else { if self.config.model_verbosity.is_some() { warn!( "model_verbosity is set but ignored as the model does not support verbosity: {}", - self.config.model_family.family + model_family.family ); } None @@ -305,7 +306,7 @@ impl ModelClient { /// Returns the currently configured model family. pub fn get_model_family(&self) -> ModelFamily { - self.config.model_family.clone() + self.model_family.clone() } /// Returns the current reasoning effort setting. @@ -342,7 +343,7 @@ impl ModelClient { .with_telemetry(Some(request_telemetry)); let instructions = prompt - .get_full_instructions(&self.config.model_family) + .get_full_instructions(&self.get_model_family()) .into_owned(); let payload = ApiCompactionInput { model: &self.config.model, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 34cde906e..94951c055 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -398,7 +398,7 @@ pub(crate) struct SessionSettingsUpdate { impl Session { fn make_turn_context( auth_manager: Option>, - models_manager: &ModelsManager, + models_manager: Arc, otel_event_manager: &OtelEventManager, provider: ModelProviderInfo, session_configuration: &SessionConfiguration, @@ -407,13 +407,13 @@ impl Session { ) -> TurnContext { let config = session_configuration.original_config_do_not_use.clone(); let features = &config.features; - let model_family = models_manager.construct_model_family(&session_configuration.model); let mut per_turn_config = (*config).clone(); per_turn_config.model = session_configuration.model.clone(); - per_turn_config.model_family = model_family.clone(); per_turn_config.model_reasoning_effort = session_configuration.model_reasoning_effort; per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary; per_turn_config.features = features.clone(); + let model_family = + models_manager.construct_model_family(&per_turn_config.model, &per_turn_config); if let Some(model_info) = get_model_info(&model_family) { per_turn_config.model_context_window = Some(model_info.context_window); } @@ -426,6 +426,7 @@ impl Session { let client = ModelClient::new( Arc::new(per_turn_config.clone()), auth_manager, + model_family.clone(), otel_event_manager, provider, session_configuration.model_reasoning_effort, @@ -455,7 +456,10 @@ impl Session { codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), tool_call_gate: Arc::new(ReadinessFlag::new()), exec_policy: session_configuration.exec_policy.clone(), - truncation_policy: TruncationPolicy::new(&per_turn_config), + truncation_policy: TruncationPolicy::new( + &per_turn_config, + model_family.truncation_policy, + ), } } @@ -539,10 +543,12 @@ impl Session { }); } + let model_family = models_manager.construct_model_family(&config.model, &config); + // todo(aibrahim): why are we passing model here while it can change? let otel_event_manager = OtelEventManager::new( conversation_id, config.model.as_str(), - config.model_family.slug.as_str(), + model_family.slug.as_str(), auth_manager.auth().and_then(|a| a.get_account_id()), auth_manager.auth().and_then(|a| a.get_account_email()), auth_manager.auth().map(|a| a.mode), @@ -762,7 +768,7 @@ impl Session { let mut turn_context: TurnContext = Self::make_turn_context( Some(Arc::clone(&self.services.auth_manager)), - &self.services.models_manager, + Arc::clone(&self.services.models_manager), &self.services.otel_event_manager, session_configuration.provider.clone(), &session_configuration, @@ -862,6 +868,7 @@ impl Session { auth_manager, &otel, self.conversation_id, + self.services.models_manager.clone(), turn_context.client.get_session_source(), call_id, command, @@ -1891,7 +1898,10 @@ async fn spawn_review_thread( resolved: crate::review_prompts::ResolvedReviewRequest, ) { let model = config.review_model.clone(); - let review_model_family = sess.services.models_manager.construct_model_family(&model); + let review_model_family = sess + .services + .models_manager + .construct_model_family(&model, &config); // For reviews, disable web_search and view_image regardless of global settings. let mut review_features = sess.features.clone(); review_features @@ -1911,7 +1921,6 @@ async fn spawn_review_thread( // Build per‑turn client with the requested model/family. let mut per_turn_config = (*config).clone(); per_turn_config.model = model.clone(); - per_turn_config.model_family = model_family.clone(); per_turn_config.model_reasoning_effort = Some(ReasoningEffortConfig::Low); per_turn_config.model_reasoning_summary = ReasoningSummaryConfig::Detailed; per_turn_config.features = review_features.clone(); @@ -1924,13 +1933,14 @@ async fn spawn_review_thread( .get_otel_event_manager() .with_model( per_turn_config.model.as_str(), - per_turn_config.model_family.slug.as_str(), + review_model_family.slug.as_str(), ); let per_turn_config = Arc::new(per_turn_config); let client = ModelClient::new( per_turn_config.clone(), auth_manager, + model_family.clone(), otel_event_manager, provider, per_turn_config.model_reasoning_effort, @@ -1955,7 +1965,7 @@ async fn spawn_review_thread( codex_linux_sandbox_exe: parent_turn_context.codex_linux_sandbox_exe.clone(), tool_call_gate: Arc::new(ReadinessFlag::new()), exec_policy: parent_turn_context.exec_policy.clone(), - truncation_policy: TruncationPolicy::new(&per_turn_config), + truncation_policy: TruncationPolicy::new(&per_turn_config, model_family.truncation_policy), }; // Seed the child task with the review prompt as the initial user message. @@ -2149,10 +2159,7 @@ async fn run_turn( let mut base_instructions = turn_context.base_instructions.clone(); if parallel_tool_calls { static INSTRUCTIONS: &str = include_str!("../templates/parallel/instructions.md"); - let family = sess - .services - .models_manager - .construct_model_family(&sess.state.lock().await.session_configuration.model); + let family = turn_context.client.get_model_family(); let mut new_instructions = base_instructions.unwrap_or(family.base_instructions); new_instructions.push_str(INSTRUCTIONS); base_instructions = Some(new_instructions); @@ -2787,11 +2794,18 @@ mod tests { }) } - fn otel_event_manager(conversation_id: ConversationId, config: &Config) -> OtelEventManager { + fn otel_event_manager( + conversation_id: ConversationId, + config: &Config, + models_manager: &ModelsManager, + ) -> OtelEventManager { OtelEventManager::new( conversation_id, config.model.as_str(), - config.model_family.slug.as_str(), + models_manager + .construct_model_family(&config.model, config) + .slug + .as_str(), None, Some("test@test.com".to_string()), Some(AuthMode::ChatGPT), @@ -2811,13 +2825,14 @@ mod tests { .expect("load default test config"); let config = Arc::new(config); let conversation_id = ConversationId::default(); - let otel_event_manager = otel_event_manager(conversation_id, config.as_ref()); let auth_manager = AuthManager::shared( config.cwd.clone(), false, config.cli_auth_credentials_store_mode, ); let models_manager = Arc::new(ModelsManager::new(auth_manager.get_auth_mode())); + let otel_event_manager = + otel_event_manager(conversation_id, config.as_ref(), &models_manager); let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), @@ -2854,7 +2869,7 @@ mod tests { let turn_context = Session::make_turn_context( Some(Arc::clone(&auth_manager)), - &models_manager, + models_manager, &otel_event_manager, session_configuration.provider.clone(), &session_configuration, @@ -2892,13 +2907,14 @@ mod tests { .expect("load default test config"); let config = Arc::new(config); let conversation_id = ConversationId::default(); - let otel_event_manager = otel_event_manager(conversation_id, config.as_ref()); let auth_manager = AuthManager::shared( config.cwd.clone(), false, config.cli_auth_credentials_store_mode, ); let models_manager = Arc::new(ModelsManager::new(auth_manager.get_auth_mode())); + let otel_event_manager = + otel_event_manager(conversation_id, config.as_ref(), &models_manager); let session_configuration = SessionConfiguration { provider: config.model_provider.clone(), @@ -2935,7 +2951,7 @@ mod tests { let turn_context = Arc::new(Session::make_turn_context( Some(Arc::clone(&auth_manager)), - &models_manager, + models_manager, &otel_event_manager, session_configuration.provider.clone(), &session_configuration, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index def63df84..a1cc46cf2 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -27,7 +27,6 @@ use crate::model_provider_info::ModelProviderInfo; use crate::model_provider_info::OLLAMA_OSS_PROVIDER_ID; use crate::model_provider_info::built_in_model_providers; use crate::openai_model_info::get_model_info; -use crate::openai_models::model_family::ModelFamily; use crate::openai_models::model_family::find_family_for_model; use crate::project_doc::DEFAULT_PROJECT_DOC_FILENAME; use crate::project_doc::LOCAL_PROJECT_DOC_FILENAME; @@ -80,9 +79,6 @@ pub struct Config { /// Model used specifically for review sessions. Defaults to "gpt-5.1-codex-max". pub review_model: String, - // todo(aibrahim): remove this field - pub model_family: ModelFamily, - /// Size of the context window for the model, in tokens. pub model_context_window: Option, @@ -195,6 +191,7 @@ pub struct Config { /// Additional filenames to try when looking for project-level docs. pub project_doc_fallback_filenames: Vec, + // todo(aibrahim): this should be used in the override model family /// Token budget applied when storing tool/function outputs in the context manager. pub tool_output_token_limit: Option, @@ -225,6 +222,12 @@ pub struct Config { /// request using the Responses API. pub model_reasoning_summary: ReasoningSummary, + /// Optional override to force-enable reasoning summaries for the configured model. + pub model_supports_reasoning_summaries: Option, + + /// Optional override to force reasoning summary format for the configured model. + pub model_reasoning_summary_format: Option, + /// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`). pub model_verbosity: Option, @@ -1108,14 +1111,7 @@ impl Config { .or(cfg.model) .unwrap_or_else(default_model); - let mut model_family = find_family_for_model(&model); - - if let Some(supports_reasoning_summaries) = cfg.model_supports_reasoning_summaries { - model_family.supports_reasoning_summaries = supports_reasoning_summaries; - } - if let Some(model_reasoning_summary_format) = cfg.model_reasoning_summary_format { - model_family.reasoning_summary_format = model_reasoning_summary_format; - } + let model_family = find_family_for_model(&model); let openai_model_info = get_model_info(&model_family); let model_context_window = cfg @@ -1172,7 +1168,6 @@ impl Config { let config = Self { model, review_model, - model_family, model_context_window, model_auto_compact_token_limit, model_provider_id, @@ -1228,6 +1223,8 @@ impl Config { .model_reasoning_summary .or(cfg.model_reasoning_summary) .unwrap_or_default(), + model_supports_reasoning_summaries: cfg.model_supports_reasoning_summaries, + model_reasoning_summary_format: cfg.model_reasoning_summary_format.clone(), model_verbosity: config_profile.model_verbosity.or(cfg.model_verbosity), chatgpt_base_url: config_profile .chatgpt_base_url @@ -2953,7 +2950,6 @@ model_verbosity = "high" Config { model: "o3".to_string(), review_model: OPENAI_DEFAULT_REVIEW_MODEL.to_string(), - model_family: find_family_for_model("o3"), model_context_window: Some(200_000), model_auto_compact_token_limit: Some(180_000), model_provider_id: "openai".to_string(), @@ -2981,6 +2977,8 @@ model_verbosity = "high" show_raw_agent_reasoning: false, model_reasoning_effort: Some(ReasoningEffort::High), model_reasoning_summary: ReasoningSummary::Detailed, + model_supports_reasoning_summaries: None, + model_reasoning_summary_format: None, model_verbosity: None, chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), base_instructions: None, @@ -3027,7 +3025,6 @@ model_verbosity = "high" let expected_gpt3_profile_config = Config { model: "gpt-3.5-turbo".to_string(), review_model: OPENAI_DEFAULT_REVIEW_MODEL.to_string(), - model_family: find_family_for_model("gpt-3.5-turbo"), model_context_window: Some(16_385), model_auto_compact_token_limit: Some(14_746), model_provider_id: "openai-chat-completions".to_string(), @@ -3055,6 +3052,8 @@ model_verbosity = "high" show_raw_agent_reasoning: false, model_reasoning_effort: None, model_reasoning_summary: ReasoningSummary::default(), + model_supports_reasoning_summaries: None, + model_reasoning_summary_format: None, model_verbosity: None, chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), base_instructions: None, @@ -3116,7 +3115,6 @@ model_verbosity = "high" let expected_zdr_profile_config = Config { model: "o3".to_string(), review_model: OPENAI_DEFAULT_REVIEW_MODEL.to_string(), - model_family: find_family_for_model("o3"), model_context_window: Some(200_000), model_auto_compact_token_limit: Some(180_000), model_provider_id: "openai".to_string(), @@ -3144,6 +3142,8 @@ model_verbosity = "high" show_raw_agent_reasoning: false, model_reasoning_effort: None, model_reasoning_summary: ReasoningSummary::default(), + model_supports_reasoning_summaries: None, + model_reasoning_summary_format: None, model_verbosity: None, chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), base_instructions: None, @@ -3191,7 +3191,6 @@ model_verbosity = "high" let expected_gpt5_profile_config = Config { model: "gpt-5.1".to_string(), review_model: OPENAI_DEFAULT_REVIEW_MODEL.to_string(), - model_family: find_family_for_model("gpt-5.1"), model_context_window: Some(272_000), model_auto_compact_token_limit: Some(244_800), model_provider_id: "openai".to_string(), @@ -3219,6 +3218,8 @@ model_verbosity = "high" show_raw_agent_reasoning: false, model_reasoning_effort: Some(ReasoningEffort::High), model_reasoning_summary: ReasoningSummary::Detailed, + model_supports_reasoning_summaries: None, + model_reasoning_summary_format: None, model_verbosity: Some(Verbosity::High), chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(), base_instructions: None, diff --git a/codex-rs/core/src/openai_models/model_family.rs b/codex-rs/core/src/openai_models/model_family.rs index 1580ab137..9d89e1499 100644 --- a/codex-rs/core/src/openai_models/model_family.rs +++ b/codex-rs/core/src/openai_models/model_family.rs @@ -1,6 +1,7 @@ use codex_protocol::config_types::Verbosity; use codex_protocol::openai_models::ReasoningEffort; +use crate::config::Config; use crate::config::types::ReasoningSummaryFormat; use crate::tools::handlers::apply_patch::ApplyPatchToolType; use crate::tools::spec::ConfigShellToolType; @@ -72,6 +73,18 @@ pub struct ModelFamily { pub truncation_policy: TruncationPolicy, } +impl ModelFamily { + pub fn with_config_overrides(mut self, config: &Config) -> Self { + if let Some(supports_reasoning_summaries) = config.model_supports_reasoning_summaries { + self.supports_reasoning_summaries = supports_reasoning_summaries; + } + if let Some(reasoning_summary_format) = config.model_reasoning_summary_format.as_ref() { + self.reasoning_summary_format = reasoning_summary_format.clone(); + } + self + } +} + macro_rules! model_family { ( $slug:expr, $family:expr $(, $key:ident : $value:expr )* $(,)? diff --git a/codex-rs/core/src/openai_models/models_manager.rs b/codex-rs/core/src/openai_models/models_manager.rs index c6f936542..23124f10d 100644 --- a/codex-rs/core/src/openai_models/models_manager.rs +++ b/codex-rs/core/src/openai_models/models_manager.rs @@ -2,10 +2,12 @@ use codex_app_server_protocol::AuthMode; use codex_protocol::openai_models::ModelPreset; use tokio::sync::RwLock; +use crate::config::Config; use crate::openai_models::model_family::ModelFamily; use crate::openai_models::model_family::find_family_for_model; use crate::openai_models::model_presets::builtin_model_presets; +#[derive(Debug)] pub struct ModelsManager { pub available_models: RwLock>, pub etag: String, @@ -26,7 +28,7 @@ impl ModelsManager { *self.available_models.write().await = models; } - pub fn construct_model_family(&self, model: &str) -> ModelFamily { - find_family_for_model(model) + pub fn construct_model_family(&self, model: &str, config: &Config) -> ModelFamily { + find_family_for_model(model).with_config_overrides(config) } } diff --git a/codex-rs/core/src/sandboxing/assessment.rs b/codex-rs/core/src/sandboxing/assessment.rs index 225825c93..8a34a9332 100644 --- a/codex-rs/core/src/sandboxing/assessment.rs +++ b/codex-rs/core/src/sandboxing/assessment.rs @@ -10,6 +10,7 @@ use crate::client::ModelClient; use crate::client_common::Prompt; use crate::client_common::ResponseEvent; use crate::config::Config; +use crate::openai_models::models_manager::ModelsManager; use crate::protocol::SandboxPolicy; use askama::Template; use codex_otel::otel_event_manager::OtelEventManager; @@ -46,6 +47,7 @@ pub(crate) async fn assess_command( auth_manager: Arc, parent_otel: &OtelEventManager, conversation_id: ConversationId, + models_manager: Arc, session_source: SessionSource, call_id: &str, command: &[String], @@ -124,12 +126,14 @@ pub(crate) async fn assess_command( output_schema: Some(sandbox_assessment_schema()), }; - let child_otel = - parent_otel.with_model(config.model.as_str(), config.model_family.slug.as_str()); + let model_family = models_manager.construct_model_family(&config.model, &config); + + let child_otel = parent_otel.with_model(config.model.as_str(), model_family.slug.as_str()); let client = ModelClient::new( Arc::clone(&config), Some(auth_manager), + model_family, child_otel, provider, Some(SANDBOX_ASSESSMENT_REASONING_EFFORT), diff --git a/codex-rs/core/src/truncate.rs b/codex-rs/core/src/truncate.rs index 6e38ef698..1c710ba10 100644 --- a/codex-rs/core/src/truncate.rs +++ b/codex-rs/core/src/truncate.rs @@ -26,10 +26,10 @@ impl TruncationPolicy { } } - pub fn new(config: &Config) -> Self { + pub fn new(config: &Config, truncation_policy: TruncationPolicy) -> Self { let config_token_limit = config.tool_output_token_limit; - match config.model_family.truncation_policy { + match truncation_policy { TruncationPolicy::Bytes(family_bytes) => { if let Some(token_limit) = config_token_limit { Self::Bytes(approx_bytes_for_tokens(token_limit)) diff --git a/codex-rs/core/tests/chat_completions_payload.rs b/codex-rs/core/tests/chat_completions_payload.rs index accac55e0..f24a9d642 100644 --- a/codex-rs/core/tests/chat_completions_payload.rs +++ b/codex-rs/core/tests/chat_completions_payload.rs @@ -10,6 +10,7 @@ use codex_core::ModelProviderInfo; use codex_core::Prompt; use codex_core::ResponseItem; use codex_core::WireApi; +use codex_core::openai_models::models_manager::ModelsManager; use codex_otel::otel_event_manager::OtelEventManager; use codex_protocol::ConversationId; use codex_protocol::models::ReasoningItemContent; @@ -70,14 +71,15 @@ async fn run_request(input: Vec) -> Value { let config = Arc::new(config); let conversation_id = ConversationId::new(); - + let models_manager = Arc::new(ModelsManager::new(Some(AuthMode::ApiKey))); + let model_family = models_manager.construct_model_family(&config.model, &config); let otel_event_manager = OtelEventManager::new( conversation_id, config.model.as_str(), - config.model_family.slug.as_str(), + model_family.slug.as_str(), None, Some("test@test.com".to_string()), - Some(AuthMode::ChatGPT), + Some(AuthMode::ApiKey), false, "test".to_string(), ); @@ -85,6 +87,7 @@ async fn run_request(input: Vec) -> Value { let client = ModelClient::new( Arc::clone(&config), None, + model_family, otel_event_manager, provider, effort, diff --git a/codex-rs/core/tests/chat_completions_sse.rs b/codex-rs/core/tests/chat_completions_sse.rs index 7b8716360..f50f3f2ca 100644 --- a/codex-rs/core/tests/chat_completions_sse.rs +++ b/codex-rs/core/tests/chat_completions_sse.rs @@ -1,4 +1,5 @@ use assert_matches::assert_matches; +use codex_core::openai_models::models_manager::ModelsManager; use std::sync::Arc; use tracing_test::traced_test; @@ -70,14 +71,16 @@ async fn run_stream_with_bytes(sse_body: &[u8]) -> Vec { let config = Arc::new(config); let conversation_id = ConversationId::new(); - + let auth_mode = AuthMode::ApiKey; + let models_manager = Arc::new(ModelsManager::new(Some(auth_mode))); + let model_family = models_manager.construct_model_family(&config.model, &config); let otel_event_manager = OtelEventManager::new( conversation_id, config.model.as_str(), - config.model_family.slug.as_str(), + model_family.slug.as_str(), None, Some("test@test.com".to_string()), - Some(AuthMode::ChatGPT), + Some(auth_mode), false, "test".to_string(), ); @@ -85,6 +88,7 @@ async fn run_stream_with_bytes(sse_body: &[u8]) -> Vec { let client = ModelClient::new( Arc::clone(&config), None, + model_family, otel_event_manager, provider, effort, diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index aff2ab60d..23bcadadf 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -118,6 +118,7 @@ impl TestCodexBuilder { config, codex: new_conversation.conversation, session_configured: new_conversation.session_configured, + conversation_manager: Arc::new(conversation_manager), }) } @@ -160,6 +161,7 @@ pub struct TestCodex { pub codex: Arc, pub session_configured: SessionConfiguredEvent, pub config: Config, + pub conversation_manager: Arc, } impl TestCodex { diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 7b6f645f2..14264921f 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -8,8 +8,11 @@ use codex_core::Prompt; use codex_core::ResponseEvent; use codex_core::ResponseItem; use codex_core::WireApi; +use codex_core::config::types::ReasoningSummaryFormat; +use codex_core::openai_models::models_manager::ModelsManager; use codex_otel::otel_event_manager::OtelEventManager; use codex_protocol::ConversationId; +use codex_protocol::config_types::ReasoningSummary; use codex_protocol::protocol::SessionSource; use core_test_support::load_default_config_for_test; use core_test_support::responses; @@ -59,14 +62,16 @@ async fn responses_stream_includes_subagent_header_on_review() { let config = Arc::new(config); let conversation_id = ConversationId::new(); - + let auth_mode = AuthMode::ChatGPT; + let models_manager = Arc::new(ModelsManager::new(Some(auth_mode))); + let model_family = models_manager.construct_model_family(&config.model, &config); let otel_event_manager = OtelEventManager::new( conversation_id, config.model.as_str(), - config.model_family.slug.as_str(), + model_family.slug.as_str(), None, Some("test@test.com".to_string()), - Some(AuthMode::ChatGPT), + Some(auth_mode), false, "test".to_string(), ); @@ -74,6 +79,7 @@ async fn responses_stream_includes_subagent_header_on_review() { let client = ModelClient::new( Arc::clone(&config), None, + model_family, otel_event_manager, provider, effort, @@ -147,14 +153,17 @@ async fn responses_stream_includes_subagent_header_on_other() { let config = Arc::new(config); let conversation_id = ConversationId::new(); + let auth_mode = AuthMode::ChatGPT; + let models_manager = Arc::new(ModelsManager::new(Some(auth_mode))); + let model_family = models_manager.construct_model_family(&config.model, &config); let otel_event_manager = OtelEventManager::new( conversation_id, config.model.as_str(), - config.model_family.slug.as_str(), + model_family.slug.as_str(), None, Some("test@test.com".to_string()), - Some(AuthMode::ChatGPT), + Some(auth_mode), false, "test".to_string(), ); @@ -162,6 +171,7 @@ async fn responses_stream_includes_subagent_header_on_other() { let client = ModelClient::new( Arc::clone(&config), None, + model_family, otel_event_manager, provider, effort, @@ -194,3 +204,109 @@ async fn responses_stream_includes_subagent_header_on_other() { Some("my-task") ); } + +#[tokio::test] +async fn responses_respects_model_family_overrides_from_config() { + core_test_support::skip_if_no_network!(); + + let server = responses::start_mock_server().await; + let response_body = responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_completed("resp-1"), + ]); + + let request_recorder = responses::mount_sse_once(&server, response_body).await; + + let provider = ModelProviderInfo { + name: "mock".into(), + base_url: Some(format!("{}/v1", server.uri())), + env_key: None, + env_key_instructions: None, + experimental_bearer_token: None, + wire_api: WireApi::Responses, + query_params: None, + http_headers: None, + env_http_headers: None, + request_max_retries: Some(0), + stream_max_retries: Some(0), + stream_idle_timeout_ms: Some(5_000), + requires_openai_auth: false, + }; + + let codex_home = TempDir::new().expect("failed to create TempDir"); + let mut config = load_default_config_for_test(&codex_home); + config.model = "gpt-3.5-turbo".to_string(); + config.model_provider_id = provider.name.clone(); + config.model_provider = provider.clone(); + config.model_supports_reasoning_summaries = Some(true); + config.model_reasoning_summary_format = Some(ReasoningSummaryFormat::Experimental); + config.model_reasoning_summary = ReasoningSummary::Detailed; + let effort = config.model_reasoning_effort; + let summary = config.model_reasoning_summary; + let config = Arc::new(config); + + let conversation_id = ConversationId::new(); + let auth_mode = AuthMode::ChatGPT; + let models_manager = Arc::new(ModelsManager::new(Some(auth_mode))); + let model_family = models_manager.construct_model_family(&config.model, &config); + let otel_event_manager = OtelEventManager::new( + conversation_id, + config.model.as_str(), + model_family.slug.as_str(), + None, + Some("test@test.com".to_string()), + Some(auth_mode), + false, + "test".to_string(), + ); + + let client = ModelClient::new( + Arc::clone(&config), + None, + model_family, + otel_event_manager, + provider, + effort, + summary, + conversation_id, + SessionSource::SubAgent(codex_protocol::protocol::SubAgentSource::Other( + "override-check".to_string(), + )), + ); + + let mut prompt = Prompt::default(); + prompt.input = vec![ResponseItem::Message { + id: None, + role: "user".into(), + content: vec![ContentItem::InputText { + text: "hello".into(), + }], + }]; + + let mut stream = client.stream(&prompt).await.expect("stream failed"); + while let Some(event) = stream.next().await { + if matches!(event, Ok(ResponseEvent::Completed { .. })) { + break; + } + } + + let request = request_recorder.single_request(); + let body = request.body_json(); + let reasoning = body + .get("reasoning") + .and_then(|value| value.as_object()) + .cloned(); + + assert!( + reasoning.is_some(), + "reasoning should be present when config enables summaries" + ); + + assert_eq!( + reasoning + .as_ref() + .and_then(|value| value.get("summary")) + .and_then(|value| value.as_str()), + Some("detailed") + ); +} diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index e5d3d7e61..1170d13a9 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -16,7 +16,7 @@ use codex_core::auth::AuthCredentialsStoreMode; use codex_core::built_in_model_providers; use codex_core::error::CodexErr; use codex_core::features::Feature; -use codex_core::openai_models::model_family::find_family_for_model; +use codex_core::openai_models::models_manager::ModelsManager; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_core::protocol::SessionSource; @@ -1017,11 +1017,13 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { let config = Arc::new(config); let conversation_id = ConversationId::new(); - + let auth_mode = AuthMode::ChatGPT; + let models_manager = Arc::new(ModelsManager::new(Some(auth_mode))); + let model_family = models_manager.construct_model_family(&config.model, &config); let otel_event_manager = OtelEventManager::new( conversation_id, config.model.as_str(), - config.model_family.slug.as_str(), + model_family.slug.as_str(), None, Some("test@test.com".to_string()), Some(AuthMode::ChatGPT), @@ -1032,6 +1034,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { let client = ModelClient::new( Arc::clone(&config), None, + model_family, otel_event_manager, provider, effort, @@ -1378,7 +1381,6 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res let TestCodex { codex, .. } = test_codex() .with_config(|config| { config.model = "gpt-5.1".to_string(); - config.model_family = find_family_for_model("gpt-5.1"); config.model_context_window = Some(272_000); }) .build(&server) diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 219f29e2f..95f2d35cd 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -1,7 +1,6 @@ #![allow(clippy::unwrap_used)] use codex_core::features::Feature; -use codex_core::openai_models::model_family::find_family_for_model; use codex_core::protocol::AskForApproval; use codex_core::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG; use codex_core::protocol::EventMsg; @@ -73,7 +72,6 @@ async fn codex_mini_latest_tools() -> anyhow::Result<()> { config.user_instructions = Some("be consistent and helpful".to_string()); config.features.disable(Feature::ApplyPatchFreeform); config.model = "codex-mini-latest".to_string(); - config.model_family = find_family_for_model("codex-mini-latest") }) .build(&server) .await?; @@ -125,13 +123,22 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { let req1 = mount_sse_once(&server, sse_completed("resp-1")).await; let req2 = mount_sse_once(&server, sse_completed("resp-2")).await; - let TestCodex { codex, config, .. } = test_codex() + let TestCodex { + codex, + config, + conversation_manager, + .. + } = test_codex() .with_config(|config| { config.user_instructions = Some("be consistent and helpful".to_string()); }) .build(&server) .await?; - let base_instructions = config.model_family.base_instructions.clone(); + let base_instructions = conversation_manager + .get_models_manager() + .construct_model_family(&config.model, &config) + .base_instructions + .clone(); codex .submit(Op::UserInput { diff --git a/codex-rs/core/tests/suite/shell_serialization.rs b/codex-rs/core/tests/suite/shell_serialization.rs index 5dbdda4fc..6969b6533 100644 --- a/codex-rs/core/tests/suite/shell_serialization.rs +++ b/codex-rs/core/tests/suite/shell_serialization.rs @@ -2,9 +2,6 @@ #![allow(clippy::expect_used)] use anyhow::Result; -use codex_core::config::Config; -use codex_core::features::Feature; -use codex_core::openai_models::model_family::find_family_for_model; use codex_core::protocol::SandboxPolicy; use core_test_support::assert_regex_match; use core_test_support::responses::ev_assistant_message; @@ -18,6 +15,7 @@ use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::ApplyPatchModelOutput; use core_test_support::test_codex::ShellModelOutput; +use core_test_support::test_codex::TestCodexBuilder; use core_test_support::test_codex::test_codex; use pretty_assertions::assert_eq; use regex_lite::Regex; @@ -41,19 +39,6 @@ const FIXTURE_JSON: &str = r#"{ } "#; -fn configure_shell_command_model(output_type: ShellModelOutput, config: &mut Config) { - if !matches!(output_type, ShellModelOutput::ShellCommand) { - return; - } - - let shell_command_family = find_family_for_model("test-gpt-5-codex"); - if config.model_family.shell_type == shell_command_family.shell_type { - return; - } - config.model = shell_command_family.slug.clone(); - config.model_family = shell_command_family; -} - fn shell_responses( call_id: &str, command: Vec<&str>, @@ -113,6 +98,24 @@ fn shell_responses( } } +fn configure_shell_model( + builder: TestCodexBuilder, + output_type: ShellModelOutput, + include_apply_patch_tool: bool, +) -> TestCodexBuilder { + let builder = match (output_type, include_apply_patch_tool) { + (ShellModelOutput::ShellCommand, _) => builder.with_model("test-gpt-5-codex"), + (ShellModelOutput::LocalShell, true) => builder.with_model("gpt-5.1-codex"), + (ShellModelOutput::Shell, true) => builder.with_model("gpt-5.1-codex"), + (ShellModelOutput::LocalShell, false) => builder.with_model("codex-mini-latest"), + (ShellModelOutput::Shell, false) => builder.with_model("gpt-5"), + }; + + builder.with_config(move |config| { + config.include_apply_patch_tool = include_apply_patch_tool; + }) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[test_case(ShellModelOutput::Shell)] #[test_case(ShellModelOutput::LocalShell)] @@ -122,10 +125,7 @@ async fn shell_output_stays_json_without_freeform_apply_patch( skip_if_no_network!(Ok(())); let server = start_mock_server().await; - let mut builder = test_codex().with_model("gpt-5").with_config(move |config| { - config.features.disable(Feature::ApplyPatchFreeform); - configure_shell_command_model(output_type, config); - }); + let mut builder = configure_shell_model(test_codex(), output_type, false); let test = builder.build(&server).await?; let call_id = "shell-json"; @@ -177,10 +177,7 @@ async fn shell_output_is_structured_with_freeform_apply_patch( skip_if_no_network!(Ok(())); let server = start_mock_server().await; - let mut builder = test_codex().with_config(move |config| { - config.features.enable(Feature::ApplyPatchFreeform); - configure_shell_command_model(output_type, config); - }); + let mut builder = configure_shell_model(test_codex(), output_type, true); let test = builder.build(&server).await?; let call_id = "shell-structured"; @@ -225,10 +222,7 @@ async fn shell_output_preserves_fixture_json_without_serialization( skip_if_no_network!(Ok(())); let server = start_mock_server().await; - let mut builder = test_codex().with_model("gpt-5").with_config(move |config| { - config.features.disable(Feature::ApplyPatchFreeform); - configure_shell_command_model(output_type, config); - }); + let mut builder = configure_shell_model(test_codex(), output_type, false); let test = builder.build(&server).await?; let fixture_path = test.cwd.path().join("fixture.json"); @@ -292,10 +286,7 @@ async fn shell_output_structures_fixture_with_serialization( skip_if_no_network!(Ok(())); let server = start_mock_server().await; - let mut builder = test_codex().with_config(move |config| { - config.features.enable(Feature::ApplyPatchFreeform); - configure_shell_command_model(output_type, config); - }); + let mut builder = configure_shell_model(test_codex(), output_type, true); let test = builder.build(&server).await?; let fixture_path = test.cwd.path().join("fixture.json"); @@ -354,10 +345,7 @@ async fn shell_output_for_freeform_tool_records_duration( skip_if_no_network!(Ok(())); let server = start_mock_server().await; - let mut builder = test_codex().with_config(move |config| { - config.include_apply_patch_tool = true; - configure_shell_command_model(output_type, config); - }); + let mut builder = configure_shell_model(test_codex(), output_type, true); let test = builder.build(&server).await?; let call_id = "shell-structured"; @@ -407,11 +395,9 @@ async fn shell_output_reserializes_truncated_content(output_type: ShellModelOutp skip_if_no_network!(Ok(())); let server = start_mock_server().await; - let mut builder = test_codex() - .with_model("gpt-5.1-codex") - .with_config(move |config| { + let mut builder = + configure_shell_model(test_codex(), output_type, true).with_config(move |config| { config.tool_output_token_limit = Some(200); - configure_shell_command_model(output_type, config); }); let test = builder.build(&server).await?; @@ -712,7 +698,6 @@ async fn shell_output_is_structured_for_nonzero_exit(output_type: ShellModelOutp .with_model("gpt-5.1-codex") .with_config(move |config| { config.include_apply_patch_tool = true; - configure_shell_command_model(output_type, config); }); let test = builder.build(&server).await?; @@ -748,7 +733,7 @@ async fn shell_command_output_is_freeform() -> Result<()> { let server = start_mock_server().await; let mut builder = test_codex().with_config(move |config| { - configure_shell_command_model(ShellModelOutput::ShellCommand, config); + config.include_apply_patch_tool = true; }); let test = builder.build(&server).await?; diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 6120c7978..28535e536 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -26,7 +26,6 @@ use codex_core::ConversationManager; use codex_core::config::Config; use codex_core::config::edit::ConfigEditsBuilder; use codex_core::features::Feature; -use codex_core::openai_models::model_family::find_family_for_model; use codex_core::openai_models::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG; use codex_core::openai_models::model_presets::HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG; use codex_core::openai_models::models_manager::ModelsManager; @@ -162,7 +161,6 @@ async fn handle_model_migration_prompt_if_needed( migration_config: migration_config_key.to_string(), }); config.model = target_model.to_string(); - config.model_family = find_family_for_model(&target_model); let mapped_effort = if let Some(reasoning_effort_mapping) = reasoning_effort_mapping && let Some(reasoning_effort) = config.model_reasoning_effort @@ -680,9 +678,7 @@ impl App { } AppEvent::UpdateModel(model) => { self.chat_widget.set_model(&model); - self.config.model = model.clone(); - let family = find_family_for_model(&model); - self.config.model_family = family; + self.config.model = model; } AppEvent::OpenReasoningPopup { model } => { self.chat_widget.open_reasoning_popup(model); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 8f9db3b9d..8e07ce9be 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -465,10 +465,13 @@ impl ChatWidget { fn on_agent_reasoning_final(&mut self) { // At the end of a reasoning block, record transcript-only content. self.full_reasoning_buffer.push_str(&self.reasoning_buffer); + let model_family = self + .models_manager + .construct_model_family(&self.config.model, &self.config); if !self.full_reasoning_buffer.is_empty() { let cell = history_cell::new_reasoning_summary_block( self.full_reasoning_buffer.clone(), - &self.config, + &model_family, ); self.add_boxed_history(cell); } diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index bdcaca7be..1b8755efd 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -27,6 +27,7 @@ use codex_common::format_env_display::format_env_display; use codex_core::config::Config; use codex_core::config::types::McpServerTransportConfig; use codex_core::config::types::ReasoningSummaryFormat; +use codex_core::openai_models::model_family::ModelFamily; use codex_core::protocol::FileChange; use codex_core::protocol::McpAuthStatus; use codex_core::protocol::McpInvocation; @@ -1420,9 +1421,9 @@ pub(crate) fn new_view_image_tool_call(path: PathBuf, cwd: &Path) -> PlainHistor pub(crate) fn new_reasoning_summary_block( full_reasoning_buffer: String, - config: &Config, + model_family: &ModelFamily, ) -> Box { - if config.model_family.reasoning_summary_format == ReasoningSummaryFormat::Experimental { + if model_family.reasoning_summary_format == ReasoningSummaryFormat::Experimental { // Experimental format is following: // ** header ** // @@ -1517,12 +1518,15 @@ mod tests { use codex_core::config::ConfigToml; use codex_core::config::types::McpServerConfig; use codex_core::config::types::McpServerTransportConfig; + use codex_core::openai_models::models_manager::ModelsManager; use codex_core::protocol::McpAuthStatus; + use codex_login::AuthMode; use codex_protocol::parse_command::ParsedCommand; use dirs::home_dir; use pretty_assertions::assert_eq; use serde_json::json; use std::collections::HashMap; + use std::sync::Arc; use codex_core::protocol::ExecCommandSource; use mcp_types::CallToolResult; @@ -2320,12 +2324,12 @@ mod tests { } #[test] fn reasoning_summary_block() { - let mut config = test_config(); - config.model_family.reasoning_summary_format = ReasoningSummaryFormat::Experimental; - + let config = test_config(); + let models_manager = Arc::new(ModelsManager::new(Some(AuthMode::ApiKey))); + let model_family = models_manager.construct_model_family(&config.model, &config); let cell = new_reasoning_summary_block( "**High level reasoning**\n\nDetailed reasoning goes here.".to_string(), - &config, + &model_family, ); let rendered_display = render_lines(&cell.display_lines(80)); @@ -2337,24 +2341,47 @@ mod tests { #[test] fn reasoning_summary_block_returns_reasoning_cell_when_feature_disabled() { - let mut config = test_config(); - config.model_family.reasoning_summary_format = ReasoningSummaryFormat::Experimental; - + let config = test_config(); + let models_manager = Arc::new(ModelsManager::new(Some(AuthMode::ApiKey))); + let model_family = models_manager.construct_model_family(&config.model, &config); let cell = - new_reasoning_summary_block("Detailed reasoning goes here.".to_string(), &config); + new_reasoning_summary_block("Detailed reasoning goes here.".to_string(), &model_family); let rendered = render_transcript(cell.as_ref()); assert_eq!(rendered, vec!["• Detailed reasoning goes here."]); } #[test] - fn reasoning_summary_block_falls_back_when_header_is_missing() { + fn reasoning_summary_block_respects_config_overrides() { let mut config = test_config(); - config.model_family.reasoning_summary_format = ReasoningSummaryFormat::Experimental; + config.model = "gpt-3.5-turbo".to_string(); + config.model_supports_reasoning_summaries = Some(true); + config.model_reasoning_summary_format = Some(ReasoningSummaryFormat::Experimental); + let models_manager = Arc::new(ModelsManager::new(Some(AuthMode::ApiKey))); + + let model_family = models_manager.construct_model_family(&config.model, &config); + assert_eq!( + model_family.reasoning_summary_format, + ReasoningSummaryFormat::Experimental + ); + let cell = new_reasoning_summary_block( + "**High level reasoning**\n\nDetailed reasoning goes here.".to_string(), + &model_family, + ); + + let rendered_display = render_lines(&cell.display_lines(80)); + assert_eq!(rendered_display, vec!["• Detailed reasoning goes here."]); + } + + #[test] + fn reasoning_summary_block_falls_back_when_header_is_missing() { + let config = test_config(); + let models_manager = Arc::new(ModelsManager::new(Some(AuthMode::ApiKey))); + let model_family = models_manager.construct_model_family(&config.model, &config); let cell = new_reasoning_summary_block( "**High level reasoning without closing".to_string(), - &config, + &model_family, ); let rendered = render_transcript(cell.as_ref()); @@ -2363,12 +2390,12 @@ mod tests { #[test] fn reasoning_summary_block_falls_back_when_summary_is_missing() { - let mut config = test_config(); - config.model_family.reasoning_summary_format = ReasoningSummaryFormat::Experimental; - + let config = test_config(); + let models_manager = Arc::new(ModelsManager::new(Some(AuthMode::ApiKey))); + let model_family = models_manager.construct_model_family(&config.model, &config); let cell = new_reasoning_summary_block( "**High level reasoning without closing**".to_string(), - &config, + &model_family, ); let rendered = render_transcript(cell.as_ref()); @@ -2376,7 +2403,7 @@ mod tests { let cell = new_reasoning_summary_block( "**High level reasoning without closing**\n\n ".to_string(), - &config, + &model_family, ); let rendered = render_transcript(cell.as_ref()); @@ -2385,12 +2412,12 @@ mod tests { #[test] fn reasoning_summary_block_splits_header_and_summary_when_present() { - let mut config = test_config(); - config.model_family.reasoning_summary_format = ReasoningSummaryFormat::Experimental; - + let config = test_config(); + let models_manager = Arc::new(ModelsManager::new(Some(AuthMode::ApiKey))); + let model_family = models_manager.construct_model_family(&config.model, &config); let cell = new_reasoning_summary_block( "**High level plan**\n\nWe should fix the bug next.".to_string(), - &config, + &model_family, ); let rendered_display = render_lines(&cell.display_lines(80));