add WebSearchMode enum (#9216)

### What
Add `WebSearchMode` enum (disabled, cached live, defaults to cached) to
config + V2 protocol. This enum takes precedence over legacy flags:
`web_search_cached`, `web_search_request`, and `tools.web_search`.

Keep `--search` as live.

### Tests
Added tests
This commit is contained in:
sayan-oai
2026-01-14 12:51:42 -08:00
committed by GitHub
parent 27da8a68d3
commit 5e426ac270
22 changed files with 302 additions and 72 deletions
+5
View File
@@ -34,6 +34,7 @@ use async_channel::Receiver;
use async_channel::Sender;
use codex_protocol::ThreadId;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::items::TurnItem;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::protocol::FileChange;
@@ -537,6 +538,7 @@ impl Session {
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &per_turn_config.features,
web_search_mode: per_turn_config.web_search_mode,
});
let base_instructions = if per_turn_config.features.enabled(Feature::Collab) {
@@ -2409,9 +2411,11 @@ async fn spawn_review_thread(
review_features
.disable(crate::features::Feature::WebSearchRequest)
.disable(crate::features::Feature::WebSearchCached);
let review_web_search_mode = WebSearchMode::Disabled;
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &review_model_info,
features: &review_features,
web_search_mode: review_web_search_mode,
});
let base_instructions = REVIEW_PROMPT.to_string();
@@ -2424,6 +2428,7 @@ async fn spawn_review_thread(
let mut per_turn_config = (*config).clone();
per_turn_config.model = Some(model.clone());
per_turn_config.features = review_features.clone();
per_turn_config.web_search_mode = review_web_search_mode;
let otel_manager = parent_turn_context
.client
+75 -7
View File
@@ -42,6 +42,7 @@ use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::config_types::Verbosity;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::openai_models::ReasoningEffort;
use codex_rmcp_client::OAuthCredentialsStoreMode;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -336,7 +337,7 @@ pub struct Config {
/// model info's default preference.
pub include_apply_patch_tool: bool,
pub tools_web_search_request: bool,
pub web_search_mode: WebSearchMode,
/// If set to `true`, used only the experimental unified exec tool.
pub use_experimental_unified_exec_tool: bool,
@@ -894,6 +895,9 @@ pub struct ConfigToml {
pub projects: Option<HashMap<String, ProjectConfig>>,
/// Controls the web search tool mode: disabled, cached, or live.
pub web_search: Option<WebSearchMode>,
/// Nested tools section for feature toggles
pub tools: Option<ToolsToml>,
@@ -1178,6 +1182,26 @@ pub fn resolve_oss_provider(
}
}
/// Resolve the web search mode from the config, profile, and features.
fn resolve_web_search_mode(
config_toml: &ConfigToml,
config_profile: &ConfigProfile,
features: &Features,
) -> WebSearchMode {
// Enum gets precedence over features flags
if let Some(mode) = config_profile.web_search.or(config_toml.web_search) {
return mode;
}
if features.enabled(Feature::WebSearchCached) {
return WebSearchMode::Cached;
}
if features.enabled(Feature::WebSearchRequest) {
return WebSearchMode::Live;
}
// Fall back to default
WebSearchMode::default()
}
impl Config {
#[cfg(test)]
fn load_from_base_config_with_overrides(
@@ -1242,6 +1266,7 @@ impl Config {
};
let features = Features::from_config(&cfg, &config_profile, feature_overrides);
let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features);
#[cfg(target_os = "windows")]
{
// Base flag controls sandbox on/off; elevated only applies when base is enabled.
@@ -1361,7 +1386,6 @@ impl Config {
};
let include_apply_patch_tool_flag = features.enabled(Feature::ApplyPatchFreeform);
let tools_web_search_request = features.enabled(Feature::WebSearchRequest);
let use_experimental_unified_exec_tool = features.enabled(Feature::UnifiedExec);
let forced_chatgpt_workspace_id =
@@ -1502,7 +1526,7 @@ impl Config {
forced_chatgpt_workspace_id,
forced_login_method,
include_apply_patch_tool: include_apply_patch_tool_flag,
tools_web_search_request,
web_search_mode,
use_experimental_unified_exec_tool,
ghost_snapshot,
features,
@@ -2177,6 +2201,50 @@ trust_level = "trusted"
Ok(())
}
#[test]
fn web_search_mode_uses_default_if_unset() {
let cfg = ConfigToml::default();
let profile = ConfigProfile::default();
let features = Features::with_defaults();
assert_eq!(
resolve_web_search_mode(&cfg, &profile, &features),
WebSearchMode::default()
);
}
#[test]
fn web_search_mode_prefers_profile_over_legacy_flags() {
let cfg = ConfigToml::default();
let profile = ConfigProfile {
web_search: Some(WebSearchMode::Live),
..Default::default()
};
let mut features = Features::with_defaults();
features.enable(Feature::WebSearchCached);
assert_eq!(
resolve_web_search_mode(&cfg, &profile, &features),
WebSearchMode::Live
);
}
#[test]
fn web_search_mode_disabled_overrides_legacy_request() {
let cfg = ConfigToml {
web_search: Some(WebSearchMode::Disabled),
..Default::default()
};
let profile = ConfigProfile::default();
let mut features = Features::with_defaults();
features.enable(Feature::WebSearchRequest);
assert_eq!(
resolve_web_search_mode(&cfg, &profile, &features),
WebSearchMode::Disabled
);
}
#[test]
fn profile_legacy_toggles_override_base() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
@@ -3513,7 +3581,7 @@ model_verbosity = "high"
forced_chatgpt_workspace_id: None,
forced_login_method: None,
include_apply_patch_tool: false,
tools_web_search_request: false,
web_search_mode: WebSearchMode::default(),
use_experimental_unified_exec_tool: false,
ghost_snapshot: GhostSnapshotConfig::default(),
features: Features::with_defaults(),
@@ -3600,7 +3668,7 @@ model_verbosity = "high"
forced_chatgpt_workspace_id: None,
forced_login_method: None,
include_apply_patch_tool: false,
tools_web_search_request: false,
web_search_mode: WebSearchMode::default(),
use_experimental_unified_exec_tool: false,
ghost_snapshot: GhostSnapshotConfig::default(),
features: Features::with_defaults(),
@@ -3702,7 +3770,7 @@ model_verbosity = "high"
forced_chatgpt_workspace_id: None,
forced_login_method: None,
include_apply_patch_tool: false,
tools_web_search_request: false,
web_search_mode: WebSearchMode::default(),
use_experimental_unified_exec_tool: false,
ghost_snapshot: GhostSnapshotConfig::default(),
features: Features::with_defaults(),
@@ -3790,7 +3858,7 @@ model_verbosity = "high"
forced_chatgpt_workspace_id: None,
forced_login_method: None,
include_apply_patch_tool: false,
tools_web_search_request: false,
web_search_mode: WebSearchMode::default(),
use_experimental_unified_exec_tool: false,
ghost_snapshot: GhostSnapshotConfig::default(),
features: Features::with_defaults(),
+2
View File
@@ -7,6 +7,7 @@ use crate::protocol::AskForApproval;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::Verbosity;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::openai_models::ReasoningEffort;
/// Collection of common configuration options that a user can define as a unit
@@ -31,6 +32,7 @@ pub struct ConfigProfile {
pub experimental_use_freeform_apply_patch: Option<bool>,
pub tools_web_search: Option<bool>,
pub tools_view_image: Option<bool>,
pub web_search: Option<WebSearchMode>,
pub analytics: Option<crate::config::types::AnalyticsConfigToml>,
/// Optional feature toggles scoped to this profile.
#[serde(default)]
+1
View File
@@ -4,6 +4,7 @@
// definitions that do not contain business logic.
pub use codex_protocol::config_types::AltScreenMode;
pub use codex_protocol::config_types::WebSearchMode;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::BTreeMap;
use std::collections::HashMap;
+2 -3
View File
@@ -1,6 +1,7 @@
use std::sync::Arc;
use async_trait::async_trait;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::items::TurnItem;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
@@ -85,9 +86,7 @@ async fn start_review_conversation(
let mut sub_agent_config = config.as_ref().clone();
// Carry over review-only feature restrictions so the delegate cannot
// re-enable blocked tools (web search, view image).
sub_agent_config
.features
.disable(crate::features::Feature::WebSearchRequest);
sub_agent_config.web_search_mode = WebSearchMode::Disabled;
// Set explicit review rubric for the sub-agent
sub_agent_config.base_instructions = Some(crate::REVIEW_PROMPT.to_string());
+63 -41
View File
@@ -8,6 +8,7 @@ use crate::tools::handlers::apply_patch::create_apply_patch_json_tool;
use crate::tools::handlers::collab::DEFAULT_WAIT_TIMEOUT_MS;
use crate::tools::handlers::collab::MAX_WAIT_TIMEOUT_MS;
use crate::tools::registry::ToolRegistryBuilder;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::models::VIEW_IMAGE_TOOL_NAME;
use codex_protocol::openai_models::ApplyPatchToolType;
use codex_protocol::openai_models::ConfigShellToolType;
@@ -23,8 +24,7 @@ use std::collections::HashMap;
pub(crate) struct ToolsConfig {
pub shell_type: ConfigShellToolType,
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
pub web_search_request: bool,
pub web_search_cached: bool,
pub web_search_mode: WebSearchMode,
pub collab_tools: bool,
pub experimental_supported_tools: Vec<String>,
}
@@ -32,6 +32,7 @@ pub(crate) struct ToolsConfig {
pub(crate) struct ToolsConfigParams<'a> {
pub(crate) model_info: &'a ModelInfo,
pub(crate) features: &'a Features,
pub(crate) web_search_mode: WebSearchMode,
}
impl ToolsConfig {
@@ -39,10 +40,9 @@ impl ToolsConfig {
let ToolsConfigParams {
model_info,
features,
web_search_mode,
} = params;
let include_apply_patch_tool = features.enabled(Feature::ApplyPatchFreeform);
let include_web_search_request = features.enabled(Feature::WebSearchRequest);
let include_web_search_cached = features.enabled(Feature::WebSearchCached);
let include_collab_tools = features.enabled(Feature::Collab);
let shell_type = if !features.enabled(Feature::ShellTool) {
@@ -73,8 +73,7 @@ impl ToolsConfig {
Self {
shell_type,
apply_patch_tool_type,
web_search_request: include_web_search_request,
web_search_cached: include_web_search_cached,
web_search_mode: *web_search_mode,
collab_tools: include_collab_tools,
experimental_supported_tools: model_info.experimental_supported_tools.clone(),
}
@@ -1225,15 +1224,18 @@ pub(crate) fn build_specs(
builder.register_handler("test_sync_tool", test_sync_handler);
}
// Prefer web_search_cached flag over web_search_request
if config.web_search_cached {
builder.push_spec(ToolSpec::WebSearch {
external_web_access: Some(false),
});
} else if config.web_search_request {
builder.push_spec(ToolSpec::WebSearch {
external_web_access: Some(true),
});
match config.web_search_mode {
WebSearchMode::Disabled => {}
WebSearchMode::Cached => {
builder.push_spec(ToolSpec::WebSearch {
external_web_access: Some(false),
});
}
WebSearchMode::Live => {
builder.push_spec(ToolSpec::WebSearch {
external_web_access: Some(true),
});
}
}
builder.push_spec_with_parallel_support(create_view_image_tool(), true);
@@ -1374,10 +1376,10 @@ mod tests {
let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
features.enable(Feature::WebSearchRequest);
let config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: WebSearchMode::Live,
});
let (tools, _) = build_specs(&config, None).build();
@@ -1439,6 +1441,7 @@ mod tests {
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: WebSearchMode::Cached,
});
let (tools, _) = build_specs(&tools_config, None).build();
assert_contains_tool_names(
@@ -1447,12 +1450,18 @@ mod tests {
);
}
fn assert_model_tools(model_slug: &str, features: &Features, expected_tools: &[&str]) {
fn assert_model_tools(
model_slug: &str,
features: &Features,
web_search_mode: WebSearchMode,
expected_tools: &[&str],
) {
let config = test_config();
let model_info = ModelsManager::construct_model_info_offline(model_slug, &config);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features,
web_search_mode,
});
let (tools, _) = build_specs(&tools_config, Some(HashMap::new())).build();
let tool_names = tools.iter().map(|t| t.spec.name()).collect::<Vec<_>>();
@@ -1460,15 +1469,15 @@ mod tests {
}
#[test]
fn web_search_cached_sets_external_web_access_false() {
fn web_search_mode_cached_sets_external_web_access_false() {
let config = test_config();
let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::WebSearchCached);
let features = Features::with_defaults();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: WebSearchMode::Cached,
});
let (tools, _) = build_specs(&tools_config, None).build();
@@ -1482,16 +1491,15 @@ mod tests {
}
#[test]
fn web_search_cached_takes_precedence_over_web_search_request() {
fn web_search_mode_live_sets_external_web_access_true() {
let config = test_config();
let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::WebSearchCached);
features.enable(Feature::WebSearchRequest);
let features = Features::with_defaults();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: WebSearchMode::Live,
});
let (tools, _) = build_specs(&tools_config, None).build();
@@ -1499,7 +1507,7 @@ mod tests {
assert_eq!(
tool.spec,
ToolSpec::WebSearch {
external_web_access: Some(false),
external_web_access: Some(true),
}
);
}
@@ -1509,6 +1517,7 @@ mod tests {
assert_model_tools(
"gpt-5-codex",
&Features::with_defaults(),
WebSearchMode::Cached,
&[
"shell_command",
"list_mcp_resources",
@@ -1516,6 +1525,7 @@ mod tests {
"read_mcp_resource",
"update_plan",
"apply_patch",
"web_search",
"view_image",
],
);
@@ -1526,6 +1536,7 @@ mod tests {
assert_model_tools(
"gpt-5.1-codex",
&Features::with_defaults(),
WebSearchMode::Cached,
&[
"shell_command",
"list_mcp_resources",
@@ -1533,6 +1544,7 @@ mod tests {
"read_mcp_resource",
"update_plan",
"apply_patch",
"web_search",
"view_image",
],
);
@@ -1542,9 +1554,8 @@ mod tests {
fn test_build_specs_gpt5_codex_unified_exec_web_search() {
assert_model_tools(
"gpt-5-codex",
Features::with_defaults()
.enable(Feature::UnifiedExec)
.enable(Feature::WebSearchRequest),
Features::with_defaults().enable(Feature::UnifiedExec),
WebSearchMode::Live,
&[
"exec_command",
"write_stdin",
@@ -1563,9 +1574,8 @@ mod tests {
fn test_build_specs_gpt51_codex_unified_exec_web_search() {
assert_model_tools(
"gpt-5.1-codex",
Features::with_defaults()
.enable(Feature::UnifiedExec)
.enable(Feature::WebSearchRequest),
Features::with_defaults().enable(Feature::UnifiedExec),
WebSearchMode::Live,
&[
"exec_command",
"write_stdin",
@@ -1585,12 +1595,14 @@ mod tests {
assert_model_tools(
"codex-mini-latest",
&Features::with_defaults(),
WebSearchMode::Cached,
&[
"local_shell",
"list_mcp_resources",
"list_mcp_resource_templates",
"read_mcp_resource",
"update_plan",
"web_search",
"view_image",
],
);
@@ -1601,6 +1613,7 @@ mod tests {
assert_model_tools(
"gpt-5.1-codex-mini",
&Features::with_defaults(),
WebSearchMode::Cached,
&[
"shell_command",
"list_mcp_resources",
@@ -1608,6 +1621,7 @@ mod tests {
"read_mcp_resource",
"update_plan",
"apply_patch",
"web_search",
"view_image",
],
);
@@ -1618,12 +1632,14 @@ mod tests {
assert_model_tools(
"gpt-5",
&Features::with_defaults(),
WebSearchMode::Cached,
&[
"shell",
"list_mcp_resources",
"list_mcp_resource_templates",
"read_mcp_resource",
"update_plan",
"web_search",
"view_image",
],
);
@@ -1634,6 +1650,7 @@ mod tests {
assert_model_tools(
"gpt-5.1",
&Features::with_defaults(),
WebSearchMode::Cached,
&[
"shell_command",
"list_mcp_resources",
@@ -1641,6 +1658,7 @@ mod tests {
"read_mcp_resource",
"update_plan",
"apply_patch",
"web_search",
"view_image",
],
);
@@ -1651,6 +1669,7 @@ mod tests {
assert_model_tools(
"exp-5.1",
&Features::with_defaults(),
WebSearchMode::Cached,
&[
"exec_command",
"write_stdin",
@@ -1659,6 +1678,7 @@ mod tests {
"read_mcp_resource",
"update_plan",
"apply_patch",
"web_search",
"view_image",
],
);
@@ -1668,9 +1688,8 @@ mod tests {
fn test_codex_mini_unified_exec_web_search() {
assert_model_tools(
"codex-mini-latest",
Features::with_defaults()
.enable(Feature::UnifiedExec)
.enable(Feature::WebSearchRequest),
Features::with_defaults().enable(Feature::UnifiedExec),
WebSearchMode::Live,
&[
"exec_command",
"write_stdin",
@@ -1689,11 +1708,11 @@ mod tests {
let config = test_config();
let model_info = ModelsManager::construct_model_info_offline("o3", &config);
let mut features = Features::with_defaults();
features.enable(Feature::WebSearchRequest);
features.enable(Feature::UnifiedExec);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: WebSearchMode::Live,
});
let (tools, _) = build_specs(&tools_config, Some(HashMap::new())).build();
@@ -1715,6 +1734,7 @@ mod tests {
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: WebSearchMode::Cached,
});
let (tools, _) = build_specs(&tools_config, None).build();
@@ -1733,6 +1753,7 @@ mod tests {
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: WebSearchMode::Cached,
});
let (tools, _) = build_specs(&tools_config, None).build();
@@ -1760,10 +1781,10 @@ mod tests {
let model_info = ModelsManager::construct_model_info_offline("o3", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
features.enable(Feature::WebSearchRequest);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: WebSearchMode::Live,
});
let (tools, _) = build_specs(
&tools_config,
@@ -1858,6 +1879,7 @@ mod tests {
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: WebSearchMode::Cached,
});
// Intentionally construct a map with keys that would sort alphabetically.
@@ -1931,10 +1953,10 @@ mod tests {
let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
features.enable(Feature::WebSearchRequest);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: WebSearchMode::Cached,
});
let (tools, _) = build_specs(
@@ -1988,10 +2010,10 @@ mod tests {
let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
features.enable(Feature::WebSearchRequest);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: WebSearchMode::Cached,
});
let (tools, _) = build_specs(
@@ -2041,11 +2063,11 @@ mod tests {
let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
features.enable(Feature::WebSearchRequest);
features.enable(Feature::ApplyPatchFreeform);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: WebSearchMode::Cached,
});
let (tools, _) = build_specs(
@@ -2098,10 +2120,10 @@ mod tests {
let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
features.enable(Feature::WebSearchRequest);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: WebSearchMode::Cached,
});
let (tools, _) = build_specs(
@@ -2210,10 +2232,10 @@ Examples of valid command strings:
let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::UnifiedExec);
features.enable(Feature::WebSearchRequest);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: WebSearchMode::Cached,
});
let (tools, _) = build_specs(
&tools_config,