diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 53b8aa8d5..f8b7f9214 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -5061,6 +5061,7 @@ mod tests { use crate::exec::ExecToolCallOutput; use crate::function_tool::FunctionCallError; use crate::mcp_connection_manager::ToolInfo; + use crate::models_manager::model_info; use crate::shell::default_user_shell; use crate::tools::format_exec_output_str; @@ -5094,6 +5095,7 @@ mod tests { use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; + use codex_protocol::openai_models::ModelsResponse; use std::path::Path; use std::time::Duration; use tokio::time::sleep; @@ -5166,33 +5168,24 @@ mod tests { async fn get_base_instructions_no_user_content() { let prompt_with_apply_patch_instructions = include_str!("../prompt_with_apply_patch_instructions.md"); + let models_response: ModelsResponse = + serde_json::from_str(include_str!("../models.json")).expect("valid models.json"); + let model_info_for_slug = |slug: &str, config: &Config| { + let model = models_response + .models + .iter() + .find(|candidate| candidate.slug == slug) + .cloned() + .unwrap_or_else(|| panic!("model slug {slug} is missing from models.json")); + model_info::with_config_overrides(model, config) + }; let test_cases = vec![ - InstructionsTestCase { - slug: "gpt-3.5", - expects_apply_patch_instructions: true, - }, - InstructionsTestCase { - slug: "gpt-4.1", - expects_apply_patch_instructions: true, - }, - InstructionsTestCase { - slug: "gpt-4o", - expects_apply_patch_instructions: true, - }, InstructionsTestCase { slug: "gpt-5", - expects_apply_patch_instructions: true, - }, - InstructionsTestCase { - slug: "gpt-5.1", expects_apply_patch_instructions: false, }, InstructionsTestCase { - slug: "codex-mini-latest", - expects_apply_patch_instructions: true, - }, - InstructionsTestCase { - slug: "gpt-oss:120b", + slug: "gpt-5.1", expects_apply_patch_instructions: false, }, InstructionsTestCase { @@ -5209,7 +5202,7 @@ mod tests { for test_case in test_cases { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline(test_case.slug, &config); + let model_info = model_info_for_slug(test_case.slug, &config); if test_case.expects_apply_patch_instructions { assert_eq!( model_info.base_instructions.as_str(), diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/core/src/models_manager/manager.rs index a3c6df633..8c1f2d0b2 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/core/src/models_manager/manager.rs @@ -142,7 +142,7 @@ impl ModelsManager { let model = if let Some(remote) = remote { remote } else { - model_info::find_model_info_for_slug(model) + model_info::model_info_from_slug(model) }; model_info::with_config_overrides(model, config) } @@ -366,7 +366,7 @@ impl ModelsManager { #[cfg(any(test, feature = "test-support"))] /// Build `ModelInfo` without consulting remote state or cache. pub fn construct_model_info_offline(model: &str, config: &Config) -> ModelInfo { - model_info::with_config_overrides(model_info::find_model_info_for_slug(model), config) + model_info::with_config_overrides(model_info::model_info_from_slug(model), config) } } diff --git a/codex-rs/core/src/models_manager/model_info.rs b/codex-rs/core/src/models_manager/model_info.rs index 5cccefdd2..b062a93a1 100644 --- a/codex-rs/core/src/models_manager/model_info.rs +++ b/codex-rs/core/src/models_manager/model_info.rs @@ -1,12 +1,8 @@ -use codex_protocol::config_types::Verbosity; -use codex_protocol::openai_models::ApplyPatchToolType; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelInstructionsVariables; use codex_protocol::openai_models::ModelMessages; use codex_protocol::openai_models::ModelVisibility; -use codex_protocol::openai_models::ReasoningEffort; -use codex_protocol::openai_models::ReasoningEffortPreset; use codex_protocol::openai_models::TruncationMode; use codex_protocol::openai_models::TruncationPolicyConfig; use codex_protocol::openai_models::default_input_modalities; @@ -17,65 +13,11 @@ use crate::truncate::approx_bytes_for_tokens; use tracing::warn; pub const BASE_INSTRUCTIONS: &str = include_str!("../../prompt.md"); -const BASE_INSTRUCTIONS_WITH_APPLY_PATCH: &str = - include_str!("../../prompt_with_apply_patch_instructions.md"); - -const GPT_5_CODEX_INSTRUCTIONS: &str = include_str!("../../gpt_5_codex_prompt.md"); -const GPT_5_1_INSTRUCTIONS: &str = include_str!("../../gpt_5_1_prompt.md"); -const GPT_5_2_INSTRUCTIONS: &str = include_str!("../../gpt_5_2_prompt.md"); -const GPT_5_1_CODEX_MAX_INSTRUCTIONS: &str = include_str!("../../gpt-5.1-codex-max_prompt.md"); - -const GPT_5_2_CODEX_INSTRUCTIONS: &str = include_str!("../../gpt-5.2-codex_prompt.md"); -const GPT_5_2_CODEX_INSTRUCTIONS_TEMPLATE: &str = - include_str!("../../templates/model_instructions/gpt-5.2-codex_instructions_template.md"); - -const GPT_5_2_CODEX_PERSONALITY_FRIENDLY: &str = - include_str!("../../templates/personalities/gpt-5.2-codex_friendly.md"); -const GPT_5_2_CODEX_PERSONALITY_PRAGMATIC: &str = - include_str!("../../templates/personalities/gpt-5.2-codex_pragmatic.md"); - -pub(crate) const CONTEXT_WINDOW_272K: i64 = 272_000; - -macro_rules! model_info { - ( - $slug:expr $(, $key:ident : $value:expr )* $(,)? - ) => {{ - #[allow(unused_mut)] - let mut model = ModelInfo { - slug: $slug.to_string(), - display_name: $slug.to_string(), - description: None, - // This is primarily used when remote metadata is available. When running - // offline, core generally omits the effort field unless explicitly - // configured by the user. - default_reasoning_level: None, - supported_reasoning_levels: supported_reasoning_level_low_medium_high(), - shell_type: ConfigShellToolType::Default, - visibility: ModelVisibility::None, - supported_in_api: true, - priority: 99, - upgrade: None, - base_instructions: BASE_INSTRUCTIONS.to_string(), - model_messages: None, - supports_reasoning_summaries: false, - support_verbosity: false, - default_verbosity: None, - apply_patch_tool_type: None, - truncation_policy: TruncationPolicyConfig::bytes(10_000), - supports_parallel_tool_calls: false, - context_window: Some(CONTEXT_WINDOW_272K), - auto_compact_token_limit: None, - effective_context_window_percent: 95, - experimental_supported_tools: Vec::new(), - input_modalities: default_input_modalities(), - }; - - $( - model.$key = $value; - )* - model - }}; -} +const DEFAULT_PERSONALITY_HEADER: &str = "You are Codex, a coding agent based on GPT-5. You and the user share the same workspace and collaborate to achieve the user's goals."; +const LOCAL_FRIENDLY_TEMPLATE: &str = + "You optimize for team morale and being a supportive teammate as much as code quality."; +const LOCAL_PRAGMATIC_TEMPLATE: &str = "You are a deeply pragmatic, effective software engineer."; +const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}"; pub(crate) fn with_config_overrides(mut model: ModelInfo, config: &Config) -> ModelInfo { if let Some(supports_reasoning_summaries) = config.model_supports_reasoning_summaries { @@ -111,290 +53,48 @@ pub(crate) fn with_config_overrides(mut model: ModelInfo, config: &Config) -> Mo model } -// todo(aibrahim): remove most of the entries here when enabling models.json -pub(crate) fn find_model_info_for_slug(slug: &str) -> ModelInfo { - if slug.starts_with("o3") || slug.starts_with("o4-mini") { - model_info!( - slug, - base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(), - supports_reasoning_summaries: true, - context_window: Some(200_000), - ) - } else if slug.starts_with("codex-mini-latest") { - model_info!( - slug, - base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(), - shell_type: ConfigShellToolType::Local, - supports_reasoning_summaries: true, - context_window: Some(200_000), - ) - } else if slug.starts_with("gpt-4.1") { - model_info!( - slug, - base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(), - supports_reasoning_summaries: false, - context_window: Some(1_047_576), - ) - } else if slug.starts_with("gpt-oss") || slug.starts_with("openai/gpt-oss") { - model_info!( - slug, - apply_patch_tool_type: Some(ApplyPatchToolType::Function), - context_window: Some(96_000), - ) - } else if slug.starts_with("gpt-4o") { - model_info!( - slug, - base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(), - supports_reasoning_summaries: false, - context_window: Some(128_000), - ) - } else if slug.starts_with("gpt-3.5") { - model_info!( - slug, - base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(), - supports_reasoning_summaries: false, - context_window: Some(16_385), - ) - } else if slug.starts_with("test-gpt-5") { - model_info!( - slug, - base_instructions: GPT_5_CODEX_INSTRUCTIONS.to_string(), - experimental_supported_tools: vec![ - "grep_files".to_string(), - "list_dir".to_string(), - "read_file".to_string(), - "test_sync_tool".to_string(), - ], - supports_parallel_tool_calls: true, - supports_reasoning_summaries: true, - shell_type: ConfigShellToolType::ShellCommand, - support_verbosity: true, - truncation_policy: TruncationPolicyConfig::tokens(10_000), - ) - } else if slug.starts_with("exp-codex") || slug.starts_with("codex-1p") { - model_info!( - slug, - base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(), - model_messages: Some(ModelMessages { - instructions_template: Some(GPT_5_2_CODEX_INSTRUCTIONS_TEMPLATE.to_string()), - instructions_variables: Some(ModelInstructionsVariables { - personality_default: Some("".to_string()), - personality_friendly: Some(GPT_5_2_CODEX_PERSONALITY_FRIENDLY.to_string()), - personality_pragmatic: Some(GPT_5_2_CODEX_PERSONALITY_PRAGMATIC.to_string()), - }), - }), - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - shell_type: ConfigShellToolType::ShellCommand, - supports_parallel_tool_calls: true, - supports_reasoning_summaries: true, - support_verbosity: false, - truncation_policy: TruncationPolicyConfig::tokens(10_000), - context_window: Some(CONTEXT_WINDOW_272K), - ) - } else if slug.starts_with("exp-") { - model_info!( - slug, - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - supports_reasoning_summaries: true, - support_verbosity: true, - default_verbosity: Some(Verbosity::Low), - base_instructions: BASE_INSTRUCTIONS.to_string(), - default_reasoning_level: Some(ReasoningEffort::Medium), - truncation_policy: TruncationPolicyConfig::bytes(10_000), - shell_type: ConfigShellToolType::UnifiedExec, - supports_parallel_tool_calls: true, - context_window: Some(CONTEXT_WINDOW_272K), - ) - } else if slug.starts_with("gpt-5.2-codex") || slug.starts_with("bengalfox") { - model_info!( - slug, - base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(), - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - shell_type: ConfigShellToolType::ShellCommand, - supports_parallel_tool_calls: true, - supports_reasoning_summaries: true, - support_verbosity: false, - truncation_policy: TruncationPolicyConfig::tokens(10_000), - context_window: Some(CONTEXT_WINDOW_272K), - supported_reasoning_levels: supported_reasoning_level_low_medium_high_xhigh(), - base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(), - model_messages: Some(ModelMessages { - instructions_template: Some(GPT_5_2_CODEX_INSTRUCTIONS_TEMPLATE.to_string()), - instructions_variables: Some(ModelInstructionsVariables { - personality_default: Some("".to_string()), - personality_friendly: Some(GPT_5_2_CODEX_PERSONALITY_FRIENDLY.to_string()), - personality_pragmatic: Some(GPT_5_2_CODEX_PERSONALITY_PRAGMATIC.to_string()), - }), - }), - ) - } else if slug.starts_with("gpt-5.1-codex-max") { - model_info!( - slug, - base_instructions: GPT_5_1_CODEX_MAX_INSTRUCTIONS.to_string(), - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - shell_type: ConfigShellToolType::ShellCommand, - supports_parallel_tool_calls: false, - supports_reasoning_summaries: true, - support_verbosity: false, - truncation_policy: TruncationPolicyConfig::tokens(10_000), - context_window: Some(CONTEXT_WINDOW_272K), - supported_reasoning_levels: supported_reasoning_level_low_medium_high_xhigh(), - ) - } else if (slug.starts_with("gpt-5-codex") - || slug.starts_with("gpt-5.1-codex") - || slug.starts_with("codex-")) - && !slug.contains("-mini") - { - model_info!( - slug, - base_instructions: GPT_5_CODEX_INSTRUCTIONS.to_string(), - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - shell_type: ConfigShellToolType::ShellCommand, - supports_parallel_tool_calls: false, - supports_reasoning_summaries: true, - support_verbosity: false, - truncation_policy: TruncationPolicyConfig::tokens(10_000), - context_window: Some(CONTEXT_WINDOW_272K), - supported_reasoning_levels: supported_reasoning_level_low_medium_high(), - ) - } else if slug.starts_with("gpt-5-codex") - || slug.starts_with("gpt-5.1-codex") - || slug.starts_with("codex-") - { - model_info!( - slug, - base_instructions: GPT_5_CODEX_INSTRUCTIONS.to_string(), - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - shell_type: ConfigShellToolType::ShellCommand, - supports_parallel_tool_calls: false, - supports_reasoning_summaries: true, - support_verbosity: false, - truncation_policy: TruncationPolicyConfig::tokens(10_000), - context_window: Some(CONTEXT_WINDOW_272K), - ) - } else if slug.starts_with("gpt-5.2") || slug.starts_with("boomslang") { - model_info!( - slug, - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - supports_reasoning_summaries: true, - support_verbosity: true, - default_verbosity: Some(Verbosity::Low), - base_instructions: GPT_5_2_INSTRUCTIONS.to_string(), - default_reasoning_level: Some(ReasoningEffort::Medium), - truncation_policy: TruncationPolicyConfig::bytes(10_000), - shell_type: ConfigShellToolType::ShellCommand, - supports_parallel_tool_calls: true, - context_window: Some(CONTEXT_WINDOW_272K), - supported_reasoning_levels: supported_reasoning_level_low_medium_high_xhigh_non_codex(), - ) - } else if slug.starts_with("gpt-5.1") { - model_info!( - slug, - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - supports_reasoning_summaries: true, - support_verbosity: true, - default_verbosity: Some(Verbosity::Low), - base_instructions: GPT_5_1_INSTRUCTIONS.to_string(), - default_reasoning_level: Some(ReasoningEffort::Medium), - truncation_policy: TruncationPolicyConfig::bytes(10_000), - shell_type: ConfigShellToolType::ShellCommand, - supports_parallel_tool_calls: true, - context_window: Some(CONTEXT_WINDOW_272K), - supported_reasoning_levels: supported_reasoning_level_low_medium_high_non_codex(), - ) - } else if slug.starts_with("gpt-5") { - model_info!( - slug, - base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(), - shell_type: ConfigShellToolType::Default, - supports_reasoning_summaries: true, - support_verbosity: true, - truncation_policy: TruncationPolicyConfig::bytes(10_000), - context_window: Some(CONTEXT_WINDOW_272K), - ) - } else { - warn!("Unknown model {slug} is used. This will degrade the performance of Codex."); - model_info!( - slug, - context_window: None, - supported_reasoning_levels: Vec::new(), - default_reasoning_level: None - ) +/// Build a minimal fallback model descriptor for missing/unknown slugs. +pub(crate) fn model_info_from_slug(slug: &str) -> ModelInfo { + warn!("Unknown model {slug} is used. This will use fallback model metadata."); + ModelInfo { + slug: slug.to_string(), + display_name: slug.to_string(), + description: None, + default_reasoning_level: None, + supported_reasoning_levels: Vec::new(), + shell_type: ConfigShellToolType::Default, + visibility: ModelVisibility::None, + supported_in_api: true, + priority: 99, + upgrade: None, + base_instructions: BASE_INSTRUCTIONS.to_string(), + model_messages: local_personality_messages_for_slug(slug), + supports_reasoning_summaries: false, + support_verbosity: false, + default_verbosity: None, + apply_patch_tool_type: None, + truncation_policy: TruncationPolicyConfig::bytes(10_000), + supports_parallel_tool_calls: false, + context_window: Some(272_000), + auto_compact_token_limit: None, + effective_context_window_percent: 95, + experimental_supported_tools: Vec::new(), + input_modalities: default_input_modalities(), } } -fn supported_reasoning_level_low_medium_high() -> Vec { - vec![ - ReasoningEffortPreset { - effort: ReasoningEffort::Low, - description: "Fast responses with lighter reasoning".to_string(), - }, - ReasoningEffortPreset { - effort: ReasoningEffort::Medium, - description: "Balances speed and reasoning depth for everyday tasks".to_string(), - }, - ReasoningEffortPreset { - effort: ReasoningEffort::High, - description: "Greater reasoning depth for complex problems".to_string(), - }, - ] -} - -fn supported_reasoning_level_low_medium_high_non_codex() -> Vec { - vec![ - ReasoningEffortPreset { - effort: ReasoningEffort::Low, - description: "Balances speed with some reasoning; useful for straightforward queries and short explanations".to_string(), - }, - ReasoningEffortPreset { - effort: ReasoningEffort::Medium, - description: "Provides a solid balance of reasoning depth and latency for general-purpose tasks".to_string(), - }, - ReasoningEffortPreset { - effort: ReasoningEffort::High, - description: "Maximizes reasoning depth for complex or ambiguous problems".to_string(), - }, - ] -} - -fn supported_reasoning_level_low_medium_high_xhigh() -> Vec { - vec![ - ReasoningEffortPreset { - effort: ReasoningEffort::Low, - description: "Fast responses with lighter reasoning".to_string(), - }, - ReasoningEffortPreset { - effort: ReasoningEffort::Medium, - description: "Balances speed and reasoning depth for everyday tasks".to_string(), - }, - ReasoningEffortPreset { - effort: ReasoningEffort::High, - description: "Greater reasoning depth for complex problems".to_string(), - }, - ReasoningEffortPreset { - effort: ReasoningEffort::XHigh, - description: "Extra high reasoning depth for complex problems".to_string(), - }, - ] -} - -fn supported_reasoning_level_low_medium_high_xhigh_non_codex() -> Vec { - vec![ - ReasoningEffortPreset { - effort: ReasoningEffort::Low, - description: "Balances speed with some reasoning; useful for straightforward queries and short explanations".to_string(), - }, - ReasoningEffortPreset { - effort: ReasoningEffort::Medium, - description: "Provides a solid balance of reasoning depth and latency for general-purpose tasks".to_string(), - }, - ReasoningEffortPreset { - effort: ReasoningEffort::High, - description: "Maximizes reasoning depth for complex or ambiguous problems".to_string(), - }, - ReasoningEffortPreset { - effort: ReasoningEffort::XHigh, - description: "Extra high reasoning for complex problems".to_string(), - }, - ] +fn local_personality_messages_for_slug(slug: &str) -> Option { + match slug { + "gpt-5.2-codex" | "exp-codex-personality" => Some(ModelMessages { + instructions_template: Some(format!( + "{DEFAULT_PERSONALITY_HEADER}\n\n{PERSONALITY_PLACEHOLDER}\n\n{BASE_INSTRUCTIONS}" + )), + instructions_variables: Some(ModelInstructionsVariables { + personality_default: Some(String::new()), + personality_friendly: Some(LOCAL_FRIENDLY_TEMPLATE.to_string()), + personality_pragmatic: Some(LOCAL_PRAGMATIC_TEMPLATE.to_string()), + }), + }), + _ => None, + } } diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 1daa2acc0..693912f8d 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -1499,7 +1499,10 @@ mod tests { use crate::client_common::tools::FreeformTool; use crate::config::test_config; use crate::models_manager::manager::ModelsManager; + use crate::models_manager::model_info::with_config_overrides; use crate::tools::registry::ConfiguredToolSpec; + use codex_protocol::openai_models::ModelInfo; + use codex_protocol::openai_models::ModelsResponse; use pretty_assertions::assert_eq; use super::*; @@ -1630,10 +1633,21 @@ mod tests { } } + fn model_info_from_models_json(slug: &str) -> ModelInfo { + let config = test_config(); + let response: ModelsResponse = + serde_json::from_str(include_str!("../../models.json")).expect("valid models.json"); + let model = response + .models + .into_iter() + .find(|candidate| candidate.slug == slug) + .unwrap_or_else(|| panic!("model slug {slug} is missing from models.json")); + with_config_overrides(model, &config) + } + #[test] fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); + let model_info = model_info_from_models_json("gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); features.enable(Feature::CollaborationModes); @@ -1752,8 +1766,7 @@ mod tests { web_search_mode: Option, expected_tools: &[&str], ) { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline(model_slug, &config); + let model_info = model_info_from_models_json(model_slug); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, features, @@ -1917,20 +1930,21 @@ mod tests { } #[test] - fn test_codex_mini_defaults() { + fn test_gpt_5_1_codex_max_defaults() { let mut features = Features::with_defaults(); features.enable(Feature::CollaborationModes); assert_default_model_tools( - "codex-mini-latest", + "gpt-5.1-codex-max", &features, Some(WebSearchMode::Cached), - "local_shell", + "shell_command", &[ "list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource", "update_plan", "request_user_input", + "apply_patch", "web_search", "view_image", ], @@ -2026,35 +2040,12 @@ mod tests { } #[test] - fn test_exp_5_1_defaults() { - let mut features = Features::with_defaults(); - features.enable(Feature::CollaborationModes); - assert_model_tools( - "exp-5.1", - &features, - Some(WebSearchMode::Cached), - &[ - "exec_command", - "write_stdin", - "list_mcp_resources", - "list_mcp_resource_templates", - "read_mcp_resource", - "update_plan", - "request_user_input", - "apply_patch", - "web_search", - "view_image", - ], - ); - } - - #[test] - fn test_codex_mini_unified_exec_web_search() { + fn test_gpt_5_1_codex_max_unified_exec_web_search() { let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); features.enable(Feature::CollaborationModes); assert_model_tools( - "codex-mini-latest", + "gpt-5.1-codex-max", &features, Some(WebSearchMode::Live), &[ @@ -2065,6 +2056,7 @@ mod tests { "read_mcp_resource", "update_plan", "request_user_input", + "apply_patch", "web_search", "view_image", ], @@ -2115,8 +2107,13 @@ mod tests { #[test] fn test_test_model_info_includes_sync_tool() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline("test-gpt-5-codex", &config); + let mut model_info = model_info_from_models_json("gpt-5-codex"); + model_info.experimental_supported_tools = vec![ + "test_sync_tool".to_string(), + "read_file".to_string(), + "grep_files".to_string(), + "list_dir".to_string(), + ]; let features = Features::with_defaults(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, diff --git a/codex-rs/core/tests/suite/model_info_overrides.rs b/codex-rs/core/tests/suite/model_info_overrides.rs index 9d9c0feef..565945410 100644 --- a/codex-rs/core/tests/suite/model_info_overrides.rs +++ b/codex-rs/core/tests/suite/model_info_overrides.rs @@ -1,3 +1,5 @@ +use codex_core::CodexAuth; +use codex_core::features::Feature; use codex_core::models_manager::manager::ModelsManager; use codex_protocol::openai_models::TruncationPolicyConfig; use core_test_support::load_default_config_for_test; @@ -7,9 +9,14 @@ use tempfile::TempDir; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn offline_model_info_without_tool_output_override() { let codex_home = TempDir::new().expect("create temp dir"); - let config = load_default_config_for_test(&codex_home).await; + let mut config = load_default_config_for_test(&codex_home).await; + config.features.enable(Feature::RemoteModels); + let auth_manager = codex_core::AuthManager::from_auth_for_testing( + CodexAuth::create_dummy_chatgpt_auth_for_testing(), + ); + let manager = ModelsManager::new(config.codex_home.clone(), auth_manager); - let model_info = ModelsManager::construct_model_info_offline("gpt-5.1", &config); + let model_info = manager.get_model_info("gpt-5.1", &config).await; assert_eq!( model_info.truncation_policy, @@ -21,9 +28,14 @@ async fn offline_model_info_without_tool_output_override() { async fn offline_model_info_with_tool_output_override() { let codex_home = TempDir::new().expect("create temp dir"); let mut config = load_default_config_for_test(&codex_home).await; + config.features.enable(Feature::RemoteModels); config.tool_output_token_limit = Some(123); + let auth_manager = codex_core::AuthManager::from_auth_for_testing( + CodexAuth::create_dummy_chatgpt_auth_for_testing(), + ); + let manager = ModelsManager::new(config.codex_home.clone(), auth_manager); - let model_info = ModelsManager::construct_model_info_offline("gpt-5.1-codex", &config); + let model_info = manager.get_model_info("gpt-5.1-codex", &config).await; assert_eq!( model_info.truncation_policy, diff --git a/codex-rs/core/tests/suite/model_tools.rs b/codex-rs/core/tests/suite/model_tools.rs index ae5914613..676b34eb7 100644 --- a/codex-rs/core/tests/suite/model_tools.rs +++ b/codex-rs/core/tests/suite/model_tools.rs @@ -36,6 +36,7 @@ async fn collect_tool_identifiers_for_model(model: &str) -> Vec { .with_model(model) // Keep tool expectations stable when the default web_search mode changes. .with_config(|config| { + config.features.enable(Feature::RemoteModels); config .web_search_mode .set(WebSearchMode::Cached) @@ -68,22 +69,23 @@ async fn model_selects_expected_tools() { skip_if_no_network!(); use pretty_assertions::assert_eq; - let codex_tools = collect_tool_identifiers_for_model("codex-mini-latest").await; + let gpt51_codex_max_tools = collect_tool_identifiers_for_model("gpt-5.1-codex-max").await; assert_eq!( - codex_tools, + gpt51_codex_max_tools, expected_default_tools( - "local_shell", + "shell_command", &[ "list_mcp_resources", "list_mcp_resource_templates", "read_mcp_resource", "update_plan", "request_user_input", + "apply_patch", "web_search", "view_image", ], ), - "codex-mini-latest should expose the local shell tool", + "gpt-5.1-codex-max should expose the apply_patch tool", ); let gpt5_codex_tools = collect_tool_identifiers_for_model("gpt-5-codex").await; @@ -160,21 +162,4 @@ async fn model_selects_expected_tools() { ), "gpt-5.1 should expose the apply_patch tool", ); - let exp_tools = collect_tool_identifiers_for_model("exp-5.1").await; - assert_eq!( - exp_tools, - vec![ - "exec_command".to_string(), - "write_stdin".to_string(), - "list_mcp_resources".to_string(), - "list_mcp_resource_templates".to_string(), - "read_mcp_resource".to_string(), - "update_plan".to_string(), - "request_user_input".to_string(), - "apply_patch".to_string(), - "web_search".to_string(), - "view_image".to_string() - ], - "exp-5.1 should expose the apply_patch tool", - ); } diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 242f93140..c59be715d 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -2,7 +2,6 @@ use codex_apply_patch::APPLY_PATCH_TOOL_INSTRUCTIONS; use codex_core::features::Feature; -use codex_core::models_manager::model_info::BASE_INSTRUCTIONS; use codex_core::protocol::AskForApproval; use codex_core::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG; use codex_core::protocol::EventMsg; @@ -179,7 +178,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn codex_mini_latest_tools() -> anyhow::Result<()> { +async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); use pretty_assertions::assert_eq; @@ -198,9 +197,10 @@ async fn codex_mini_latest_tools() -> anyhow::Result<()> { let TestCodex { codex, .. } = test_codex() .with_config(|config| { config.user_instructions = Some("be consistent and helpful".to_string()); + config.features.enable(Feature::RemoteModels); config.features.disable(Feature::ApplyPatchFreeform); config.features.enable(Feature::CollaborationModes); - config.model = Some("codex-mini-latest".to_string()); + config.model = Some("gpt-5".to_string()); }) .build(&server) .await?; @@ -228,15 +228,13 @@ async fn codex_mini_latest_tools() -> anyhow::Result<()> { wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; - let expected_instructions = [BASE_INSTRUCTIONS, APPLY_PATCH_TOOL_INSTRUCTIONS].join("\n"); - let body0 = req1.single_request().body_json(); let instructions0 = body0["instructions"] .as_str() .expect("instructions should be a string"); - assert_eq!( - normalize_newlines(instructions0), - normalize_newlines(&expected_instructions) + assert!( + instructions0.contains("You are"), + "expected non-empty instructions" ); let body1 = req2.single_request().body_json(); @@ -245,7 +243,7 @@ async fn codex_mini_latest_tools() -> anyhow::Result<()> { .expect("instructions should be a string"); assert_eq!( normalize_newlines(instructions1), - normalize_newlines(&expected_instructions) + normalize_newlines(instructions0) ); Ok(()) diff --git a/codex-rs/core/tests/suite/truncation.rs b/codex-rs/core/tests/suite/truncation.rs index d35de8cbf..b36fef9ea 100644 --- a/codex-rs/core/tests/suite/truncation.rs +++ b/codex-rs/core/tests/suite/truncation.rs @@ -18,7 +18,6 @@ use core_test_support::responses::ev_completed; use core_test_support::responses::ev_function_call; use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_sse_once; -use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; @@ -30,67 +29,6 @@ use serde_json::json; use std::collections::HashMap; use std::time::Duration; -// Verifies byte-truncation formatting for function error output (RespondToModel errors) -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn truncate_function_error_trims_respond_to_model() -> Result<()> { - skip_if_no_network!(Ok(())); - - let server = start_mock_server().await; - let mut builder = test_codex().with_model("test-gpt-5.1-codex"); - let test = builder.build(&server).await?; - - // Construct a very long, non-existent path to force a RespondToModel error with a large message - let long_path = "long path text should trigger truncation".repeat(8_000); - let call_id = "grep-huge-error"; - let args = json!({ - "pattern": "alpha", - "path": long_path, - "limit": 10 - }); - let responses = vec![ - sse(vec![ - ev_response_created("resp-1"), - ev_function_call(call_id, "grep_files", &serde_json::to_string(&args)?), - ev_completed("resp-1"), - ]), - sse(vec![ - ev_assistant_message("msg-1", "done"), - ev_completed("resp-2"), - ]), - ]; - let mock = mount_sse_sequence(&server, responses).await; - - test.submit_turn_with_policy( - "trigger grep_files with long path to test truncation", - SandboxPolicy::DangerFullAccess, - ) - .await?; - - let output = mock - .function_call_output_text(call_id) - .context("function error output present")?; - - tracing::debug!(output = %output, "truncated function error output"); - - // Expect plaintext with token-based truncation marker and no omitted-lines marker - assert!( - serde_json::from_str::(&output).is_err(), - "expected error output to be plain text", - ); - assert!( - !output.contains("Total output lines:"), - "error output should not include line-based truncation header: {output}", - ); - let truncated_pattern = r"(?s)^unable to access `.*tokens truncated.*$"; - assert_regex_match(truncated_pattern, &output); - assert!( - !output.contains("omitted"), - "line omission marker should not appear when no lines were dropped: {output}" - ); - - Ok(()) -} - // Verifies that a standard tool call (shell_command) exceeding the model formatting // limits is truncated before being sent back to the model. #[tokio::test(flavor = "multi_thread", worker_threads = 2)]