diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index 62a8f55d1..d43896e5d 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -47,6 +47,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { input_modalities: default_input_modalities(), prefer_websockets: false, used_fallback_model_metadata: false, + supports_search_tool: false, } } diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index cc471ae89..fd95f5551 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -95,6 +95,7 @@ async fn models_client_hits_models_endpoint() { input_modalities: default_input_modalities(), prefer_websockets: false, used_fallback_model_metadata: false, + supports_search_tool: false, }], }; diff --git a/codex-rs/core/src/models_manager/model_info.rs b/codex-rs/core/src/models_manager/model_info.rs index d82cb92b2..adf82b8e8 100644 --- a/codex-rs/core/src/models_manager/model_info.rs +++ b/codex-rs/core/src/models_manager/model_info.rs @@ -90,6 +90,7 @@ pub(crate) fn model_info_from_slug(slug: &str) -> ModelInfo { input_modalities: default_input_modalities(), prefer_websockets: false, used_fallback_model_metadata: true, // this is the fallback model metadata + supports_search_tool: false, } } diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 9fed64097..a7ddb398a 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -177,7 +177,7 @@ impl ToolsConfig { let include_request_user_input = !matches!(session_source, SessionSource::SubAgent(_)); let include_default_mode_request_user_input = include_request_user_input && features.enabled(Feature::DefaultModeRequestUserInput); - let include_search_tool = features.enabled(Feature::Apps); + let include_search_tool = model_info.supports_search_tool; let include_tool_suggest = include_search_tool && features.enabled(Feature::ToolSuggest); let include_original_image_detail = can_request_original_image_detail(features, model_info); let include_artifact_tools = diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index 4d4bcaad7..292902f9c 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -44,6 +44,14 @@ fn discoverable_connector(id: &str, name: &str, description: &str) -> Discoverab })) } +fn search_capable_model_info() -> ModelInfo { + let config = test_config(); + let mut model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + model_info.supports_search_tool = true; + model_info +} + #[test] fn mcp_tool_to_openai_tool_inserts_empty_properties() { let mut schema = rmcp::model::JsonObject::new(); @@ -1582,8 +1590,7 @@ fn test_build_specs_mcp_tools_sorted_by_name() { #[test] fn search_tool_description_includes_only_codex_apps_connector_names() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = search_capable_model_info(); let mut features = Features::with_defaults(); features.enable(Feature::Apps); let available_models = Vec::new(); @@ -1659,9 +1666,8 @@ fn search_tool_description_includes_only_codex_apps_connector_names() { } #[test] -fn search_tool_requires_apps_feature_flag_only() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); +fn search_tool_requires_model_capability_only() { + let model_info = search_capable_model_info(); let app_tools = Some(HashMap::from([( "mcp__codex_apps__calendar_create_event".to_string(), ToolInfo { @@ -1683,7 +1689,10 @@ fn search_tool_requires_apps_feature_flag_only() { let features = Features::with_defaults(); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_info: &model_info, + model_info: &ModelInfo { + supports_search_tool: false, + ..model_info.clone() + }, available_models: &available_models, features: &features, web_search_mode: Some(WebSearchMode::Cached), @@ -1693,8 +1702,6 @@ fn search_tool_requires_apps_feature_flag_only() { }); let (tools, _) = build_specs(&tools_config, None, app_tools.clone(), &[]).build(); assert_lacks_tool_name(&tools, TOOL_SEARCH_TOOL_NAME); - let mut features = Features::with_defaults(); - features.enable(Feature::Apps); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { model_info: &model_info, @@ -1711,8 +1718,7 @@ fn search_tool_requires_apps_feature_flag_only() { #[test] fn tool_suggest_is_not_registered_without_feature_flag() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = search_capable_model_info(); let mut features = Features::with_defaults(); features.enable(Feature::Apps); let available_models = Vec::new(); @@ -1747,8 +1753,7 @@ fn tool_suggest_is_not_registered_without_feature_flag() { #[test] fn search_tool_description_handles_no_enabled_apps() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = search_capable_model_info(); let mut features = Features::with_defaults(); features.enable(Feature::Apps); let available_models = Vec::new(); @@ -1774,8 +1779,7 @@ fn search_tool_description_handles_no_enabled_apps() { #[test] fn search_tool_registers_namespaced_app_tool_aliases() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = search_capable_model_info(); let mut features = Features::with_defaults(); features.enable(Feature::Apps); let available_models = Vec::new(); @@ -1840,8 +1844,7 @@ fn search_tool_registers_namespaced_app_tool_aliases() { #[test] fn tool_suggest_description_lists_discoverable_tools() { - let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = search_capable_model_info(); let mut features = Features::with_defaults(); features.enable(Feature::Apps); features.enable(Feature::ToolSuggest); diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 937de8d53..e4c9935d8 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -55,6 +55,7 @@ fn test_model_info( input_modalities, prefer_websockets: false, used_fallback_model_metadata: false, + supports_search_tool: false, priority: 1, upgrade: None, base_instructions: "base instructions".to_string(), @@ -675,6 +676,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< input_modalities: default_input_modalities(), prefer_websockets: false, used_fallback_model_metadata: false, + supports_search_tool: false, priority: 1, upgrade: None, base_instructions: "base instructions".to_string(), diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index e8f9cbf7f..103817ba9 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -353,5 +353,6 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo { input_modalities: default_input_modalities(), prefer_websockets: false, used_fallback_model_metadata: false, + supports_search_tool: false, } } diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 329db44f3..7caae2dbd 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -658,6 +658,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow input_modalities: default_input_modalities(), prefer_websockets: false, used_fallback_model_metadata: false, + supports_search_tool: false, }; let _models_mock = mount_models_once( @@ -773,6 +774,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - input_modalities: default_input_modalities(), prefer_websockets: false, used_fallback_model_metadata: false, + supports_search_tool: false, }; let _models_mock = mount_models_once( diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 4610bec09..272ebf464 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -291,6 +291,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { input_modalities: default_input_modalities(), prefer_websockets: false, used_fallback_model_metadata: false, + supports_search_tool: false, priority: 1, upgrade: None, base_instructions: "base instructions".to_string(), @@ -533,6 +534,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { input_modalities: default_input_modalities(), prefer_websockets: false, used_fallback_model_metadata: false, + supports_search_tool: false, priority: 1, upgrade: None, base_instructions: remote_base.to_string(), @@ -999,6 +1001,7 @@ fn test_remote_model_with_policy( input_modalities: default_input_modalities(), prefer_websockets: false, used_fallback_model_metadata: false, + supports_search_tool: false, priority, upgrade: None, base_instructions: "base instructions".to_string(), diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 3c6948354..5b7e025ec 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -421,6 +421,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re input_modalities: vec![InputModality::Text], prefer_websockets: false, used_fallback_model_metadata: false, + supports_search_tool: false, }], }, ) diff --git a/codex-rs/core/tests/suite/search_tool.rs b/codex-rs/core/tests/suite/search_tool.rs index 7b0a72b17..e7f0a60cb 100644 --- a/codex-rs/core/tests/suite/search_tool.rs +++ b/codex-rs/core/tests/suite/search_tool.rs @@ -5,6 +5,7 @@ use anyhow::Result; use codex_core::CodexAuth; use codex_core::config::Config; use codex_core::features::Feature; +use codex_protocol::openai_models::ModelsResponse; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::McpInvocation; @@ -93,6 +94,17 @@ fn configure_apps(config: &mut Config, apps_base_url: &str) { .disable(Feature::AppsMcpGateway) .expect("test config should allow feature update"); config.chatgpt_base_url = apps_base_url.to_string(); + config.model = Some("gpt-5-codex".to_string()); + + let mut model_catalog: ModelsResponse = + serde_json::from_str(include_str!("../../models.json")).expect("valid models.json"); + let model = model_catalog + .models + .iter_mut() + .find(|model| model.slug == "gpt-5-codex") + .expect("gpt-5-codex exists in bundled models.json"); + model.supports_search_tool = true; + config.model_catalog = Some(model_catalog); } fn configured_builder(apps_base_url: String) -> TestCodexBuilder { diff --git a/codex-rs/core/tests/suite/spawn_agent_description.rs b/codex-rs/core/tests/suite/spawn_agent_description.rs index 117d1d9ef..b194c8704 100644 --- a/codex-rs/core/tests/suite/spawn_agent_description.rs +++ b/codex-rs/core/tests/suite/spawn_agent_description.rs @@ -66,6 +66,7 @@ fn test_model_info( input_modalities: default_input_modalities(), prefer_websockets: false, used_fallback_model_metadata: false, + supports_search_tool: false, priority: 1, upgrade: None, base_instructions: "base instructions".to_string(), diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index 3bf8627b5..a3e341f19 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -1272,6 +1272,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an input_modalities: vec![InputModality::Text], prefer_websockets: false, used_fallback_model_metadata: false, + supports_search_tool: false, priority: 1, upgrade: None, base_instructions: "base instructions".to_string(), diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index 3d668c447..04ca8dc9d 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -282,6 +282,8 @@ pub struct ModelInfo { #[schemars(skip)] #[ts(skip)] pub used_fallback_model_metadata: bool, + #[serde(default)] + pub supports_search_tool: bool, } impl ModelInfo { @@ -538,6 +540,7 @@ mod tests { input_modalities: default_input_modalities(), prefer_websockets: false, used_fallback_model_metadata: false, + supports_search_tool: false, } } @@ -732,6 +735,7 @@ mod tests { assert_eq!(model.availability_nux, None); assert!(!model.supports_image_detail_original); assert_eq!(model.web_search_tool_type, WebSearchToolType::Text); + assert!(!model.supports_search_tool); } #[test]