mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Forward turn metadata to image generation and edit requests (#54058)
## Why Native extensions need configured Responses metadata for request attribution, but extension calls used the MCP metadata template, which omits it. Image requests also did not forward the turn metadata to the backend. ## What changed Use the Responses metadata template for native extension calls while continuing to exclude tool inventory. Forward the resulting metadata in the `x-codex-turn-metadata` header on image generation and edit requests. ## Testing Extend metadata tests to cover configured values in extension calls and app-server tests to verify `display_name` and `turn_trigger` on image generation and edit requests, including a Unicode display name. GitOrigin-RevId: a2ffaa4d15259c662917612391b8717ee34b9c6c
This commit is contained in:
@@ -119,10 +119,27 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model(
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build_initialized_with_timeout(DEFAULT_READ_TIMEOUT)
|
||||
.await?;
|
||||
let turn_id = start_image_generation_turn(
|
||||
let turn_id = start_turn(
|
||||
&mut mcp,
|
||||
ThreadStartParams {
|
||||
service_name: Some("chatgpt_cca".to_string()),
|
||||
config: Some(
|
||||
[(
|
||||
"responses_api_metadata".to_string(),
|
||||
json!({
|
||||
"display_name": "Image test 🚀",
|
||||
}),
|
||||
)]
|
||||
.into(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
TurnStartParams {
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "Generate an image".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
turn_trigger: Some("page_prompt".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -183,6 +200,15 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model(
|
||||
"chatgpt_cca"
|
||||
);
|
||||
assert_image_turn_id_header(&image_request, &turn_id)?;
|
||||
let metadata = serde_json::from_str::<serde_json::Value>(
|
||||
image_request
|
||||
.headers
|
||||
.get("x-codex-turn-metadata")
|
||||
.context("image request should include the current turn metadata")?
|
||||
.to_str()?,
|
||||
)?;
|
||||
assert_eq!(metadata["display_name"], "Image test 🚀");
|
||||
assert_eq!(metadata["turn_trigger"], "page_prompt");
|
||||
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
@@ -971,10 +997,13 @@ async fn start_image_generation_turn(
|
||||
start_turn(
|
||||
mcp,
|
||||
thread_start_params,
|
||||
vec![V2UserInput::Text {
|
||||
text: "Generate an image".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
TurnStartParams {
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "Generate an image".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1025,9 +1054,22 @@ async fn run_image_edit_test(
|
||||
&mut mcp,
|
||||
ThreadStartParams {
|
||||
service_name: Some("chatgpt_cca".to_string()),
|
||||
config: Some(
|
||||
[(
|
||||
"responses_api_metadata".to_string(),
|
||||
json!({
|
||||
"display_name": "Image edit test",
|
||||
}),
|
||||
)]
|
||||
.into(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
TurnStartParams {
|
||||
input,
|
||||
turn_trigger: Some("page_prompt".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
input,
|
||||
)
|
||||
.await?;
|
||||
let completed = timeout(
|
||||
@@ -1063,6 +1105,15 @@ async fn run_image_edit_test(
|
||||
"chatgpt_cca"
|
||||
);
|
||||
assert_image_turn_id_header(image_request, &turn_id)?;
|
||||
let metadata = serde_json::from_str::<serde_json::Value>(
|
||||
image_request
|
||||
.headers
|
||||
.get("x-codex-turn-metadata")
|
||||
.context("image edit request should include the current turn metadata")?
|
||||
.to_str()?,
|
||||
)?;
|
||||
assert_eq!(metadata["display_name"], "Image edit test");
|
||||
assert_eq!(metadata["turn_trigger"], "page_prompt");
|
||||
Ok((
|
||||
image_request.body_json::<serde_json::Value>()?,
|
||||
completed_image,
|
||||
@@ -1084,7 +1135,7 @@ fn assert_image_turn_id_header(request: &wiremock::Request, expected_turn_id: &s
|
||||
async fn start_turn(
|
||||
mcp: &mut TestAppServer,
|
||||
thread_start_params: ThreadStartParams,
|
||||
input: Vec<V2UserInput>,
|
||||
turn_start_params: TurnStartParams,
|
||||
) -> Result<String> {
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request_with_auto_env(thread_start_params)
|
||||
@@ -1095,9 +1146,7 @@ async fn start_turn(
|
||||
let turn_req = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id,
|
||||
client_user_message_id: None,
|
||||
input,
|
||||
..Default::default()
|
||||
..turn_start_params
|
||||
})
|
||||
.await?;
|
||||
let TurnStartResponse { turn } =
|
||||
|
||||
@@ -195,7 +195,7 @@ async fn to_extension_call<'a>(
|
||||
let codex_turn_metadata = invocation
|
||||
.turn
|
||||
.turn_metadata_state
|
||||
.current_meta_value_for_mcp_request(ExecutionMetadata::from_settings(
|
||||
.current_meta_value_for_extension_request(ExecutionMetadata::from_settings(
|
||||
&invocation.step_context.settings,
|
||||
))
|
||||
.and_then(|metadata| to_ascii_json_string(&metadata).ok());
|
||||
|
||||
@@ -276,7 +276,23 @@ impl TurnMetadataState {
|
||||
&self,
|
||||
execution: ExecutionMetadata<'_>,
|
||||
) -> Option<serde_json::Value> {
|
||||
let mut responses_metadata = self.mcp_metadata_template();
|
||||
self.current_meta_value_for_tool_request(self.mcp_metadata_template(), execution)
|
||||
}
|
||||
|
||||
pub(crate) fn current_meta_value_for_extension_request(
|
||||
&self,
|
||||
execution: ExecutionMetadata<'_>,
|
||||
) -> Option<serde_json::Value> {
|
||||
// Native extensions need configured Responses metadata for request attribution,
|
||||
// which the MCP template omits for external servers.
|
||||
self.current_meta_value_for_tool_request(self.responses_metadata_template(), execution)
|
||||
}
|
||||
|
||||
fn current_meta_value_for_tool_request(
|
||||
&self,
|
||||
mut responses_metadata: CodexResponsesMetadata,
|
||||
execution: ExecutionMetadata<'_>,
|
||||
) -> Option<serde_json::Value> {
|
||||
if responses_metadata
|
||||
.extra
|
||||
.get("misalignment_override")
|
||||
@@ -286,7 +302,7 @@ impl TurnMetadataState {
|
||||
responses_metadata.extra.remove("misalignment_override");
|
||||
}
|
||||
execution.apply_to(&mut responses_metadata);
|
||||
// Never serialize harness-owned tool inventory for external MCP servers.
|
||||
// Never serialize harness-owned tool inventory for tool requests.
|
||||
responses_metadata.tool_namespaces_info = None;
|
||||
let Value::Object(mut metadata) = responses_metadata.turn_metadata_value()? else {
|
||||
return None;
|
||||
|
||||
@@ -972,6 +972,14 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
assert!(meta.get(WINDOW_ID_KEY).is_none());
|
||||
assert!(meta.get("codex_security_surface").is_none());
|
||||
assert!(meta.get(ANALYTICS_ENABLED_KEY).is_none());
|
||||
let extension_meta = state
|
||||
.current_meta_value_for_extension_request(test_mcp_turn_metadata_context())
|
||||
.expect("extension metadata should be present");
|
||||
assert_eq!(extension_meta["codex_security_surface"], "sdk");
|
||||
assert_eq!(extension_meta["source"], " Configured_Source ");
|
||||
assert_eq!(extension_meta[TURN_TRIGGER_KEY], "goal");
|
||||
assert_eq!(extension_meta["model"], "gpt-5.4");
|
||||
assert!(extension_meta.get(TOOL_NAMESPACES_INFO_KEY).is_none());
|
||||
assert_eq!(state.workspace_kind().as_deref(), Some("projectless"));
|
||||
assert_eq!(
|
||||
(state.turn_trigger(), state.codex_turn_source()),
|
||||
|
||||
@@ -5,6 +5,7 @@ use codex_api::ImageResponse;
|
||||
use codex_api::ImagesClient;
|
||||
use codex_api::ReqwestTransport;
|
||||
use codex_api::map_api_error;
|
||||
use codex_core::X_CODEX_TURN_METADATA_HEADER;
|
||||
use codex_http_client::ClientRouteClass;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_login::default_client::add_originator_header;
|
||||
@@ -101,12 +102,13 @@ impl CodexImagesBackend {
|
||||
&self,
|
||||
request: ImageGenerationRequest,
|
||||
turn_id: &str,
|
||||
codex_turn_metadata: Option<&str>,
|
||||
) -> Result<(ImageResponse, Option<String>), ImageBackendError> {
|
||||
self.client()
|
||||
.await?
|
||||
.generate(
|
||||
&request,
|
||||
image_request_headers(self.originator.as_deref(), turn_id),
|
||||
image_request_headers(self.originator.as_deref(), turn_id, codex_turn_metadata),
|
||||
)
|
||||
.await
|
||||
.map_err(ImageBackendError::from_image_request)
|
||||
@@ -117,19 +119,24 @@ impl CodexImagesBackend {
|
||||
&self,
|
||||
request: ImageEditRequest,
|
||||
turn_id: &str,
|
||||
codex_turn_metadata: Option<&str>,
|
||||
) -> Result<(ImageResponse, Option<String>), ImageBackendError> {
|
||||
self.client()
|
||||
.await?
|
||||
.edit(
|
||||
&request,
|
||||
image_request_headers(self.originator.as_deref(), turn_id),
|
||||
image_request_headers(self.originator.as_deref(), turn_id, codex_turn_metadata),
|
||||
)
|
||||
.await
|
||||
.map_err(ImageBackendError::from_image_request)
|
||||
}
|
||||
}
|
||||
|
||||
fn image_request_headers(originator: Option<&str>, turn_id: &str) -> HeaderMap {
|
||||
fn image_request_headers(
|
||||
originator: Option<&str>,
|
||||
turn_id: &str,
|
||||
codex_turn_metadata: Option<&str>,
|
||||
) -> HeaderMap {
|
||||
let mut headers = HeaderMap::new();
|
||||
if let Ok(turn_id) = HeaderValue::from_str(turn_id) {
|
||||
headers.insert(X_CODEX_IMAGE_TURN_ID_HEADER, turn_id);
|
||||
@@ -137,5 +144,12 @@ fn image_request_headers(originator: Option<&str>, turn_id: &str) -> HeaderMap {
|
||||
if let Some(originator) = originator {
|
||||
add_originator_header(&mut headers, originator);
|
||||
}
|
||||
|
||||
if let Some(metadata) = codex_turn_metadata
|
||||
&& let Ok(metadata) = HeaderValue::from_str(metadata)
|
||||
{
|
||||
headers.insert(X_CODEX_TURN_METADATA_HEADER, metadata);
|
||||
}
|
||||
|
||||
headers
|
||||
}
|
||||
|
||||
@@ -169,8 +169,16 @@ impl ImageGenerationTool {
|
||||
))
|
||||
.await;
|
||||
let result = match request {
|
||||
ImageRequest::Generate(request) => self.backend.generate(request, &call.turn_id).await,
|
||||
ImageRequest::Edit(request) => self.backend.edit(request, &call.turn_id).await,
|
||||
ImageRequest::Generate(request) => {
|
||||
self.backend
|
||||
.generate(request, &call.turn_id, call.codex_turn_metadata.as_deref())
|
||||
.await
|
||||
}
|
||||
ImageRequest::Edit(request) => {
|
||||
self.backend
|
||||
.edit(request, &call.turn_id, call.codex_turn_metadata.as_deref())
|
||||
.await
|
||||
}
|
||||
}
|
||||
.map_err(|error| {
|
||||
(
|
||||
|
||||
Reference in New Issue
Block a user