diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 7199782c3a..3a6c28bdb2 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -461,6 +461,9 @@ "web_search_cached": { "type": "boolean" }, + "web_search_image_support": { + "type": "boolean" + }, "web_search_request": { "type": "boolean" } @@ -1852,6 +1855,9 @@ "web_search_cached": { "type": "boolean" }, + "web_search_image_support": { + "type": "boolean" + }, "web_search_request": { "type": "boolean" } diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 50facf53e4..d775c9a376 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -176,6 +176,8 @@ pub(crate) mod tools { WebSearch { #[serde(skip_serializing_if = "Option::is_none")] external_web_access: Option, + #[serde(skip_serializing_if = "Option::is_none")] + search_content_types: Option>, }, #[serde(rename = "custom")] Freeform(FreeformTool), diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index dbc4e70685..53321a05d7 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -163,6 +163,9 @@ pub enum Feature { ResponsesWebsockets, /// Enable Responses API websocket v2 mode. ResponsesWebsocketsV2, + /// Allow Codex web_search to request image results and send the temporary + /// Responses feature-override header needed to enable that server-side path. + WebSearchImageSupport, } impl Feature { @@ -753,6 +756,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::UnderDevelopment, default_enabled: false, }, + FeatureSpec { + id: Feature::WebSearchImageSupport, + key: "web_search_image_support", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, ]; /// Push a warning event if any under-development features are enabled. diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index dcdb48500e..f9666dbbff 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -36,6 +36,8 @@ use std::collections::HashMap; const SEARCH_TOOL_BM25_DESCRIPTION_TEMPLATE: &str = include_str!("../../templates/search_tool/tool_description.md"); +const WEB_SEARCH_CONTENT_TYPES: [&str; 2] = ["text", "image"]; + #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum ShellCommandBackendConfig { Classic, @@ -49,6 +51,7 @@ pub(crate) struct ToolsConfig { pub allow_login_shell: bool, pub apply_patch_tool_type: Option, pub web_search_mode: Option, + pub web_search_image_support: bool, pub image_gen_tool: bool, pub agent_roles: BTreeMap, pub search_tool: bool, @@ -140,6 +143,7 @@ impl ToolsConfig { allow_login_shell: true, apply_patch_tool_type, web_search_mode: *web_search_mode, + web_search_image_support: features.enabled(Feature::WebSearchImageSupport), image_gen_tool: include_image_gen_tool, agent_roles: BTreeMap::new(), search_tool: include_search_tool, @@ -167,6 +171,15 @@ impl ToolsConfig { } } +fn web_search_content_types(web_search_image_support: bool) -> Option> { + web_search_image_support.then(|| { + WEB_SEARCH_CONTENT_TYPES + .into_iter() + .map(str::to_string) + .collect() + }) +} + fn supports_image_generation(model_info: &ModelInfo) -> bool { model_info.input_modalities.contains(&InputModality::Image) } @@ -1881,11 +1894,13 @@ pub(crate) fn build_specs( Some(WebSearchMode::Cached) => { builder.push_spec(ToolSpec::WebSearch { external_web_access: Some(false), + search_content_types: web_search_content_types(config.web_search_image_support), }); } Some(WebSearchMode::Live) => { builder.push_spec(ToolSpec::WebSearch { external_web_access: Some(true), + search_content_types: web_search_content_types(config.web_search_image_support), }); } Some(WebSearchMode::Disabled) | None => {} @@ -2172,6 +2187,7 @@ mod tests { create_apply_patch_freeform_tool(), ToolSpec::WebSearch { external_web_access: Some(true), + search_content_types: None, }, create_view_image_tool(), ] { @@ -2486,6 +2502,7 @@ mod tests { tool.spec, ToolSpec::WebSearch { external_web_access: Some(false), + search_content_types: None, } ); } @@ -2510,6 +2527,38 @@ mod tests { tool.spec, ToolSpec::WebSearch { external_web_access: Some(true), + search_content_types: None, + } + ); + } + + #[test] + fn web_search_image_support_sets_search_content_types_when_web_search_enabled() { + let config = test_config(); + let model_info = + ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let mut features = Features::with_defaults(); + features.enable(Feature::WebSearchImageSupport); + + let tools_config = ToolsConfig::new(&ToolsConfigParams { + model_info: &model_info, + features: &features, + web_search_mode: Some(WebSearchMode::Cached), + session_source: SessionSource::Cli, + }); + let (tools, _) = build_specs(&tools_config, None, None, &[]).build(); + + let tool = find_tool(&tools, "web_search"); + assert_eq!( + tool.spec, + ToolSpec::WebSearch { + external_web_access: Some(false), + search_content_types: Some( + WEB_SEARCH_CONTENT_TYPES + .into_iter() + .map(str::to_string) + .collect(), + ), } ); } diff --git a/codex-rs/core/tests/suite/web_search.rs b/codex-rs/core/tests/suite/web_search.rs index 6df0338e92..367276c5c6 100644 --- a/codex-rs/core/tests/suite/web_search.rs +++ b/codex-rs/core/tests/suite/web_search.rs @@ -223,3 +223,47 @@ async fn web_search_mode_updates_between_turns_with_sandbox_policy() { "danger-full-access policy should default web_search to live" ); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn web_search_image_support_feature_sets_content_types() { + skip_if_no_network!(); + + let server = start_mock_server().await; + let sse = responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_completed("resp-1"), + ]); + let resp_mock = responses::mount_sse_once(&server, sse).await; + + let mut builder = test_codex() + .with_model("gpt-5-codex") + .with_config(|config| { + config.features.enable(Feature::WebSearchImageSupport); + config + .web_search_mode + .set(WebSearchMode::Cached) + .expect("test web_search_mode should satisfy constraints"); + }); + let test = builder + .build(&server) + .await + .expect("create test Codex conversation"); + + test.submit_turn_with_policy( + "hello image web search", + SandboxPolicy::new_read_only_policy(), + ) + .await + .expect("submit turn"); + + let body = resp_mock.single_request().body_json(); + let tool = find_web_search_tool(&body); + assert_eq!( + tool.get("search_content_types").and_then(Value::as_array), + Some(&vec![ + Value::String("text".to_string()), + Value::String("image".to_string()), + ]), + "web_search image support should request both text and image results" + ); +}