Following up on #2371 post commit feedback (#2852)

- Introduce websearch end to complement the begin 
- Moves the logic of adding the sebsearch tool to
create_tools_json_for_responses_api
- Making it the client responsibility to toggle the tool on or off 
- Other misc in #2371 post commit feedback
- Show the query:

<img width="1392" height="151" alt="image"
src="https://github.com/user-attachments/assets/8457f1a6-f851-44cf-bcca-0d4fe460ce89"
/>
This commit is contained in:
Ahmed Ibrahim
2025-08-28 19:24:38 -07:00
committed by GitHub
parent b8e8454b3f
commit 9dbe7284d2
13 changed files with 89 additions and 49 deletions

View File

@@ -129,7 +129,9 @@ pub(crate) async fn stream_chat_completions(
"content": output,
}));
}
ResponseItem::Reasoning { .. } | ResponseItem::Other => {
ResponseItem::Reasoning { .. }
| ResponseItem::WebSearchCall { .. }
| ResponseItem::Other => {
// Omit these items from the conversation history.
continue;
}
@@ -623,11 +625,8 @@ 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,
})));
Poll::Ready(Some(Ok(ResponseEvent::WebSearchCallBegin { call_id }))) => {
return Poll::Ready(Some(Ok(ResponseEvent::WebSearchCallBegin { call_id })));
}
}
}

View File

@@ -160,21 +160,7 @@ impl ModelClient {
let store = prompt.store && auth_mode != Some(AuthMode::ChatGPT);
let full_instructions = prompt.get_full_instructions(&self.config.model_family);
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 tools_json = create_tools_json_for_responses_api(&prompt.tools)?;
let reasoning = create_reasoning_param_for_request(
&self.config.model_family,
self.effort,
@@ -607,11 +593,9 @@ async fn process_sse<S>(
| "response.custom_tool_call_input.delta"
| "response.custom_tool_call_input.done" // also emitted as response.output_item.done
| "response.in_progress"
| "response.output_item.added"
| "response.output_text.done" => {
if event.kind == "response.output_item.added"
&& let Some(item) = event.item.as_ref()
{
| "response.output_text.done" => {}
"response.output_item.added" => {
if 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"
@@ -621,7 +605,7 @@ async fn process_sse<S>(
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let ev = ResponseEvent::WebSearchCallBegin { call_id, query: None };
let ev = ResponseEvent::WebSearchCallBegin { call_id };
if tx_event.send(Ok(ev)).await.is_err() {
return;
}

View File

@@ -95,7 +95,6 @@ pub enum ResponseEvent {
ReasoningSummaryPartAdded,
WebSearchCallBegin {
call_id: String,
query: Option<String>,
},
}

View File

@@ -101,6 +101,7 @@ use crate::protocol::Submission;
use crate::protocol::TaskCompleteEvent;
use crate::protocol::TurnDiffEvent;
use crate::protocol::WebSearchBeginEvent;
use crate::protocol::WebSearchEndEvent;
use crate::rollout::RolloutRecorder;
use crate::safety::SafetyCheck;
use crate::safety::assess_command_safety;
@@ -120,6 +121,7 @@ use codex_protocol::models::ReasoningItemReasoningSummary;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::models::ShellToolCallParams;
use codex_protocol::models::WebSearchAction;
// A convenience extension trait for acquiring mutex locks where poisoning is
// unrecoverable and should abort the program. This avoids scattered `.unwrap()`
@@ -1769,13 +1771,12 @@ 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());
ResponseEvent::WebSearchCallBegin { call_id } => {
let _ = sess
.tx_event
.send(Event {
id: sub_id.to_string(),
msg: EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id, query: q }),
msg: EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id }),
})
.await;
}
@@ -2074,6 +2075,17 @@ async fn handle_response_item(
debug!("unexpected CustomToolCallOutput from stream");
None
}
ResponseItem::WebSearchCall { id, action, .. } => {
if let WebSearchAction::Search { query } = action {
let call_id = id.unwrap_or_else(|| "".to_string());
let event = Event {
id: sub_id.to_string(),
msg: EventMsg::WebSearchEnd(WebSearchEndEvent { call_id, query }),
};
sess.tx_event.send(event).await.ok();
}
None
}
ResponseItem::Other => None,
};
Ok(output)

View File

@@ -506,7 +506,6 @@ pub struct ProjectConfig {
#[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>,
@@ -672,7 +671,7 @@ impl Config {
})?
.clone();
let shell_environment_policy = cfg.shell_environment_policy.clone().into();
let shell_environment_policy = cfg.shell_environment_policy.into();
let resolved_cwd = {
use std::env;
@@ -693,7 +692,7 @@ impl Config {
}
};
let history = cfg.history.clone().unwrap_or_default();
let history = cfg.history.unwrap_or_default();
let tools_web_search_request = override_tools_web_search_request
.or(cfg.tools.as_ref().and_then(|t| t.web_search))
@@ -775,7 +774,7 @@ impl Config {
codex_home,
history,
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
tui: cfg.tui.clone().unwrap_or_default(),
tui: cfg.tui.unwrap_or_default(),
codex_linux_sandbox_exe,
hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false),
@@ -794,7 +793,7 @@ 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.clone())
.or(cfg.chatgpt_base_url)
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
experimental_resume,

View File

@@ -72,7 +72,7 @@ fn is_api_message(message: &ResponseItem) -> bool {
| ResponseItem::CustomToolCallOutput { .. }
| ResponseItem::LocalShellCall { .. }
| ResponseItem::Reasoning { .. } => true,
ResponseItem::Other => false,
ResponseItem::WebSearchCall { .. } | ResponseItem::Other => false,
}
}

View File

@@ -47,7 +47,9 @@ pub(crate) enum OpenAiTool {
Function(ResponsesApiTool),
#[serde(rename = "local_shell")]
LocalShell {},
#[serde(rename = "web_search")]
// TODO: Understand why we get an error on web_search although the API docs say it's supported.
// https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses#:~:text=%7B%20type%3A%20%22web_search%22%20%7D%2C
#[serde(rename = "web_search_preview")]
WebSearch {},
#[serde(rename = "custom")]
Freeform(FreeformTool),
@@ -335,12 +337,12 @@ pub fn create_tools_json_for_responses_api(
let mut tools_json = Vec::new();
for tool in tools {
tools_json.push(serde_json::to_value(tool)?);
let json = serde_json::to_value(tool)?;
tools_json.push(json);
}
Ok(tools_json)
}
/// Returns JSON values that are compatible with Function Calling in the
/// Chat Completions API:
/// https://platform.openai.com/docs/guides/function-calling?api-mode=chat
@@ -702,8 +704,8 @@ mod tests {
"number_property": { "type": "number" },
},
"required": [
"string_property".to_string(),
"number_property".to_string()
"string_property",
"number_property",
],
"additionalProperties": Some(false),
},

View File

@@ -135,7 +135,7 @@ impl RolloutRecorder {
| ResponseItem::CustomToolCall { .. }
| ResponseItem::CustomToolCallOutput { .. }
| ResponseItem::Reasoning { .. } => filtered.push(item.clone()),
ResponseItem::Other => {
ResponseItem::WebSearchCall { .. } | ResponseItem::Other => {
// These should never be serialized.
continue;
}
@@ -199,7 +199,7 @@ impl RolloutRecorder {
| ResponseItem::CustomToolCall { .. }
| ResponseItem::CustomToolCallOutput { .. }
| ResponseItem::Reasoning { .. } => items.push(item),
ResponseItem::Other => {}
ResponseItem::WebSearchCall { .. } | ResponseItem::Other => {}
},
Err(e) => {
warn!("failed to parse item: {v:?}, error: {e}");
@@ -326,7 +326,7 @@ async fn rollout_writer(
| ResponseItem::Reasoning { .. } => {
writer.write_line(&item).await?;
}
ResponseItem::Other => {}
ResponseItem::WebSearchCall { .. } | ResponseItem::Other => {}
}
}
}