mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: search_tool migrate to bring you own tool of Responses API (#14274)
## Why to support a new bring your own search tool in Responses API(https://developers.openai.com/api/docs/guides/tools-tool-search#client-executed-tool-search) we migrating our bm25 search tool to use official way to execute search on client and communicate additional tools to the model. ## What - replace the legacy `search_tool_bm25` flow with client-executed `tool_search` - add protocol, SSE, history, and normalization support for `tool_search_call` and `tool_search_output` - return namespaced Codex Apps search results and wire namespaced follow-up tool calls back into MCP dispatch
This commit is contained in:
@@ -376,6 +376,8 @@ impl ContextManager {
|
||||
| ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
| ResponseItem::ToolSearchCall { .. }
|
||||
| ResponseItem::ToolSearchOutput { .. }
|
||||
| ResponseItem::WebSearchCall { .. }
|
||||
| ResponseItem::ImageGenerationCall { .. }
|
||||
| ResponseItem::CustomToolCall { .. }
|
||||
@@ -413,6 +415,8 @@ fn is_api_message(message: &ResponseItem) -> bool {
|
||||
ResponseItem::Message { role, .. } => role.as_str() != "system",
|
||||
ResponseItem::FunctionCallOutput { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
| ResponseItem::ToolSearchCall { .. }
|
||||
| ResponseItem::ToolSearchOutput { .. }
|
||||
| ResponseItem::CustomToolCall { .. }
|
||||
| ResponseItem::CustomToolCallOutput { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
@@ -605,12 +609,14 @@ fn is_model_generated_item(item: &ResponseItem) -> bool {
|
||||
ResponseItem::Message { role, .. } => role == "assistant",
|
||||
ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
| ResponseItem::ToolSearchCall { .. }
|
||||
| ResponseItem::WebSearchCall { .. }
|
||||
| ResponseItem::ImageGenerationCall { .. }
|
||||
| ResponseItem::CustomToolCall { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::Compaction { .. } => true,
|
||||
ResponseItem::FunctionCallOutput { .. }
|
||||
| ResponseItem::ToolSearchOutput { .. }
|
||||
| ResponseItem::CustomToolCallOutput { .. }
|
||||
| ResponseItem::GhostSnapshot { .. }
|
||||
| ResponseItem::Other => false,
|
||||
@@ -620,7 +626,9 @@ fn is_model_generated_item(item: &ResponseItem) -> bool {
|
||||
pub(crate) fn is_codex_generated_item(item: &ResponseItem) -> bool {
|
||||
matches!(
|
||||
item,
|
||||
ResponseItem::FunctionCallOutput { .. } | ResponseItem::CustomToolCallOutput { .. }
|
||||
ResponseItem::FunctionCallOutput { .. }
|
||||
| ResponseItem::ToolSearchOutput { .. }
|
||||
| ResponseItem::CustomToolCallOutput { .. }
|
||||
) || matches!(item, ResponseItem::Message { role, .. } if role == "developer")
|
||||
}
|
||||
|
||||
|
||||
@@ -271,6 +271,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
|
||||
ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "view_image".to_string(),
|
||||
namespace: None,
|
||||
arguments: "{}".to_string(),
|
||||
call_id: "call-1".to_string(),
|
||||
},
|
||||
@@ -332,6 +333,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() {
|
||||
ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "view_image".to_string(),
|
||||
namespace: None,
|
||||
arguments: "{}".to_string(),
|
||||
call_id: "call-1".to_string(),
|
||||
},
|
||||
@@ -547,6 +549,7 @@ fn remove_first_item_removes_matching_output_for_function_call() {
|
||||
ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "do_it".to_string(),
|
||||
namespace: None,
|
||||
arguments: "{}".to_string(),
|
||||
call_id: "call-1".to_string(),
|
||||
},
|
||||
@@ -570,6 +573,7 @@ fn remove_first_item_removes_matching_call_for_output() {
|
||||
ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "do_it".to_string(),
|
||||
namespace: None,
|
||||
arguments: "{}".to_string(),
|
||||
call_id: "call-2".to_string(),
|
||||
},
|
||||
@@ -586,6 +590,7 @@ fn remove_last_item_removes_matching_call_for_output() {
|
||||
ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "do_it".to_string(),
|
||||
namespace: None,
|
||||
arguments: "{}".to_string(),
|
||||
call_id: "call-delete-last".to_string(),
|
||||
},
|
||||
@@ -1059,6 +1064,7 @@ fn normalize_adds_missing_output_for_function_call() {
|
||||
let items = vec![ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "do_it".to_string(),
|
||||
namespace: None,
|
||||
arguments: "{}".to_string(),
|
||||
call_id: "call-x".to_string(),
|
||||
}];
|
||||
@@ -1072,6 +1078,7 @@ fn normalize_adds_missing_output_for_function_call() {
|
||||
ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "do_it".to_string(),
|
||||
namespace: None,
|
||||
arguments: "{}".to_string(),
|
||||
call_id: "call-x".to_string(),
|
||||
},
|
||||
@@ -1193,6 +1200,7 @@ fn normalize_mixed_inserts_and_removals() {
|
||||
ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "f1".to_string(),
|
||||
namespace: None,
|
||||
arguments: "{}".to_string(),
|
||||
call_id: "c1".to_string(),
|
||||
},
|
||||
@@ -1233,6 +1241,7 @@ fn normalize_mixed_inserts_and_removals() {
|
||||
ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "f1".to_string(),
|
||||
namespace: None,
|
||||
arguments: "{}".to_string(),
|
||||
call_id: "c1".to_string(),
|
||||
},
|
||||
@@ -1276,6 +1285,7 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() {
|
||||
let items = vec![ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "do_it".to_string(),
|
||||
namespace: None,
|
||||
arguments: "{}".to_string(),
|
||||
call_id: "call-x".to_string(),
|
||||
}];
|
||||
@@ -1287,6 +1297,7 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() {
|
||||
ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "do_it".to_string(),
|
||||
namespace: None,
|
||||
arguments: "{}".to_string(),
|
||||
call_id: "call-x".to_string(),
|
||||
},
|
||||
@@ -1298,6 +1309,39 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_adds_missing_output_for_tool_search_call() {
|
||||
let items = vec![ResponseItem::ToolSearchCall {
|
||||
id: None,
|
||||
call_id: Some("search-call-x".to_string()),
|
||||
status: Some("completed".to_string()),
|
||||
execution: "client".to_string(),
|
||||
arguments: "{}".into(),
|
||||
}];
|
||||
let mut h = create_history_with_items(items);
|
||||
|
||||
h.normalize_history(&default_input_modalities());
|
||||
|
||||
assert_eq!(
|
||||
h.raw_items(),
|
||||
vec![
|
||||
ResponseItem::ToolSearchCall {
|
||||
id: None,
|
||||
call_id: Some("search-call-x".to_string()),
|
||||
status: Some("completed".to_string()),
|
||||
execution: "client".to_string(),
|
||||
arguments: "{}".into(),
|
||||
},
|
||||
ResponseItem::ToolSearchOutput {
|
||||
call_id: Some("search-call-x".to_string()),
|
||||
status: "completed".to_string(),
|
||||
execution: "client".to_string(),
|
||||
tools: Vec::new(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[test]
|
||||
#[should_panic]
|
||||
@@ -1357,6 +1401,59 @@ fn normalize_removes_orphan_custom_tool_call_output_panics_in_debug() {
|
||||
h.normalize_history(&default_input_modalities());
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[test]
|
||||
fn normalize_removes_orphan_client_tool_search_output() {
|
||||
let items = vec![ResponseItem::ToolSearchOutput {
|
||||
call_id: Some("orphan-search".to_string()),
|
||||
status: "completed".to_string(),
|
||||
execution: "client".to_string(),
|
||||
tools: Vec::new(),
|
||||
}];
|
||||
let mut h = create_history_with_items(items);
|
||||
|
||||
h.normalize_history(&default_input_modalities());
|
||||
|
||||
assert_eq!(h.raw_items(), vec![]);
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn normalize_removes_orphan_client_tool_search_output_panics_in_debug() {
|
||||
let items = vec![ResponseItem::ToolSearchOutput {
|
||||
call_id: Some("orphan-search".to_string()),
|
||||
status: "completed".to_string(),
|
||||
execution: "client".to_string(),
|
||||
tools: Vec::new(),
|
||||
}];
|
||||
let mut h = create_history_with_items(items);
|
||||
h.normalize_history(&default_input_modalities());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_keeps_server_tool_search_output_without_matching_call() {
|
||||
let items = vec![ResponseItem::ToolSearchOutput {
|
||||
call_id: Some("server-search".to_string()),
|
||||
status: "completed".to_string(),
|
||||
execution: "server".to_string(),
|
||||
tools: Vec::new(),
|
||||
}];
|
||||
let mut h = create_history_with_items(items);
|
||||
|
||||
h.normalize_history(&default_input_modalities());
|
||||
|
||||
assert_eq!(
|
||||
h.raw_items(),
|
||||
vec![ResponseItem::ToolSearchOutput {
|
||||
call_id: Some("server-search".to_string()),
|
||||
status: "completed".to_string(),
|
||||
execution: "server".to_string(),
|
||||
tools: Vec::new(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[test]
|
||||
#[should_panic]
|
||||
@@ -1365,6 +1462,7 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() {
|
||||
ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "f1".to_string(),
|
||||
namespace: None,
|
||||
arguments: "{}".to_string(),
|
||||
call_id: "c1".to_string(),
|
||||
},
|
||||
|
||||
@@ -38,6 +38,31 @@ pub(crate) fn ensure_call_outputs_present(items: &mut Vec<ResponseItem>) {
|
||||
));
|
||||
}
|
||||
}
|
||||
ResponseItem::ToolSearchCall {
|
||||
call_id: Some(call_id),
|
||||
..
|
||||
} => {
|
||||
let has_output = items.iter().any(|i| match i {
|
||||
ResponseItem::ToolSearchOutput {
|
||||
call_id: Some(existing),
|
||||
..
|
||||
} => existing == call_id,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
if !has_output {
|
||||
info!("Tool search output is missing for call id: {call_id}");
|
||||
missing_outputs_to_insert.push((
|
||||
idx,
|
||||
ResponseItem::ToolSearchOutput {
|
||||
call_id: Some(call_id.clone()),
|
||||
status: "completed".to_string(),
|
||||
execution: "client".to_string(),
|
||||
tools: Vec::new(),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
ResponseItem::CustomToolCall { call_id, .. } => {
|
||||
let has_output = items.iter().any(|i| match i {
|
||||
ResponseItem::CustomToolCallOutput {
|
||||
@@ -102,6 +127,17 @@ pub(crate) fn remove_orphan_outputs(items: &mut Vec<ResponseItem>) {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let tool_search_call_ids: HashSet<String> = items
|
||||
.iter()
|
||||
.filter_map(|i| match i {
|
||||
ResponseItem::ToolSearchCall {
|
||||
call_id: Some(call_id),
|
||||
..
|
||||
} => Some(call_id.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let local_shell_call_ids: HashSet<String> = items
|
||||
.iter()
|
||||
.filter_map(|i| match i {
|
||||
@@ -141,6 +177,18 @@ pub(crate) fn remove_orphan_outputs(items: &mut Vec<ResponseItem>) {
|
||||
}
|
||||
has_match
|
||||
}
|
||||
ResponseItem::ToolSearchOutput { execution, .. } if execution == "server" => true,
|
||||
ResponseItem::ToolSearchOutput {
|
||||
call_id: Some(call_id),
|
||||
..
|
||||
} => {
|
||||
let has_match = tool_search_call_ids.contains(call_id);
|
||||
if !has_match {
|
||||
error_or_panic(format!("Orphan tool search output for call id: {call_id}"));
|
||||
}
|
||||
has_match
|
||||
}
|
||||
ResponseItem::ToolSearchOutput { call_id: None, .. } => true,
|
||||
_ => true,
|
||||
});
|
||||
}
|
||||
@@ -168,6 +216,37 @@ pub(crate) fn remove_corresponding_for(items: &mut Vec<ResponseItem>, item: &Res
|
||||
items.remove(pos);
|
||||
}
|
||||
}
|
||||
ResponseItem::ToolSearchCall {
|
||||
call_id: Some(call_id),
|
||||
..
|
||||
} => {
|
||||
remove_first_matching(items, |i| {
|
||||
matches!(
|
||||
i,
|
||||
ResponseItem::ToolSearchOutput {
|
||||
call_id: Some(existing),
|
||||
..
|
||||
} if existing == call_id
|
||||
)
|
||||
});
|
||||
}
|
||||
ResponseItem::ToolSearchOutput {
|
||||
call_id: Some(call_id),
|
||||
..
|
||||
} => {
|
||||
remove_first_matching(
|
||||
items,
|
||||
|i| {
|
||||
matches!(
|
||||
i,
|
||||
ResponseItem::ToolSearchCall {
|
||||
call_id: Some(existing),
|
||||
..
|
||||
} if existing == call_id
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
ResponseItem::CustomToolCall { call_id, .. } => {
|
||||
remove_first_matching(items, |i| {
|
||||
matches!(
|
||||
|
||||
Reference in New Issue
Block a user