Files
codex/prs/bolinfest/PR-2371.md
2025-09-02 15:17:45 -07:00

833 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PR #2371: Add web search tool
- URL: https://github.com/openai/codex/pull/2371
- Author: ReubenNarad
- Created: 2025-08-16 04:06:55 UTC
- Updated: 2025-08-24 18:49:35 UTC
- Changes: +158/-26, Files changed: 16, Commits: 22
## Description
Adds web_search tool, enabling the model to use Responses API web_search tool.
- Disabled by default, enabled by --search flag
- When --search is passed, exposes web_search_request function tool to the model, which triggers user approval. When approved, the model can use the web_search tool for the remainder of the turn
<img width="1033" height="294" alt="image" src="https://github.com/user-attachments/assets/62ac6563-b946-465c-ba5d-9325af28b28f" />
Currently only works for API key login
## Full Diff
```diff
diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs
index 55b200911a..641574074e 100644
--- a/codex-rs/core/src/chat_completions.rs
+++ b/codex-rs/core/src/chat_completions.rs
@@ -623,6 +623,12 @@ where
Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryPartAdded))) => {
continue;
}
+ Poll::Ready(Some(Ok(ResponseEvent::WebSearchCallBegin { .. }))) => {
+ return Poll::Ready(Some(Ok(ResponseEvent::WebSearchCallBegin {
+ call_id: String::new(),
+ query: None,
+ })));
+ }
}
}
}
diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs
index 6206b4264a..ecf1ae7125 100644
--- a/codex-rs/core/src/client.rs
+++ b/codex-rs/core/src/client.rs
@@ -149,7 +149,21 @@ impl ModelClient {
let store = prompt.store && auth_mode != Some(AuthMode::ChatGPT);
let full_instructions = prompt.get_full_instructions(&self.config.model_family);
- let tools_json = create_tools_json_for_responses_api(&prompt.tools)?;
+ let mut tools_json = create_tools_json_for_responses_api(&prompt.tools)?;
+ // ChatGPT backend expects the preview name for web search.
+ if auth_mode == Some(AuthMode::ChatGPT) {
+ for tool in &mut tools_json {
+ if let Some(map) = tool.as_object_mut()
+ && map.get("type").and_then(|v| v.as_str()) == Some("web_search")
+ {
+ map.insert(
+ "type".to_string(),
+ serde_json::Value::String("web_search_preview".to_string()),
+ );
+ }
+ }
+ }
+
let reasoning = create_reasoning_param_for_request(
&self.config.model_family,
self.effort,
@@ -466,7 +480,8 @@ async fn process_sse<S>(
}
};
- trace!("SSE event: {}", sse.data);
+ let raw = sse.data.clone();
+ trace!("SSE event: {}", raw);
let event: SseEvent = match serde_json::from_str(&sse.data) {
Ok(event) => event,
@@ -580,8 +595,24 @@ async fn process_sse<S>(
| "response.in_progress"
| "response.output_item.added"
| "response.output_text.done" => {
- // Currently, we ignore this event, but we handle it
- // separately to skip the logging message in the `other` case.
+ if event.kind == "response.output_item.added"
+ && let Some(item) = event.item.as_ref()
+ {
+ // Detect web_search_call begin and forward a synthetic event upstream.
+ if let Some(ty) = item.get("type").and_then(|v| v.as_str())
+ && ty == "web_search_call"
+ {
+ let call_id = item
+ .get("id")
+ .and_then(|v| v.as_str())
+ .unwrap_or("")
+ .to_string();
+ let ev = ResponseEvent::WebSearchCallBegin { call_id, query: None };
+ if tx_event.send(Ok(ev)).await.is_err() {
+ return;
+ }
+ }
+ }
}
"response.reasoning_summary_part.added" => {
// Boundary between reasoning summary sections (e.g., titles).
@@ -591,7 +622,7 @@ async fn process_sse<S>(
}
}
"response.reasoning_summary_text.done" => {}
- other => debug!(other, "sse event"),
+ _ => {}
}
}
}
diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs
index c320d8d0d7..e2c191f4a5 100644
--- a/codex-rs/core/src/client_common.rs
+++ b/codex-rs/core/src/client_common.rs
@@ -93,6 +93,10 @@ pub enum ResponseEvent {
ReasoningSummaryDelta(String),
ReasoningContentDelta(String),
ReasoningSummaryPartAdded,
+ WebSearchCallBegin {
+ call_id: String,
+ query: Option<String>,
+ },
}
#[derive(Debug, Serialize)]
diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index a7f8e7c30d..e175f55094 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -96,6 +96,7 @@ use crate::protocol::StreamErrorEvent;
use crate::protocol::Submission;
use crate::protocol::TaskCompleteEvent;
use crate::protocol::TurnDiffEvent;
+use crate::protocol::WebSearchBeginEvent;
use crate::rollout::RolloutRecorder;
use crate::safety::SafetyCheck;
use crate::safety::assess_command_safety;
@@ -511,6 +512,7 @@ impl Session {
sandbox_policy.clone(),
config.include_plan_tool,
config.include_apply_patch_tool,
+ config.tools_web_search_request,
config.use_experimental_streamable_shell_tool,
),
user_instructions,
@@ -1096,6 +1098,7 @@ async fn submission_loop(
new_sandbox_policy.clone(),
config.include_plan_tool,
config.include_apply_patch_tool,
+ config.tools_web_search_request,
config.use_experimental_streamable_shell_tool,
);
@@ -1175,6 +1178,7 @@ async fn submission_loop(
sandbox_policy.clone(),
config.include_plan_tool,
config.include_apply_patch_tool,
+ config.tools_web_search_request,
config.use_experimental_streamable_shell_tool,
),
user_instructions: turn_context.user_instructions.clone(),
@@ -1687,6 +1691,7 @@ async fn try_run_turn(
let mut stream = turn_context.client.clone().stream(&prompt).await?;
let mut output = Vec::new();
+
loop {
// Poll the next item from the model stream. We must inspect *both* Ok and Err
// cases so that transient stream failures (e.g., dropped SSE connection before
@@ -1723,6 +1728,16 @@ async fn try_run_turn(
.await?;
output.push(ProcessedResponseItem { item, response });
}
+ ResponseEvent::WebSearchCallBegin { call_id, query } => {
+ let q = query.unwrap_or_else(|| "Searching Web...".to_string());
+ let _ = sess
+ .tx_event
+ .send(Event {
+ id: sub_id.to_string(),
+ msg: EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id, query: q }),
+ })
+ .await;
+ }
ResponseEvent::Completed {
response_id: _,
token_usage,
diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs
index fbf0387a01..98a8fde135 100644
--- a/codex-rs/core/src/config.rs
+++ b/codex-rs/core/src/config.rs
@@ -169,6 +169,8 @@ pub struct Config {
/// model family's default preference.
pub include_apply_patch_tool: bool,
+ pub tools_web_search_request: bool,
+
/// The value for the `originator` header included with Responses API requests.
pub responses_originator_header: String,
@@ -480,6 +482,9 @@ pub struct ConfigToml {
/// If set to `true`, the API key will be signed with the `originator` header.
pub preferred_auth_method: Option<AuthMode>,
+
+ /// Nested tools section for feature toggles
+ pub tools: Option<ToolsToml>,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
@@ -487,6 +492,13 @@ pub struct ProjectConfig {
pub trust_level: Option<String>,
}
+#[derive(Deserialize, Debug, Clone, Default)]
+pub struct ToolsToml {
+ // Renamed from `web_search_request`; keep alias for backwards compatibility.
+ #[serde(default, alias = "web_search_request")]
+ pub web_search: Option<bool>,
+}
+
impl ConfigToml {
/// Derive the effective sandbox policy from the configuration.
fn derive_sandbox_policy(&self, sandbox_mode_override: Option<SandboxMode>) -> SandboxPolicy {
@@ -576,6 +588,7 @@ pub struct ConfigOverrides {
pub include_apply_patch_tool: Option<bool>,
pub disable_response_storage: Option<bool>,
pub show_raw_agent_reasoning: Option<bool>,
+ pub tools_web_search_request: Option<bool>,
}
impl Config {
@@ -602,6 +615,7 @@ impl Config {
include_apply_patch_tool,
disable_response_storage,
show_raw_agent_reasoning,
+ tools_web_search_request: override_tools_web_search_request,
} = overrides;
let config_profile = match config_profile_key.as_ref().or(cfg.profile.as_ref()) {
@@ -640,7 +654,7 @@ impl Config {
})?
.clone();
- let shell_environment_policy = cfg.shell_environment_policy.into();
+ let shell_environment_policy = cfg.shell_environment_policy.clone().into();
let resolved_cwd = {
use std::env;
@@ -661,7 +675,11 @@ impl Config {
}
};
- let history = cfg.history.unwrap_or_default();
+ let history = cfg.history.clone().unwrap_or_default();
+
+ let tools_web_search_request = override_tools_web_search_request
+ .or(cfg.tools.as_ref().and_then(|t| t.web_search))
+ .unwrap_or(false);
let model = model
.or(config_profile.model)
@@ -735,7 +753,7 @@ impl Config {
codex_home,
history,
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
- tui: cfg.tui.unwrap_or_default(),
+ tui: cfg.tui.clone().unwrap_or_default(),
codex_linux_sandbox_exe,
hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false),
@@ -754,12 +772,13 @@ impl Config {
model_verbosity: config_profile.model_verbosity.or(cfg.model_verbosity),
chatgpt_base_url: config_profile
.chatgpt_base_url
- .or(cfg.chatgpt_base_url)
+ .or(cfg.chatgpt_base_url.clone())
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
experimental_resume,
include_plan_tool: include_plan_tool.unwrap_or(false),
include_apply_patch_tool: include_apply_patch_tool.unwrap_or(false),
+ tools_web_search_request,
responses_originator_header,
preferred_auth_method: cfg.preferred_auth_method.unwrap_or(AuthMode::ChatGPT),
use_experimental_streamable_shell_tool: cfg
@@ -1129,6 +1148,7 @@ disable_response_storage = true
base_instructions: None,
include_plan_tool: false,
include_apply_patch_tool: false,
+ tools_web_search_request: false,
responses_originator_header: "codex_cli_rs".to_string(),
preferred_auth_method: AuthMode::ChatGPT,
use_experimental_streamable_shell_tool: false,
@@ -1184,6 +1204,7 @@ disable_response_storage = true
base_instructions: None,
include_plan_tool: false,
include_apply_patch_tool: false,
+ tools_web_search_request: false,
responses_originator_header: "codex_cli_rs".to_string(),
preferred_auth_method: AuthMode::ChatGPT,
use_experimental_streamable_shell_tool: false,
@@ -1254,6 +1275,7 @@ disable_response_storage = true
base_instructions: None,
include_plan_tool: false,
include_apply_patch_tool: false,
+ tools_web_search_request: false,
responses_originator_header: "codex_cli_rs".to_string(),
preferred_auth_method: AuthMode::ChatGPT,
use_experimental_streamable_shell_tool: false,
diff --git a/codex-rs/core/src/openai_tools.rs b/codex-rs/core/src/openai_tools.rs
index 272c901dc2..516a984453 100644
--- a/codex-rs/core/src/openai_tools.rs
+++ b/codex-rs/core/src/openai_tools.rs
@@ -47,6 +47,8 @@ pub(crate) enum OpenAiTool {
Function(ResponsesApiTool),
#[serde(rename = "local_shell")]
LocalShell {},
+ #[serde(rename = "web_search")]
+ WebSearch {},
#[serde(rename = "custom")]
Freeform(FreeformTool),
}
@@ -64,6 +66,7 @@ pub struct ToolsConfig {
pub shell_type: ConfigShellToolType,
pub plan_tool: bool,
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
+ pub web_search_request: bool,
}
impl ToolsConfig {
@@ -73,6 +76,7 @@ impl ToolsConfig {
sandbox_policy: SandboxPolicy,
include_plan_tool: bool,
include_apply_patch_tool: bool,
+ include_web_search_request: bool,
use_streamable_shell_tool: bool,
) -> Self {
let mut shell_type = if use_streamable_shell_tool {
@@ -104,6 +108,7 @@ impl ToolsConfig {
shell_type,
plan_tool: include_plan_tool,
apply_patch_tool_type,
+ web_search_request: include_web_search_request,
}
}
}
@@ -521,6 +526,10 @@ pub(crate) fn get_openai_tools(
}
}
+ if config.web_search_request {
+ tools.push(OpenAiTool::WebSearch {});
+ }
+
if let Some(mcp_tools) = mcp_tools {
for (name, tool) in mcp_tools {
match mcp_tool_to_openai_tool(name.clone(), tool.clone()) {
@@ -549,6 +558,7 @@ mod tests {
.map(|tool| match tool {
OpenAiTool::Function(ResponsesApiTool { name, .. }) => name,
OpenAiTool::LocalShell {} => "local_shell",
+ OpenAiTool::WebSearch {} => "web_search",
OpenAiTool::Freeform(FreeformTool { name, .. }) => name,
})
.collect::<Vec<_>>();
@@ -576,11 +586,12 @@ mod tests {
SandboxPolicy::ReadOnly,
true,
false,
+ true,
/*use_experimental_streamable_shell_tool*/ false,
);
let tools = get_openai_tools(&config, Some(HashMap::new()));
- assert_eq_tool_names(&tools, &["local_shell", "update_plan"]);
+ assert_eq_tool_names(&tools, &["local_shell", "update_plan", "web_search"]);
}
#[test]
@@ -592,11 +603,12 @@ mod tests {
SandboxPolicy::ReadOnly,
true,
false,
+ true,
/*use_experimental_streamable_shell_tool*/ false,
);
let tools = get_openai_tools(&config, Some(HashMap::new()));
- assert_eq_tool_names(&tools, &["shell", "update_plan"]);
+ assert_eq_tool_names(&tools, &["shell", "update_plan", "web_search"]);
}
#[test]
@@ -608,6 +620,7 @@ mod tests {
SandboxPolicy::ReadOnly,
false,
false,
+ true,
/*use_experimental_streamable_shell_tool*/ false,
);
let tools = get_openai_tools(
@@ -631,8 +644,8 @@ mod tests {
"number_property": { "type": "number" },
},
"required": [
- "string_property",
- "number_property"
+ "string_property".to_string(),
+ "number_property".to_string()
],
"additionalProperties": Some(false),
},
@@ -648,10 +661,13 @@ mod tests {
)])),
);
- assert_eq_tool_names(&tools, &["shell", "test_server/do_something_cool"]);
+ assert_eq_tool_names(
+ &tools,
+ &["shell", "web_search", "test_server/do_something_cool"],
+ );
assert_eq!(
- tools[1],
+ tools[2],
OpenAiTool::Function(ResponsesApiTool {
name: "test_server/do_something_cool".to_string(),
parameters: JsonSchema::Object {
@@ -703,6 +719,7 @@ mod tests {
SandboxPolicy::ReadOnly,
false,
false,
+ true,
/*use_experimental_streamable_shell_tool*/ false,
);
@@ -729,10 +746,10 @@ mod tests {
)])),
);
- assert_eq_tool_names(&tools, &["shell", "dash/search"]);
+ assert_eq_tool_names(&tools, &["shell", "web_search", "dash/search"]);
assert_eq!(
- tools[1],
+ tools[2],
OpenAiTool::Function(ResponsesApiTool {
name: "dash/search".to_string(),
parameters: JsonSchema::Object {
@@ -760,6 +777,7 @@ mod tests {
SandboxPolicy::ReadOnly,
false,
false,
+ true,
/*use_experimental_streamable_shell_tool*/ false,
);
@@ -784,9 +802,9 @@ mod tests {
)])),
);
- assert_eq_tool_names(&tools, &["shell", "dash/paginate"]);
+ assert_eq_tool_names(&tools, &["shell", "web_search", "dash/paginate"]);
assert_eq!(
- tools[1],
+ tools[2],
OpenAiTool::Function(ResponsesApiTool {
name: "dash/paginate".to_string(),
parameters: JsonSchema::Object {
@@ -812,6 +830,7 @@ mod tests {
SandboxPolicy::ReadOnly,
false,
false,
+ true,
/*use_experimental_streamable_shell_tool*/ false,
);
@@ -836,9 +855,9 @@ mod tests {
)])),
);
- assert_eq_tool_names(&tools, &["shell", "dash/tags"]);
+ assert_eq_tool_names(&tools, &["shell", "web_search", "dash/tags"]);
assert_eq!(
- tools[1],
+ tools[2],
OpenAiTool::Function(ResponsesApiTool {
name: "dash/tags".to_string(),
parameters: JsonSchema::Object {
@@ -867,6 +886,7 @@ mod tests {
SandboxPolicy::ReadOnly,
false,
false,
+ true,
/*use_experimental_streamable_shell_tool*/ false,
);
@@ -891,9 +911,9 @@ mod tests {
)])),
);
- assert_eq_tool_names(&tools, &["shell", "dash/value"]);
+ assert_eq_tool_names(&tools, &["shell", "web_search", "dash/value"]);
assert_eq!(
- tools[1],
+ tools[2],
OpenAiTool::Function(ResponsesApiTool {
name: "dash/value".to_string(),
parameters: JsonSchema::Object {
diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs
index 0f7e14ea40..cfdba98461 100644
--- a/codex-rs/exec/src/event_processor_with_human_output.rs
+++ b/codex-rs/exec/src/event_processor_with_human_output.rs
@@ -24,6 +24,7 @@ use codex_core::protocol::StreamErrorEvent;
use codex_core::protocol::TaskCompleteEvent;
use codex_core::protocol::TurnAbortReason;
use codex_core::protocol::TurnDiffEvent;
+use codex_core::protocol::WebSearchBeginEvent;
use owo_colors::OwoColorize;
use owo_colors::Style;
use shlex::try_join;
@@ -361,6 +362,9 @@ impl EventProcessor for EventProcessorWithHumanOutput {
}
}
}
+ EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id: _, query }) => {
+ ts_println!(self, "🌐 {query}");
+ }
EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
call_id,
auto_approved,
diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs
index d403cb7960..3de95291d1 100644
--- a/codex-rs/exec/src/lib.rs
+++ b/codex-rs/exec/src/lib.rs
@@ -150,6 +150,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
include_apply_patch_tool: None,
disable_response_storage: oss.then_some(true),
show_raw_agent_reasoning: oss.then_some(true),
+ tools_web_search_request: None,
};
// Parse `-c` overrides.
let cli_kv_overrides = match config_overrides.parse_overrides() {
diff --git a/codex-rs/mcp-server/src/codex_message_processor.rs b/codex-rs/mcp-server/src/codex_message_processor.rs
index 0bbf6ff849..97a3602d70 100644
--- a/codex-rs/mcp-server/src/codex_message_processor.rs
+++ b/codex-rs/mcp-server/src/codex_message_processor.rs
@@ -738,6 +738,7 @@ fn derive_config_from_params(
include_apply_patch_tool,
disable_response_storage: None,
show_raw_agent_reasoning: None,
+ tools_web_search_request: None,
};
let cli_overrides = cli_overrides
diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs
index 5993c10faf..69f07ff223 100644
--- a/codex-rs/mcp-server/src/codex_tool_config.rs
+++ b/codex-rs/mcp-server/src/codex_tool_config.rs
@@ -163,6 +163,7 @@ impl CodexToolCallParam {
include_apply_patch_tool: None,
disable_response_storage: None,
show_raw_agent_reasoning: None,
+ tools_web_search_request: None,
};
let cli_overrides = cli_overrides
diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs
index c6d65bc89d..8480e29c53 100644
--- a/codex-rs/mcp-server/src/codex_tool_runner.rs
+++ b/codex-rs/mcp-server/src/codex_tool_runner.rs
@@ -272,6 +272,7 @@ async fn run_codex_tool_session_inner(
| EventMsg::PatchApplyBegin(_)
| EventMsg::PatchApplyEnd(_)
| EventMsg::TurnDiff(_)
+ | EventMsg::WebSearchBegin(_)
| EventMsg::GetHistoryEntryResponse(_)
| EventMsg::PlanUpdate(_)
| EventMsg::TurnAborted(_)
diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs
index 7e7708b23c..71c1538197 100644
--- a/codex-rs/protocol/src/protocol.rs
+++ b/codex-rs/protocol/src/protocol.rs
@@ -437,6 +437,8 @@ pub enum EventMsg {
McpToolCallEnd(McpToolCallEndEvent),
+ WebSearchBegin(WebSearchBeginEvent),
+
/// Notification that the server is about to execute a command.
ExecCommandBegin(ExecCommandBeginEvent),
@@ -658,6 +660,12 @@ impl McpToolCallEndEvent {
}
}
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct WebSearchBeginEvent {
+ pub call_id: String,
+ pub query: String,
+}
+
/// Response payload for `Op::GetHistory` containing the current session's
/// in-memory transcript.
#[derive(Debug, Clone, Deserialize, Serialize)]
diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs
index 235280be9f..5f48b0e4ed 100644
--- a/codex-rs/tui/src/chatwidget.rs
+++ b/codex-rs/tui/src/chatwidget.rs
@@ -28,6 +28,7 @@ use codex_core::protocol::TaskCompleteEvent;
use codex_core::protocol::TokenUsage;
use codex_core::protocol::TurnAbortReason;
use codex_core::protocol::TurnDiffEvent;
+use codex_core::protocol::WebSearchBeginEvent;
use codex_protocol::parse_command::ParsedCommand;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
@@ -308,6 +309,11 @@ impl ChatWidget {
self.defer_or_handle(|q| q.push_mcp_end(ev), |s| s.handle_mcp_end_now(ev2));
}
+ fn on_web_search_begin(&mut self, ev: WebSearchBeginEvent) {
+ self.flush_answer_stream_with_separator();
+ self.add_to_history(history_cell::new_web_search_call(ev.query));
+ }
+
fn on_get_history_entry_response(
&mut self,
event: codex_core::protocol::GetHistoryEntryResponseEvent,
@@ -839,6 +845,7 @@ impl ChatWidget {
EventMsg::ExecCommandEnd(ev) => self.on_exec_command_end(ev),
EventMsg::McpToolCallBegin(ev) => self.on_mcp_tool_call_begin(ev),
EventMsg::McpToolCallEnd(ev) => self.on_mcp_tool_call_end(ev),
+ EventMsg::WebSearchBegin(ev) => self.on_web_search_begin(ev),
EventMsg::GetHistoryEntryResponse(ev) => self.on_get_history_entry_response(ev),
EventMsg::McpListToolsResponse(ev) => self.on_list_mcp_tools(ev),
EventMsg::ShutdownComplete => self.on_shutdown_complete(),
diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs
index 91ee9cfdc7..8eb6d6b896 100644
--- a/codex-rs/tui/src/cli.rs
+++ b/codex-rs/tui/src/cli.rs
@@ -54,6 +54,10 @@ 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).
+ #[arg(long = "search", default_value_t = false)]
+ pub web_search: bool,
+
#[clap(skip)]
pub config_overrides: CliConfigOverrides,
}
diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs
index 3e51ffd036..0b2af7a100 100644
--- a/codex-rs/tui/src/history_cell.rs
+++ b/codex-rs/tui/src/history_cell.rs
@@ -445,6 +445,12 @@ pub(crate) fn new_active_mcp_tool_call(invocation: McpInvocation) -> PlainHistor
PlainHistoryCell { lines }
}
+pub(crate) fn new_web_search_call(query: String) -> PlainHistoryCell {
+ let lines: Vec<Line<'static>> =
+ vec![Line::from(""), Line::from(vec!["🌐 ".into(), query.into()])];
+ PlainHistoryCell { lines }
+}
+
/// If the first content is an image, return a new cell with the image.
/// TODO(rgwood-dd): Handle images properly even if they're not the first result.
fn try_new_completed_mcp_tool_call_with_image_output(
diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs
index d586c202ac..e5dc5f2abe 100644
--- a/codex-rs/tui/src/lib.rs
+++ b/codex-rs/tui/src/lib.rs
@@ -128,10 +128,11 @@ pub async fn run_main(
include_apply_patch_tool: None,
disable_response_storage: cli.oss.then_some(true),
show_raw_agent_reasoning: cli.oss.then_some(true),
+ tools_web_search_request: cli.web_search.then_some(true),
};
-
- // Parse `-c` overrides from the CLI.
- let cli_kv_overrides = match cli.config_overrides.parse_overrides() {
+ let raw_overrides = cli.config_overrides.raw_overrides.clone();
+ let overrides_cli = codex_common::CliConfigOverrides { raw_overrides };
+ let cli_kv_overrides = match overrides_cli.parse_overrides() {
Ok(v) => v,
#[allow(clippy::print_stderr)]
Err(e) => {
```
## Review Comments
### codex-rs/core/src/client.rs
- Created: 2025-08-24 18:42:52 UTC | Link: https://github.com/openai/codex/pull/2371#discussion_r2296765907
```diff
@@ -149,7 +149,21 @@ impl ModelClient {
let store = prompt.store && auth_mode != Some(AuthMode::ChatGPT);
let full_instructions = prompt.get_full_instructions(&self.config.model_family);
- let tools_json = create_tools_json_for_responses_api(&prompt.tools)?;
+ let mut tools_json = create_tools_json_for_responses_api(&prompt.tools)?;
+ // ChatGPT backend expects the preview name for web search.
+ if auth_mode == Some(AuthMode::ChatGPT) {
+ for tool in &mut tools_json {
+ if let Some(map) = tool.as_object_mut()
+ && map.get("type").and_then(|v| v.as_str()) == Some("web_search")
+ {
+ map.insert(
+ "type".to_string(),
+ serde_json::Value::String("web_search_preview".to_string()),
+ );
+ }
+ }
+ }
```
> Why is this logic added here instead of inside `create_tools_json_for_responses_api()`?
- Created: 2025-08-24 18:44:48 UTC | Link: https://github.com/openai/codex/pull/2371#discussion_r2296766370
```diff
@@ -580,8 +595,24 @@ async fn process_sse<S>(
| "response.in_progress"
| "response.output_item.added"
| "response.output_text.done" => {
- // Currently, we ignore this event, but we handle it
- // separately to skip the logging message in the `other` case.
+ if event.kind == "response.output_item.added"
```
> Why is this here instead of being its own case on line 596? This whole thing starts with `match event.kind.as_str()`?
### codex-rs/core/src/config.rs
- Created: 2025-08-24 18:45:41 UTC | Link: https://github.com/openai/codex/pull/2371#discussion_r2296766581
```diff
@@ -480,13 +482,23 @@ pub struct ConfigToml {
/// If set to `true`, the API key will be signed with the `originator` header.
pub preferred_auth_method: Option<AuthMode>,
+
+ /// Nested tools section for feature toggles
+ pub tools: Option<ToolsToml>,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct ProjectConfig {
pub trust_level: Option<String>,
}
+#[derive(Deserialize, Debug, Clone, Default)]
+pub struct ToolsToml {
+ // Renamed from `web_search_request`; keep alias for backwards compatibility.
```
> Backwards compatibility? Isn't this new code?
- Created: 2025-08-24 18:46:48 UTC | Link: https://github.com/openai/codex/pull/2371#discussion_r2296766878
```diff
@@ -640,7 +654,7 @@ impl Config {
})?
.clone();
- let shell_environment_policy = cfg.shell_environment_policy.into();
+ let shell_environment_policy = cfg.shell_environment_policy.clone().into();
```
> Why are all these `clone()` calls being added?
### codex-rs/core/src/openai_tools.rs
- Created: 2025-08-24 18:47:43 UTC | Link: https://github.com/openai/codex/pull/2371#discussion_r2296767044
```diff
@@ -576,11 +586,12 @@ mod tests {
SandboxPolicy::ReadOnly,
true,
false,
+ true,
```
> `ToolsConfig::new()` really needs to take a struct: this is no longer comprehensible...
- Created: 2025-08-24 18:48:00 UTC | Link: https://github.com/openai/codex/pull/2371#discussion_r2296767107
```diff
@@ -631,8 +644,8 @@ mod tests {
"number_property": { "type": "number" },
},
"required": [
- "string_property",
- "number_property"
+ "string_property".to_string(),
```
> Why did this change?
### codex-rs/protocol/src/protocol.rs
- Created: 2025-08-24 18:48:41 UTC | Link: https://github.com/openai/codex/pull/2371#discussion_r2296767253
```diff
@@ -437,6 +437,8 @@ pub enum EventMsg {
McpToolCallEnd(McpToolCallEndEvent),
+ WebSearchBegin(WebSearchBeginEvent),
```
> There's a begin but no end?
### codex-rs/tui/src/lib.rs
- Created: 2025-08-24 18:49:32 UTC | Link: https://github.com/openai/codex/pull/2371#discussion_r2296767615
```diff
@@ -128,10 +128,11 @@ pub async fn run_main(
include_apply_patch_tool: None,
disable_response_storage: cli.oss.then_some(true),
show_raw_agent_reasoning: cli.oss.then_some(true),
+ tools_web_search_request: cli.web_search.then_some(true),
};
-
- // Parse `-c` overrides from the CLI.
- let cli_kv_overrides = match cli.config_overrides.parse_overrides() {
+ let raw_overrides = cli.config_overrides.raw_overrides.clone();
```
> Why did this change?