From 68e2c8ed69ce994a0da02c2dcf8b20a69f43c2b6 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Tue, 2 Jun 2026 14:50:16 -0700 Subject: [PATCH] [codex] Keep hosted tools visible in code-only mode (#25890) ## Why `code_mode_only` moved ordinary runtime tools behind `exec`, but it also hid hosted Responses tools. Hosted `web_search` and `image_generation` do not have a nested `exec` runtime path, so code-only sessions lost those capabilities entirely even when their existing provider, auth, model, and configuration gates passed. ## What changed - Keep hosted Responses tools top-level in `code_mode_only` sessions after their existing gates pass. - Preserve the existing nested-tool behavior for ordinary runtimes and the direct-only behavior for multi-agent v2 tools. - Add planner coverage for `code_mode_only` with default multi-agent v2 settings, hosted live web search, and hosted image generation. ## Verification - Added focused regression coverage in `codex-rs/core/src/tools/spec_plan_tests.rs`. - Left execution to CI per repository workflow. --- codex-rs/core/src/tools/spec_plan.rs | 11 +----- codex-rs/core/src/tools/spec_plan_tests.rs | 37 ++++++++++++++++++- .../tests/suite/model_runtime_selectors.rs | 6 +++ 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index 9d5aa8f8e..48111fb57 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -210,15 +210,7 @@ fn build_model_visible_specs_and_registry( specs.push(spec_for_model_request(turn_context, exposure, spec)); } } - for spec in hosted_specs { - if !is_hidden_by_code_mode_only( - turn_context, - &ToolName::plain(spec.name()), - ToolExposure::Direct, - ) { - specs.push(spec); - } - } + specs.extend(hosted_specs); let registry = ToolRegistry::from_tools(runtimes); let model_visible_specs = merge_into_namespaces(specs) @@ -267,6 +259,7 @@ fn hosted_model_tool_specs(context: &CoreToolPlanContext<'_>) -> Vec { }) { specs.push(web_search_tool); } + // TODO: Remove hosted image generation once the standalone extension is ready. if image_generation_tool_enabled(turn_context) && !standalone_image_generation_available(turn_context, context.extension_tool_executors) { diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index 1cb36e474..02a7a21f6 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -1167,7 +1167,16 @@ async fn code_mode_only_can_expose_namespaced_multi_agent_v2_as_normal_tools() { }) .await; - assert_eq!(plan.visible_names, vec!["exec", "wait", "agents"]); + assert_eq!( + plan.visible_names, + vec![ + "exec", + "wait", + "agents", + // Hosted Responses tools. + "web_search", + ] + ); assert!( !plan .namespace_function_names("agents") @@ -1235,6 +1244,32 @@ async fn hosted_tools_follow_provider_auth_model_and_config_gates() { } ); + let code_mode_only = probe(|turn| { + use_chatgpt_auth(turn); + set_features(turn, &[Feature::CodeModeOnly, Feature::MultiAgentV2]); + set_web_search_mode(turn, WebSearchMode::Live); + turn.model_info.input_modalities = vec![InputModality::Image]; + }) + .await; + assert_eq!( + code_mode_only.visible_names, + vec![ + // Code-mode entrypoints. + codex_code_mode::PUBLIC_TOOL_NAME, + codex_code_mode::WAIT_TOOL_NAME, + // Multi-agent v2 tools. + "spawn_agent", + "send_message", + "followup_task", + "wait_agent", + "close_agent", + "list_agents", + // Hosted Responses tools. + "web_search", + "image_generation", + ] + ); + let standalone_web_search_without_web_run = probe(|turn| { set_feature(turn, Feature::StandaloneWebSearch, /*enabled*/ true); set_web_search_mode(turn, WebSearchMode::Live); diff --git a/codex-rs/core/tests/suite/model_runtime_selectors.rs b/codex-rs/core/tests/suite/model_runtime_selectors.rs index 06385f656..c17121b52 100644 --- a/codex-rs/core/tests/suite/model_runtime_selectors.rs +++ b/codex-rs/core/tests/suite/model_runtime_selectors.rs @@ -5,6 +5,7 @@ use codex_login::CodexAuth; use codex_models_manager::manager::RefreshStrategy; use codex_models_manager::manager::SharedModelsManager; use codex_models_manager::model_info::model_info_from_slug; +use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ModelVisibility; @@ -165,12 +166,17 @@ async fn remote_tool_mode_selector_overrides_feature_flags() -> Result<()> { let mut code_mode_only_model = remote_model("test-tool-mode-code-mode-only"); code_mode_only_model.tool_mode = Some(ToolMode::CodeModeOnly); + code_mode_only_model.input_modalities = vec![InputModality::Text, InputModality::Image]; let code_mode_only_body = response_body_for_remote_model(code_mode_only_model, |_| {}).await?; assert_eq!( tool_names(&code_mode_only_body), vec![ + // Code-mode entrypoints. codex_code_mode::PUBLIC_TOOL_NAME.to_string(), codex_code_mode::WAIT_TOOL_NAME.to_string(), + // Hosted Responses tools. + "web_search".to_string(), + "image_generation".to_string(), ] );