chore: add web_search_tool_type for image support (#13538)

add `web_search_tool_type` on model_info that can be populated from
backend. will be used to filter which models can use `web_search` with
images and which cant.

added small unit test.
This commit is contained in:
sayan-oai
2026-03-04 23:02:27 -08:00
committed by GitHub
Unverified
parent 8f828f8a43
commit 03d55f0e6f
12 changed files with 89 additions and 12 deletions
@@ -36,6 +36,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
default_verbosity: None,
availability_nux: None,
apply_patch_tool_type: None,
web_search_tool_type: Default::default(),
truncation_policy: TruncationPolicyConfig::bytes(10_000),
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
@@ -84,6 +84,7 @@ async fn models_client_hits_models_endpoint() {
default_verbosity: None,
availability_nux: None,
apply_patch_tool_type: None,
web_search_tool_type: Default::default(),
truncation_policy: TruncationPolicyConfig::bytes(10_000),
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
+2
View File
@@ -176,6 +176,8 @@ pub(crate) mod tools {
WebSearch {
#[serde(skip_serializing_if = "Option::is_none")]
external_web_access: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
search_content_types: Option<Vec<String>>,
},
#[serde(rename = "custom")]
Freeform(FreeformTool),
@@ -6,6 +6,7 @@ use codex_protocol::openai_models::ModelMessages;
use codex_protocol::openai_models::ModelVisibility;
use codex_protocol::openai_models::TruncationMode;
use codex_protocol::openai_models::TruncationPolicyConfig;
use codex_protocol::openai_models::WebSearchToolType;
use codex_protocol::openai_models::default_input_modalities;
use crate::config::Config;
@@ -78,6 +79,7 @@ pub(crate) fn model_info_from_slug(slug: &str) -> ModelInfo {
support_verbosity: false,
default_verbosity: None,
apply_patch_tool_type: None,
web_search_tool_type: WebSearchToolType::Text,
truncation_policy: TruncationPolicyConfig::bytes(10_000),
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
+59 -12
View File
@@ -25,6 +25,7 @@ use codex_protocol::openai_models::ApplyPatchToolType;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::WebSearchToolType;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use serde::Deserialize;
@@ -36,6 +37,7 @@ 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<ApplyPatchToolType>,
pub web_search_mode: Option<WebSearchMode>,
pub web_search_tool_type: WebSearchToolType,
pub image_gen_tool: bool,
pub agent_roles: BTreeMap<String, AgentRoleConfig>,
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_tool_type: model_info.web_search_tool_type,
image_gen_tool: include_image_gen_tool,
agent_roles: BTreeMap::new(),
search_tool: include_search_tool,
@@ -1877,18 +1881,27 @@ pub(crate) fn build_specs(
builder.register_handler("test_sync_tool", test_sync_handler);
}
match config.web_search_mode {
Some(WebSearchMode::Cached) => {
builder.push_spec(ToolSpec::WebSearch {
external_web_access: Some(false),
});
}
Some(WebSearchMode::Live) => {
builder.push_spec(ToolSpec::WebSearch {
external_web_access: Some(true),
});
}
Some(WebSearchMode::Disabled) | None => {}
let external_web_access = match config.web_search_mode {
Some(WebSearchMode::Cached) => Some(false),
Some(WebSearchMode::Live) => Some(true),
Some(WebSearchMode::Disabled) | None => None,
};
if let Some(external_web_access) = external_web_access {
let search_content_types = match config.web_search_tool_type {
WebSearchToolType::Text => None,
WebSearchToolType::TextAndImage => Some(
WEB_SEARCH_CONTENT_TYPES
.into_iter()
.map(str::to_string)
.collect(),
),
};
builder.push_spec(ToolSpec::WebSearch {
external_web_access: Some(external_web_access),
search_content_types,
});
}
if config.image_gen_tool {
@@ -2172,6 +2185,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 +2500,7 @@ mod tests {
tool.spec,
ToolSpec::WebSearch {
external_web_access: Some(false),
search_content_types: None,
}
);
}
@@ -2510,6 +2525,38 @@ mod tests {
tool.spec,
ToolSpec::WebSearch {
external_web_access: Some(true),
search_content_types: None,
}
);
}
#[test]
fn web_search_tool_type_text_and_image_sets_search_content_types() {
let config = test_config();
let mut model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
model_info.web_search_tool_type = WebSearchToolType::TextAndImage;
let features = Features::with_defaults();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: Some(WebSearchMode::Live),
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(true),
search_content_types: Some(
WEB_SEARCH_CONTENT_TYPES
.into_iter()
.map(str::to_string)
.collect()
),
}
);
}
@@ -65,6 +65,7 @@ fn test_model_info(
default_verbosity: None,
availability_nux: None,
apply_patch_tool_type: None,
web_search_tool_type: Default::default(),
truncation_policy: TruncationPolicyConfig::bytes(10_000),
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
@@ -684,6 +685,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
default_verbosity: None,
availability_nux: None,
apply_patch_tool_type: None,
web_search_tool_type: Default::default(),
truncation_policy: TruncationPolicyConfig::bytes(10_000),
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
@@ -342,6 +342,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
default_verbosity: None,
availability_nux: None,
apply_patch_tool_type: None,
web_search_tool_type: Default::default(),
truncation_policy: TruncationPolicyConfig::bytes(10_000),
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
+2
View File
@@ -647,6 +647,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
default_verbosity: None,
availability_nux: None,
apply_patch_tool_type: None,
web_search_tool_type: Default::default(),
truncation_policy: TruncationPolicyConfig::bytes(10_000),
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
@@ -761,6 +762,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
default_verbosity: None,
availability_nux: None,
apply_patch_tool_type: None,
web_search_tool_type: Default::default(),
truncation_policy: TruncationPolicyConfig::bytes(10_000),
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
@@ -301,6 +301,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
default_verbosity: None,
availability_nux: None,
apply_patch_tool_type: None,
web_search_tool_type: Default::default(),
truncation_policy: TruncationPolicyConfig::bytes(10_000),
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
@@ -542,6 +543,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
default_verbosity: None,
availability_nux: None,
apply_patch_tool_type: None,
web_search_tool_type: Default::default(),
truncation_policy: TruncationPolicyConfig::bytes(10_000),
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
@@ -1007,6 +1009,7 @@ fn test_remote_model_with_policy(
default_verbosity: None,
availability_nux: None,
apply_patch_tool_type: None,
web_search_tool_type: Default::default(),
truncation_policy,
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
+1
View File
@@ -407,6 +407,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
default_verbosity: None,
availability_nux: None,
apply_patch_tool_type: None,
web_search_tool_type: Default::default(),
truncation_policy: TruncationPolicyConfig::bytes(10_000),
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
+1
View File
@@ -1003,6 +1003,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
default_verbosity: None,
availability_nux: None,
apply_patch_tool_type: None,
web_search_tool_type: Default::default(),
truncation_policy: TruncationPolicyConfig::bytes(10_000),
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
+14
View File
@@ -181,6 +181,16 @@ pub enum ApplyPatchToolType {
Function,
}
#[derive(
Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, TS, JsonSchema, Default,
)]
#[serde(rename_all = "snake_case")]
pub enum WebSearchToolType {
#[default]
Text,
TextAndImage,
}
/// Server-provided truncation policy metadata for a model.
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema)]
#[serde(rename_all = "snake_case")]
@@ -243,6 +253,8 @@ pub struct ModelInfo {
pub support_verbosity: bool,
pub default_verbosity: Option<Verbosity>,
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
#[serde(default)]
pub web_search_tool_type: WebSearchToolType,
pub truncation_policy: TruncationPolicyConfig,
pub supports_parallel_tool_calls: bool,
#[serde(default)]
@@ -515,6 +527,7 @@ mod tests {
support_verbosity: false,
default_verbosity: None,
apply_patch_tool_type: None,
web_search_tool_type: WebSearchToolType::Text,
truncation_policy: TruncationPolicyConfig::bytes(10_000),
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
@@ -718,6 +731,7 @@ mod tests {
assert_eq!(model.availability_nux, None);
assert!(!model.supports_image_detail_original);
assert_eq!(model.web_search_tool_type, WebSearchToolType::Text);
}
#[test]