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
Unverified
parent 27da8a68d3
commit 5e426ac270
22 changed files with 302 additions and 72 deletions
@@ -8,6 +8,7 @@ use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode as CoreSandboxMode;
use codex_protocol::config_types::Verbosity;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
use codex_protocol::items::TurnItem as CoreTurnItem;
use codex_protocol::models::ResponseItem;
@@ -327,6 +328,7 @@ pub struct ProfileV2 {
pub model_reasoning_effort: Option<ReasoningEffort>,
pub model_reasoning_summary: Option<ReasoningSummary>,
pub model_verbosity: Option<Verbosity>,
pub web_search: Option<WebSearchMode>,
pub chatgpt_base_url: Option<String>,
#[serde(default, flatten)]
pub additional: HashMap<String, JsonValue>,
@@ -355,6 +357,7 @@ pub struct Config {
pub sandbox_workspace_write: Option<SandboxWorkspaceWrite>,
pub forced_chatgpt_workspace_id: Option<String>,
pub forced_login_method: Option<ForcedLoginMethod>,
pub web_search: Option<WebSearchMode>,
pub tools: Option<ToolsV2>,
pub profile: Option<String>,
#[serde(default)]
+3 -3
View File
@@ -691,11 +691,11 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
.parse_overrides()
.map_err(anyhow::Error::msg)?;
// Honor `--search` via the new feature toggle.
// Honor `--search` via the canonical web_search mode.
if interactive.web_search {
cli_kv_overrides.push((
"features.web_search_request".to_string(),
toml::Value::Boolean(true),
"web_search".to_string(),
toml::Value::String("live".to_string()),
));
}
+19
View File
@@ -395,6 +395,14 @@
}
]
},
"web_search": {
"description": "Controls the web search tool mode: disabled, cached, or live.",
"allOf": [
{
"$ref": "#/definitions/WebSearchMode"
}
]
},
"windows_wsl_setup_acknowledged": {
"description": "Tracks whether the Windows onboarding screen has been acknowledged.",
"type": "boolean"
@@ -634,6 +642,9 @@
},
"tools_web_search": {
"type": "boolean"
},
"web_search": {
"$ref": "#/definitions/WebSearchMode"
}
},
"additionalProperties": false
@@ -1420,6 +1431,14 @@
"high"
]
},
"WebSearchMode": {
"type": "string",
"enum": [
"disabled",
"cached",
"live"
]
},
"WireApi": {
"description": "Wire protocol that the provider speaks. Most third-party services only implement the classic OpenAI Chat Completions JSON schema, whereas OpenAI itself (and a handful of others) additionally expose the more modern *Responses* API. The two protocols use different request/response shapes and *cannot* be auto-detected at runtime, therefore each provider entry must declare which one it expects.",
"oneOf": [
+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,
+11 -1
View File
@@ -1,5 +1,6 @@
#![allow(clippy::unwrap_used)]
use codex_protocol::config_types::WebSearchMode;
use core_test_support::load_sse_fixture_with_id;
use core_test_support::responses;
use core_test_support::responses::start_mock_server;
@@ -32,7 +33,10 @@ async fn collect_tool_identifiers_for_model(model: &str) -> Vec<String> {
let sse = sse_completed(model);
let resp_mock = responses::mount_sse_once(&server, sse).await;
let mut builder = test_codex().with_model(model);
let mut builder = test_codex()
.with_model(model)
// Keep tool expectations stable when the default web_search mode changes.
.with_config(|config| config.web_search_mode = WebSearchMode::Cached);
let test = builder
.build(&server)
.await
@@ -58,6 +62,7 @@ async fn model_selects_expected_tools() {
"list_mcp_resource_templates".to_string(),
"read_mcp_resource".to_string(),
"update_plan".to_string(),
"web_search".to_string(),
"view_image".to_string()
],
"codex-mini-latest should expose the local shell tool",
@@ -73,6 +78,7 @@ async fn model_selects_expected_tools() {
"read_mcp_resource".to_string(),
"update_plan".to_string(),
"apply_patch".to_string(),
"web_search".to_string(),
"view_image".to_string()
],
"gpt-5-codex should expose the apply_patch tool",
@@ -88,6 +94,7 @@ async fn model_selects_expected_tools() {
"read_mcp_resource".to_string(),
"update_plan".to_string(),
"apply_patch".to_string(),
"web_search".to_string(),
"view_image".to_string()
],
"gpt-5.1-codex should expose the apply_patch tool",
@@ -102,6 +109,7 @@ async fn model_selects_expected_tools() {
"list_mcp_resource_templates".to_string(),
"read_mcp_resource".to_string(),
"update_plan".to_string(),
"web_search".to_string(),
"view_image".to_string()
],
"gpt-5 should expose the apply_patch tool",
@@ -117,6 +125,7 @@ async fn model_selects_expected_tools() {
"read_mcp_resource".to_string(),
"update_plan".to_string(),
"apply_patch".to_string(),
"web_search".to_string(),
"view_image".to_string()
],
"gpt-5.1 should expose the apply_patch tool",
@@ -132,6 +141,7 @@ async fn model_selects_expected_tools() {
"read_mcp_resource".to_string(),
"update_plan".to_string(),
"apply_patch".to_string(),
"web_search".to_string(),
"view_image".to_string()
],
"exp-5.1 should expose the apply_patch tool",
+11 -1
View File
@@ -11,6 +11,7 @@ use codex_core::protocol::SandboxPolicy;
use codex_core::protocol_config_types::ReasoningSummary;
use codex_core::shell::Shell;
use codex_core::shell::default_user_shell;
use codex_protocol::config_types::WebSearchMode;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -52,7 +53,13 @@ fn assert_tool_names(body: &serde_json::Value, expected_names: &[&str]) {
.as_array()
.unwrap()
.iter()
.map(|t| t["name"].as_str().unwrap().to_string())
.map(|t| {
t.get("name")
.and_then(|value| value.as_str())
.or_else(|| t.get("type").and_then(|value| value.as_str()))
.unwrap()
.to_string()
})
.collect::<Vec<_>>(),
expected_names
);
@@ -80,6 +87,8 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
.with_config(|config| {
config.user_instructions = Some("be consistent and helpful".to_string());
config.model = Some("gpt-5.1-codex-max".to_string());
// Keep tool expectations stable when the default web_search mode changes.
config.web_search_mode = WebSearchMode::Cached;
})
.build(&server)
.await?;
@@ -122,6 +131,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
"read_mcp_resource",
"update_plan",
"apply_patch",
"web_search",
"view_image",
];
let body0 = req1.single_request().body_json();
@@ -1,6 +1,7 @@
#![allow(clippy::unwrap_used)]
use codex_core::features::Feature;
use codex_protocol::config_types::WebSearchMode;
use core_test_support::load_sse_fixture_with_id;
use core_test_support::responses;
use core_test_support::responses::start_mock_server;
@@ -24,7 +25,7 @@ fn find_web_search_tool(body: &Value) -> &Value {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn web_search_cached_sets_external_web_access_false_in_request_body() {
async fn web_search_mode_cached_sets_external_web_access_false_in_request_body() {
skip_if_no_network!();
let server = start_mock_server().await;
@@ -34,7 +35,7 @@ async fn web_search_cached_sets_external_web_access_false_in_request_body() {
let mut builder = test_codex()
.with_model("gpt-5-codex")
.with_config(|config| {
config.features.enable(Feature::WebSearchCached);
config.web_search_mode = WebSearchMode::Cached;
});
let test = builder
.build(&server)
@@ -50,12 +51,12 @@ async fn web_search_cached_sets_external_web_access_false_in_request_body() {
assert_eq!(
tool.get("external_web_access").and_then(Value::as_bool),
Some(false),
"web_search_cached should force external_web_access=false"
"web_search cached mode should force external_web_access=false"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn web_search_cached_takes_precedence_over_web_search_request_in_request_body() {
async fn web_search_mode_takes_precedence_over_legacy_flags_in_request_body() {
skip_if_no_network!();
let server = start_mock_server().await;
@@ -66,7 +67,7 @@ async fn web_search_cached_takes_precedence_over_web_search_request_in_request_b
.with_model("gpt-5-codex")
.with_config(|config| {
config.features.enable(Feature::WebSearchRequest);
config.features.enable(Feature::WebSearchCached);
config.web_search_mode = WebSearchMode::Cached;
});
let test = builder
.build(&server)
@@ -82,6 +83,6 @@ async fn web_search_cached_takes_precedence_over_web_search_request_in_request_b
assert_eq!(
tool.get("external_web_access").and_then(Value::as_bool),
Some(false),
"web_search_cached should win over web_search_request"
"web_search mode should win over legacy web_search_request"
);
}
+12
View File
@@ -63,6 +63,18 @@ pub enum SandboxMode {
DangerFullAccess,
}
#[derive(
Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS, Default,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum WebSearchMode {
#[default]
Disabled,
Cached,
Live,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
+1 -1
View File
@@ -94,7 +94,7 @@ pub struct Cli {
#[clap(long = "cd", short = 'C', value_name = "DIR")]
pub cwd: Option<PathBuf>,
/// Enable web search (off by default). When enabled, the native Responses `web_search` tool is available to the model (no percall approval).
/// Enable live web search. When enabled, the native Responses `web_search` tool is available to the model (no percall approval).
#[arg(long = "search", default_value_t = false)]
pub web_search: bool,
+2 -2
View File
@@ -123,11 +123,11 @@ pub async fn run_main(
)
};
// Map the legacy --search flag to the new feature toggle.
// Map the legacy --search flag to the canonical web_search mode.
if cli.web_search {
cli.config_overrides
.raw_overrides
.push("features.web_search_request=true".to_string());
.push("web_search=\"live\"".to_string());
}
// When using `--oss`, let the bootstrapper pick the model (defaulting to
+1 -1
View File
@@ -94,7 +94,7 @@ pub struct Cli {
#[clap(long = "cd", short = 'C', value_name = "DIR")]
pub cwd: Option<PathBuf>,
/// Enable web search (off by default). When enabled, the native Responses `web_search` tool is available to the model (no percall approval).
/// Enable live web search. When enabled, the native Responses `web_search` tool is available to the model (no percall approval).
#[arg(long = "search", default_value_t = false)]
pub web_search: bool,
+1 -1
View File
@@ -138,7 +138,7 @@ pub async fn run_main(
if cli.web_search {
cli.config_overrides
.raw_overrides
.push("features.web_search_request=true".to_string());
.push("web_search=\"live\"".to_string());
}
// When using `--oss`, let the bootstrapper pick the model (defaulting to
+15 -4
View File
@@ -3,7 +3,12 @@ import path from "node:path";
import readline from "node:readline";
import { fileURLToPath } from "node:url";
import { SandboxMode, ModelReasoningEffort, ApprovalMode } from "./threadOptions";
import {
SandboxMode,
ModelReasoningEffort,
ApprovalMode,
WebSearchMode,
} from "./threadOptions";
export type CodexExecArgs = {
input: string;
@@ -30,7 +35,9 @@ export type CodexExecArgs = {
signal?: AbortSignal;
// --config sandbox_workspace_write.network_access
networkAccessEnabled?: boolean;
// --config features.web_search_request
// --config web_search
webSearchMode?: WebSearchMode;
// legacy --config features.web_search_request
webSearchEnabled?: boolean;
// --config approval_policy
approvalPolicy?: ApprovalMode;
@@ -88,8 +95,12 @@ export class CodexExec {
);
}
if (args.webSearchEnabled !== undefined) {
commandArgs.push("--config", `features.web_search_request=${args.webSearchEnabled}`);
if (args.webSearchMode) {
commandArgs.push("--config", `web_search="${args.webSearchMode}"`);
} else if (args.webSearchEnabled === true) {
commandArgs.push("--config", `web_search="live"`);
} else if (args.webSearchEnabled === false) {
commandArgs.push("--config", `web_search="disabled"`);
}
if (args.approvalPolicy) {
+1
View File
@@ -35,5 +35,6 @@ export type {
ApprovalMode,
SandboxMode,
ModelReasoningEffort,
WebSearchMode,
} from "./threadOptions";
export type { TurnOptions } from "./turnOptions";
+1
View File
@@ -88,6 +88,7 @@ export class Thread {
modelReasoningEffort: options?.modelReasoningEffort,
signal: turnOptions.signal,
networkAccessEnabled: options?.networkAccessEnabled,
webSearchMode: options?.webSearchMode,
webSearchEnabled: options?.webSearchEnabled,
approvalPolicy: options?.approvalPolicy,
additionalDirectories: options?.additionalDirectories,
+3
View File
@@ -4,6 +4,8 @@ export type SandboxMode = "read-only" | "workspace-write" | "danger-full-access"
export type ModelReasoningEffort = "minimal" | "low" | "medium" | "high" | "xhigh";
export type WebSearchMode = "disabled" | "cached" | "live";
export type ThreadOptions = {
model?: string;
sandboxMode?: SandboxMode;
@@ -11,6 +13,7 @@ export type ThreadOptions = {
skipGitRepoCheck?: boolean;
modelReasoningEffort?: ModelReasoningEffort;
networkAccessEnabled?: boolean;
webSearchMode?: WebSearchMode;
webSearchEnabled?: boolean;
approvalPolicy?: ApprovalMode;
additionalDirectories?: string[];
+63 -1
View File
@@ -310,7 +310,69 @@ describe("Codex", () => {
const commandArgs = spawnArgs[0];
expect(commandArgs).toBeDefined();
expectPair(commandArgs, ["--config", "features.web_search_request=true"]);
expectPair(commandArgs, ["--config", 'web_search="live"']);
} finally {
restore();
await close();
}
});
it("passes webSearchMode to exec", async () => {
const { url, close } = await startResponsesTestProxy({
statusCode: 200,
responseBodies: [
sse(
responseStarted("response_1"),
assistantMessage("Web search cached", "item_1"),
responseCompleted("response_1"),
),
],
});
const { args: spawnArgs, restore } = codexExecSpy();
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread({
webSearchMode: "cached",
});
await thread.run("test web search mode");
const commandArgs = spawnArgs[0];
expect(commandArgs).toBeDefined();
expectPair(commandArgs, ["--config", 'web_search="cached"']);
} finally {
restore();
await close();
}
});
it("passes webSearchEnabled false to exec", async () => {
const { url, close } = await startResponsesTestProxy({
statusCode: 200,
responseBodies: [
sse(
responseStarted("response_1"),
assistantMessage("Web search disabled", "item_1"),
responseCompleted("response_1"),
),
],
});
const { args: spawnArgs, restore } = codexExecSpy();
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread({
webSearchEnabled: false,
});
await thread.run("test web search disabled");
const commandArgs = spawnArgs[0];
expect(commandArgs).toBeDefined();
expectPair(commandArgs, ["--config", 'web_search="disabled"']);
} finally {
restore();
await close();