Propagate turn metadata to standalone web search

This commit is contained in:
XMLIU-oai
2026-07-09 21:39:21 +00:00
parent 22781d4001
commit 1518eb8522
4 changed files with 52 additions and 8 deletions
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::path::Path;
use std::time::Duration;
@@ -44,6 +45,10 @@ const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
#[tokio::test]
async fn standalone_web_search_round_trips_output() -> Result<()> {
let call_id = "web-run-1";
let client_metadata = HashMap::from([(
"client-metadata-key".to_string(),
"client-metadata-value".to_string(),
)]);
let server = responses::start_mock_server().await;
mount_search_response(&server).await;
@@ -105,6 +110,7 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
text: "Search the web".to_string(),
text_elements: Vec::new(),
}],
responsesapi_client_metadata: Some(client_metadata.clone()),
..Default::default()
})
.await?;
@@ -144,7 +150,10 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
"standalone web search should replace hosted web search"
);
let search_body = search_request_body(&server).await?;
let search_request = search_request(&server).await?;
let search_body = search_request
.body_json::<Value>()
.context("search request body should be JSON")?;
assert_eq!(search_body["model"], json!("mock-model"));
assert_eq!(
search_body["commands"],
@@ -169,6 +178,18 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
"content": [{"type": "input_text", "text": "Search the web"}],
}))
);
let turn_metadata_header = search_request
.headers
.get("x-codex-turn-metadata")
.context("standalone search should include x-codex-turn-metadata")?
.to_str()
.context("x-codex-turn-metadata should be valid ASCII")?;
let turn_metadata: Value = serde_json::from_str(turn_metadata_header)
.context("x-codex-turn-metadata should be valid JSON")?;
assert_eq!(
turn_metadata["client-metadata-key"],
json!("client-metadata-value")
);
assert_eq!(
responses::strip_metadata_from_json(requests[1].function_call_output(call_id)),
@@ -285,16 +306,15 @@ fn has_hosted_web_search(body: &Value) -> bool {
})
}
async fn search_request_body(server: &MockServer) -> Result<Value> {
server
async fn search_request(server: &MockServer) -> Result<wiremock::Request> {
let requests = server
.received_requests()
.await
.context("failed to fetch received requests")?
.context("failed to fetch received requests")?;
requests
.into_iter()
.find(|request| request.url.path() == "/api/codex/alpha/search")
.context("expected standalone search request")?
.body_json()
.context("search request body should be JSON")
.context("expected standalone search request")
}
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
@@ -13,6 +13,7 @@ use codex_tools::ToolSearchInfo;
use codex_tools::ToolSpec;
use codex_tools::TurnItemEmissionFuture;
use codex_tools::TurnItemEmitter;
use codex_utils_string::to_ascii_json_string;
use crate::sandboxing::SandboxPermissions;
use crate::session::session::Session;
@@ -22,6 +23,7 @@ use crate::tools::context::ToolPayload;
use crate::tools::handlers::apply_granted_turn_permissions;
use crate::tools::registry::CoreToolRuntime;
use crate::tools::registry::ToolExecutor;
use crate::turn_metadata::McpTurnMetadataContext;
pub(crate) struct ExtensionToolAdapter(Arc<dyn codex_tools::ToolExecutor<ExtensionToolCall>>);
@@ -114,6 +116,14 @@ impl TurnItemEmitter for CoreTurnItemEmitter {
async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall {
let conversation_history =
ConversationHistory::new(invocation.session.clone_history().await.into_raw_items());
let codex_turn_metadata = invocation
.turn
.turn_metadata_state
.current_meta_value_for_mcp_request(McpTurnMetadataContext {
model: invocation.turn.model_info.slug.as_str(),
reasoning_effort: invocation.turn.effective_reasoning_effort(),
})
.and_then(|metadata| to_ascii_json_string(&metadata).ok());
let mut environments =
Vec::with_capacity(invocation.step_context.environments.turn_environments.len());
for environment in &invocation.step_context.environments.turn_environments {
@@ -146,6 +156,7 @@ async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall {
call_id: invocation.call_id.clone(),
tool_name: invocation.tool_name.clone(),
model: invocation.turn.model_info.slug.clone(),
codex_turn_metadata,
truncation_policy: invocation.turn.model_info.truncation_policy.into(),
conversation_history,
turn_item_emitter: Arc::new(CoreTurnItemEmitter {
+9 -1
View File
@@ -4,6 +4,7 @@ use codex_api::SearchCommands;
use codex_api::SearchQuery;
use codex_api::SearchRequest;
use codex_api::SearchSettings;
use codex_core::X_CODEX_TURN_METADATA_HEADER;
use codex_core::web_search_action_detail;
use codex_extension_api::ExtensionTurnItem;
use codex_extension_api::FunctionCallError;
@@ -28,6 +29,7 @@ use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ToolExposure;
use codex_tools::default_namespace_description;
use http::HeaderMap;
use http::HeaderValue;
use url::Url;
use crate::history::recent_input;
@@ -113,6 +115,12 @@ impl WebSearchTool {
u64::try_from(call.truncation_policy.token_budget()).unwrap_or(u64::MAX),
),
};
let mut extra_headers = HeaderMap::new();
if let Some(turn_metadata) = call.codex_turn_metadata.as_deref()
&& let Ok(header_value) = HeaderValue::from_str(turn_metadata)
{
extra_headers.insert(X_CODEX_TURN_METADATA_HEADER, header_value);
}
call.turn_item_emitter
.emit_started(extension_turn_item(
WebSearchItem {
@@ -126,7 +134,7 @@ impl WebSearchTool {
))
.await;
let response = client
.search(&request, HeaderMap::new())
.search(&request, extra_headers)
.await
.map_err(|err| FunctionCallError::Fatal(err.to_string()))?;
let legacy_action = match &command_action {
+5
View File
@@ -92,6 +92,7 @@ pub struct ToolCall {
pub call_id: String,
pub tool_name: ToolName,
pub model: String,
pub codex_turn_metadata: Option<String>,
pub truncation_policy: TruncationPolicy,
pub conversation_history: ConversationHistory,
pub turn_item_emitter: Arc<dyn TurnItemEmitter>,
@@ -106,6 +107,10 @@ impl std::fmt::Debug for ToolCall {
.field("call_id", &self.call_id)
.field("tool_name", &self.tool_name)
.field("model", &self.model)
.field(
"has_codex_turn_metadata",
&self.codex_turn_metadata.is_some(),
)
.field("truncation_policy", &self.truncation_policy)
.field("conversation_history", &self.conversation_history)
.field("turn_item_emitter", &"<host turn item emitter>")