mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Forward turn metadata in standalone web search (#32835)
## What changed - Add optional serialized Codex turn metadata to extension tool calls. - Populate it from the current turn context and send it as the `x-codex-turn-metadata` header on standalone web search requests. ## Testing - Extend the app-server web search integration test to verify that client metadata, including the model ID, reaches the standalone search request. GitOrigin-RevId: c0950108d89a1d317ac94ccd23efcee72fffc6f0
This commit is contained in:
1 parent
2e156cbe31
commit
19a000259d
7 files changed
+80
-8
No files matched your search
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -44,6 +45,18 @@ const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn standalone_web_search_round_trips_output() -> Result<()> {
|
async fn standalone_web_search_round_trips_output() -> Result<()> {
|
||||||
let call_id = "web-run-1";
|
let call_id = "web-run-1";
|
||||||
|
let expected_model_id = "model-id-from-search-context";
|
||||||
|
let search_context = json!({
|
||||||
|
"telemetry_attributes": {
|
||||||
|
"model_id": expected_model_id,
|
||||||
|
"model_slug": "mock-model",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
let client_metadata = HashMap::from([(
|
||||||
|
"mcp_request_meta".to_string(),
|
||||||
|
json!({ "openai/search_context": search_context }).to_string(),
|
||||||
|
)]);
|
||||||
let server = responses::start_mock_server().await;
|
let server = responses::start_mock_server().await;
|
||||||
mount_search_response(&server).await;
|
mount_search_response(&server).await;
|
||||||
|
|
||||||
@@ -105,6 +118,7 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
|
|||||||
text: "Search the web".to_string(),
|
text: "Search the web".to_string(),
|
||||||
text_elements: Vec::new(),
|
text_elements: Vec::new(),
|
||||||
}],
|
}],
|
||||||
|
responsesapi_client_metadata: Some(client_metadata.clone()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@@ -144,7 +158,10 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
|
|||||||
"standalone web search should replace hosted web search"
|
"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["model"], json!("mock-model"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
search_body["commands"],
|
search_body["commands"],
|
||||||
@@ -169,6 +186,30 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
|
|||||||
"content": [{"type": "input_text", "text": "Search the web"}],
|
"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")?;
|
||||||
|
let mcp_request_meta = turn_metadata["mcp_request_meta"]
|
||||||
|
.as_str()
|
||||||
|
.context("mcp_request_meta should be a JSON string")?;
|
||||||
|
let mcp_request_meta: Value = serde_json::from_str(mcp_request_meta)
|
||||||
|
.context("mcp_request_meta should contain valid JSON")?;
|
||||||
|
let search_context = mcp_request_meta["openai/search_context"]
|
||||||
|
.as_str()
|
||||||
|
.context("openai/search_context should be a JSON string")?;
|
||||||
|
let search_context: Value = serde_json::from_str(search_context)
|
||||||
|
.context("openai/search_context should contain valid JSON")?;
|
||||||
|
assert_eq!(
|
||||||
|
search_context
|
||||||
|
.pointer("/telemetry_attributes/model_id")
|
||||||
|
.and_then(Value::as_str),
|
||||||
|
Some(expected_model_id)
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
responses::strip_metadata_from_json(requests[1].function_call_output(call_id)),
|
responses::strip_metadata_from_json(requests[1].function_call_output(call_id)),
|
||||||
@@ -285,16 +326,15 @@ fn has_hosted_web_search(body: &Value) -> bool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn search_request_body(server: &MockServer) -> Result<Value> {
|
async fn search_request(server: &MockServer) -> Result<wiremock::Request> {
|
||||||
server
|
let requests = server
|
||||||
.received_requests()
|
.received_requests()
|
||||||
.await
|
.await
|
||||||
.context("failed to fetch received requests")?
|
.context("failed to fetch received requests")?;
|
||||||
|
requests
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|request| request.url.path() == "/api/codex/alpha/search")
|
.find(|request| request.url.path() == "/api/codex/alpha/search")
|
||||||
.context("expected standalone search request")?
|
.context("expected standalone search request")
|
||||||
.body_json()
|
|
||||||
.context("search request body should be JSON")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_config_toml(codex_home: &Path, server_uri: &str) -> std::io::Result<()> {
|
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::ToolSpec;
|
||||||
use codex_tools::TurnItemEmissionFuture;
|
use codex_tools::TurnItemEmissionFuture;
|
||||||
use codex_tools::TurnItemEmitter;
|
use codex_tools::TurnItemEmitter;
|
||||||
|
use codex_utils_string::to_ascii_json_string;
|
||||||
|
|
||||||
use crate::sandboxing::SandboxPermissions;
|
use crate::sandboxing::SandboxPermissions;
|
||||||
use crate::session::session::Session;
|
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::handlers::apply_granted_turn_permissions;
|
||||||
use crate::tools::registry::CoreToolRuntime;
|
use crate::tools::registry::CoreToolRuntime;
|
||||||
use crate::tools::registry::ToolExecutor;
|
use crate::tools::registry::ToolExecutor;
|
||||||
|
use crate::turn_metadata::McpTurnMetadataContext;
|
||||||
|
|
||||||
pub(crate) struct ExtensionToolAdapter(Arc<dyn codex_tools::ToolExecutor<ExtensionToolCall>>);
|
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 {
|
async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall {
|
||||||
let conversation_history =
|
let conversation_history =
|
||||||
ConversationHistory::new(invocation.session.clone_history().await.into_raw_items());
|
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 =
|
let mut environments =
|
||||||
Vec::with_capacity(invocation.step_context.environments.turn_environments.len());
|
Vec::with_capacity(invocation.step_context.environments.turn_environments.len());
|
||||||
for environment in &invocation.step_context.environments.turn_environments {
|
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(),
|
call_id: invocation.call_id.clone(),
|
||||||
tool_name: invocation.tool_name.clone(),
|
tool_name: invocation.tool_name.clone(),
|
||||||
model: invocation.turn.model_info.slug.clone(),
|
model: invocation.turn.model_info.slug.clone(),
|
||||||
|
codex_turn_metadata,
|
||||||
truncation_policy: invocation.turn.model_info.truncation_policy.into(),
|
truncation_policy: invocation.turn.model_info.truncation_policy.into(),
|
||||||
conversation_history,
|
conversation_history,
|
||||||
turn_item_emitter: Arc::new(CoreTurnItemEmitter {
|
turn_item_emitter: Arc::new(CoreTurnItemEmitter {
|
||||||
|
|||||||
@@ -1343,6 +1343,7 @@ fn tool_call(tool_name: &str, call_id: &str, arguments: serde_json::Value) -> To
|
|||||||
call_id: call_id.to_string(),
|
call_id: call_id.to_string(),
|
||||||
tool_name: codex_extension_api::ToolName::plain(tool_name),
|
tool_name: codex_extension_api::ToolName::plain(tool_name),
|
||||||
model: "gpt-test".to_string(),
|
model: "gpt-test".to_string(),
|
||||||
|
codex_turn_metadata: None,
|
||||||
truncation_policy: TruncationPolicy::Bytes(1024),
|
truncation_policy: TruncationPolicy::Bytes(1024),
|
||||||
conversation_history: codex_extension_api::ConversationHistory::default(),
|
conversation_history: codex_extension_api::ConversationHistory::default(),
|
||||||
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
|
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ async fn add_ad_hoc_note_tool_creates_note_file() {
|
|||||||
call_id: "call-1".to_string(),
|
call_id: "call-1".to_string(),
|
||||||
tool_name: memory_tool_name(crate::ADD_AD_HOC_NOTE_TOOL_NAME),
|
tool_name: memory_tool_name(crate::ADD_AD_HOC_NOTE_TOOL_NAME),
|
||||||
model: "gpt-test".to_string(),
|
model: "gpt-test".to_string(),
|
||||||
|
codex_turn_metadata: None,
|
||||||
truncation_policy: TruncationPolicy::Bytes(1024),
|
truncation_policy: TruncationPolicy::Bytes(1024),
|
||||||
conversation_history: codex_extension_api::ConversationHistory::default(),
|
conversation_history: codex_extension_api::ConversationHistory::default(),
|
||||||
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
|
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
|
||||||
@@ -256,6 +257,7 @@ async fn add_ad_hoc_note_tool_rejects_paths_as_filenames() {
|
|||||||
call_id: "call-1".to_string(),
|
call_id: "call-1".to_string(),
|
||||||
tool_name: memory_tool_name(crate::ADD_AD_HOC_NOTE_TOOL_NAME),
|
tool_name: memory_tool_name(crate::ADD_AD_HOC_NOTE_TOOL_NAME),
|
||||||
model: "gpt-test".to_string(),
|
model: "gpt-test".to_string(),
|
||||||
|
codex_turn_metadata: None,
|
||||||
truncation_policy: TruncationPolicy::Bytes(1024),
|
truncation_policy: TruncationPolicy::Bytes(1024),
|
||||||
conversation_history: codex_extension_api::ConversationHistory::default(),
|
conversation_history: codex_extension_api::ConversationHistory::default(),
|
||||||
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
|
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
|
||||||
@@ -301,6 +303,7 @@ async fn read_tool_reads_memory_file() {
|
|||||||
call_id: "call-1".to_string(),
|
call_id: "call-1".to_string(),
|
||||||
tool_name: memory_tool_name(crate::READ_TOOL_NAME),
|
tool_name: memory_tool_name(crate::READ_TOOL_NAME),
|
||||||
model: "gpt-test".to_string(),
|
model: "gpt-test".to_string(),
|
||||||
|
codex_turn_metadata: None,
|
||||||
truncation_policy: TruncationPolicy::Bytes(1024),
|
truncation_policy: TruncationPolicy::Bytes(1024),
|
||||||
conversation_history: codex_extension_api::ConversationHistory::default(),
|
conversation_history: codex_extension_api::ConversationHistory::default(),
|
||||||
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
|
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
|
||||||
@@ -349,6 +352,7 @@ async fn search_tool_accepts_multiple_queries() {
|
|||||||
call_id: "call-1".to_string(),
|
call_id: "call-1".to_string(),
|
||||||
tool_name: memory_tool_name(crate::SEARCH_TOOL_NAME),
|
tool_name: memory_tool_name(crate::SEARCH_TOOL_NAME),
|
||||||
model: "gpt-test".to_string(),
|
model: "gpt-test".to_string(),
|
||||||
|
codex_turn_metadata: None,
|
||||||
truncation_policy: TruncationPolicy::Bytes(1024),
|
truncation_policy: TruncationPolicy::Bytes(1024),
|
||||||
conversation_history: codex_extension_api::ConversationHistory::default(),
|
conversation_history: codex_extension_api::ConversationHistory::default(),
|
||||||
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
|
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
|
||||||
@@ -423,6 +427,7 @@ async fn search_tool_accepts_windowed_all_match_mode() {
|
|||||||
call_id: "call-1".to_string(),
|
call_id: "call-1".to_string(),
|
||||||
tool_name: memory_tool_name(crate::SEARCH_TOOL_NAME),
|
tool_name: memory_tool_name(crate::SEARCH_TOOL_NAME),
|
||||||
model: "gpt-test".to_string(),
|
model: "gpt-test".to_string(),
|
||||||
|
codex_turn_metadata: None,
|
||||||
truncation_policy: TruncationPolicy::Bytes(1024),
|
truncation_policy: TruncationPolicy::Bytes(1024),
|
||||||
conversation_history: codex_extension_api::ConversationHistory::default(),
|
conversation_history: codex_extension_api::ConversationHistory::default(),
|
||||||
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
|
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
|
||||||
@@ -477,6 +482,7 @@ async fn search_tool_rejects_legacy_single_query() {
|
|||||||
call_id: "call-1".to_string(),
|
call_id: "call-1".to_string(),
|
||||||
tool_name: memory_tool_name(crate::SEARCH_TOOL_NAME),
|
tool_name: memory_tool_name(crate::SEARCH_TOOL_NAME),
|
||||||
model: "gpt-test".to_string(),
|
model: "gpt-test".to_string(),
|
||||||
|
codex_turn_metadata: None,
|
||||||
truncation_policy: TruncationPolicy::Bytes(1024),
|
truncation_policy: TruncationPolicy::Bytes(1024),
|
||||||
conversation_history: codex_extension_api::ConversationHistory::default(),
|
conversation_history: codex_extension_api::ConversationHistory::default(),
|
||||||
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
|
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
|
||||||
|
|||||||
@@ -438,6 +438,7 @@ async fn skills_list_truncates_catalog_descriptions_in_tool_output() -> TestResu
|
|||||||
call_id: "call-1".to_string(),
|
call_id: "call-1".to_string(),
|
||||||
tool_name: list_tool.tool_name(),
|
tool_name: list_tool.tool_name(),
|
||||||
model: "gpt-test".to_string(),
|
model: "gpt-test".to_string(),
|
||||||
|
codex_turn_metadata: None,
|
||||||
truncation_policy: TruncationPolicy::Bytes(1_024),
|
truncation_policy: TruncationPolicy::Bytes(1_024),
|
||||||
conversation_history: ConversationHistory::default(),
|
conversation_history: ConversationHistory::default(),
|
||||||
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
|
turn_item_emitter: Arc::new(NoopTurnItemEmitter),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use codex_api::SearchCommands;
|
|||||||
use codex_api::SearchQuery;
|
use codex_api::SearchQuery;
|
||||||
use codex_api::SearchRequest;
|
use codex_api::SearchRequest;
|
||||||
use codex_api::SearchSettings;
|
use codex_api::SearchSettings;
|
||||||
|
use codex_core::X_CODEX_TURN_METADATA_HEADER;
|
||||||
use codex_core::web_search_action_detail;
|
use codex_core::web_search_action_detail;
|
||||||
use codex_extension_api::ExtensionTurnItem;
|
use codex_extension_api::ExtensionTurnItem;
|
||||||
use codex_extension_api::FunctionCallError;
|
use codex_extension_api::FunctionCallError;
|
||||||
@@ -28,6 +29,7 @@ use codex_tools::ResponsesApiNamespaceTool;
|
|||||||
use codex_tools::ToolExposure;
|
use codex_tools::ToolExposure;
|
||||||
use codex_tools::default_namespace_description;
|
use codex_tools::default_namespace_description;
|
||||||
use http::HeaderMap;
|
use http::HeaderMap;
|
||||||
|
use http::HeaderValue;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::history::recent_input;
|
use crate::history::recent_input;
|
||||||
@@ -113,6 +115,12 @@ impl WebSearchTool {
|
|||||||
u64::try_from(call.truncation_policy.token_budget()).unwrap_or(u64::MAX),
|
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
|
call.turn_item_emitter
|
||||||
.emit_started(extension_turn_item(
|
.emit_started(extension_turn_item(
|
||||||
WebSearchItem {
|
WebSearchItem {
|
||||||
@@ -126,7 +134,7 @@ impl WebSearchTool {
|
|||||||
))
|
))
|
||||||
.await;
|
.await;
|
||||||
let response = client
|
let response = client
|
||||||
.search(&request, HeaderMap::new())
|
.search(&request, extra_headers)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| FunctionCallError::Fatal(err.to_string()))?;
|
.map_err(|err| FunctionCallError::Fatal(err.to_string()))?;
|
||||||
let legacy_action = match &command_action {
|
let legacy_action = match &command_action {
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ pub struct ToolCall {
|
|||||||
pub call_id: String,
|
pub call_id: String,
|
||||||
pub tool_name: ToolName,
|
pub tool_name: ToolName,
|
||||||
pub model: String,
|
pub model: String,
|
||||||
|
pub codex_turn_metadata: Option<String>,
|
||||||
pub truncation_policy: TruncationPolicy,
|
pub truncation_policy: TruncationPolicy,
|
||||||
pub conversation_history: ConversationHistory,
|
pub conversation_history: ConversationHistory,
|
||||||
pub turn_item_emitter: Arc<dyn TurnItemEmitter>,
|
pub turn_item_emitter: Arc<dyn TurnItemEmitter>,
|
||||||
@@ -106,6 +107,10 @@ impl std::fmt::Debug for ToolCall {
|
|||||||
.field("call_id", &self.call_id)
|
.field("call_id", &self.call_id)
|
||||||
.field("tool_name", &self.tool_name)
|
.field("tool_name", &self.tool_name)
|
||||||
.field("model", &self.model)
|
.field("model", &self.model)
|
||||||
|
.field(
|
||||||
|
"has_codex_turn_metadata",
|
||||||
|
&self.codex_turn_metadata.is_some(),
|
||||||
|
)
|
||||||
.field("truncation_policy", &self.truncation_policy)
|
.field("truncation_policy", &self.truncation_policy)
|
||||||
.field("conversation_history", &self.conversation_history)
|
.field("conversation_history", &self.conversation_history)
|
||||||
.field("turn_item_emitter", &"<host turn item emitter>")
|
.field("turn_item_emitter", &"<host turn item emitter>")
|
||||||
|
|||||||
Reference in new issue
Block a user