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
This commit is contained in:
Ahmed Ibrahim
2025-12-04 11:57:58 -08:00
committed by GitHub
Unverified
parent ce0b38c056
commit 9b2055586d
18 changed files with 339 additions and 158 deletions
+6 -7
View File
@@ -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(),
+20 -19
View File
@@ -57,6 +57,7 @@ use crate::tools::spec::create_tools_json_for_responses_api;
pub struct ModelClient {
config: Arc<Config>,
auth_manager: Option<Arc<AuthManager>>,
model_family: ModelFamily,
otel_event_manager: OtelEventManager,
provider: ModelProviderInfo,
conversation_id: ConversationId,
@@ -70,6 +71,7 @@ impl ModelClient {
pub fn new(
config: Arc<Config>,
auth_manager: Option<Arc<AuthManager>>,
model_family: ModelFamily,
otel_event_manager: OtelEventManager,
provider: ModelProviderInfo,
effort: Option<ReasoningEffortConfig>,
@@ -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<i64> {
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<i64> {
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<Value> = 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,
+36 -20
View File
@@ -398,7 +398,7 @@ pub(crate) struct SessionSettingsUpdate {
impl Session {
fn make_turn_context(
auth_manager: Option<Arc<AuthManager>>,
models_manager: &ModelsManager,
models_manager: Arc<ModelsManager>,
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 perturn 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,
+18 -17
View File
@@ -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<i64>,
@@ -195,6 +191,7 @@ pub struct Config {
/// Additional filenames to try when looking for project-level docs.
pub project_doc_fallback_filenames: Vec<String>,
// 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<usize>,
@@ -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<bool>,
/// Optional override to force reasoning summary format for the configured model.
pub model_reasoning_summary_format: Option<ReasoningSummaryFormat>,
/// Optional verbosity control for GPT-5 models (Responses API `text.verbosity`).
pub model_verbosity: Option<Verbosity>,
@@ -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,
@@ -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 )* $(,)?
@@ -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<Vec<ModelPreset>>,
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)
}
}
+6 -2
View File
@@ -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<AuthManager>,
parent_otel: &OtelEventManager,
conversation_id: ConversationId,
models_manager: Arc<ModelsManager>,
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),
+2 -2
View File
@@ -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))
@@ -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<ResponseItem>) -> 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<ResponseItem>) -> Value {
let client = ModelClient::new(
Arc::clone(&config),
None,
model_family,
otel_event_manager,
provider,
effort,
+7 -3
View File
@@ -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<ResponseEvent> {
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<ResponseEvent> {
let client = ModelClient::new(
Arc::clone(&config),
None,
model_family,
otel_event_manager,
provider,
effort,
+2
View File
@@ -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<CodexConversation>,
pub session_configured: SessionConfiguredEvent,
pub config: Config,
pub conversation_manager: Arc<ConversationManager>,
}
impl TestCodex {
+121 -5
View File
@@ -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")
);
}
+6 -4
View File
@@ -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)
+11 -4
View File
@@ -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 {
@@ -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?;
+1 -5
View File
@@ -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);
+4 -1
View File
@@ -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);
}
+49 -22
View File
@@ -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<dyn HistoryCell> {
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));