mirror of
https://github.com/openai/codex.git
synced 2026-04-28 16:45:54 +00:00
833 lines
32 KiB
Markdown
833 lines
32 KiB
Markdown
# 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 per‑call 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? |