From 9e7b2a577f4ab1a098194126555e82869e9bb473 Mon Sep 17 00:00:00 2001 From: duc-openai Date: Wed, 14 Oct 2026 17:47:10 +0000 Subject: [PATCH] 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 --- .../tests/suite/v2/imagegen_extension.rs | 69 ++++++++++++++++--- .../src/tools/handlers/extension_tools.rs | 2 +- codex-rs/core/src/turn_metadata.rs | 20 +++++- codex-rs/core/src/turn_metadata_tests.rs | 8 +++ codex-rs/ext/image-generation/src/backend.rs | 20 +++++- codex-rs/ext/image-generation/src/tool.rs | 12 +++- 6 files changed, 113 insertions(+), 18 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs b/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs index 849a94f65d..b7fc11d2f2 100644 --- a/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs +++ b/codex-rs/app-server/tests/suite/v2/imagegen_extension.rs @@ -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::( + 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::( + 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::()?, 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, + turn_start_params: TurnStartParams, ) -> Result { 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 } = diff --git a/codex-rs/core/src/tools/handlers/extension_tools.rs b/codex-rs/core/src/tools/handlers/extension_tools.rs index d6eeb84616..70e7ed0164 100644 --- a/codex-rs/core/src/tools/handlers/extension_tools.rs +++ b/codex-rs/core/src/tools/handlers/extension_tools.rs @@ -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()); diff --git a/codex-rs/core/src/turn_metadata.rs b/codex-rs/core/src/turn_metadata.rs index 3e62e747f8..70e76f8713 100644 --- a/codex-rs/core/src/turn_metadata.rs +++ b/codex-rs/core/src/turn_metadata.rs @@ -276,7 +276,23 @@ impl TurnMetadataState { &self, execution: ExecutionMetadata<'_>, ) -> Option { - 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 { + // 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 { 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; diff --git a/codex-rs/core/src/turn_metadata_tests.rs b/codex-rs/core/src/turn_metadata_tests.rs index a8adf7c025..0987dd3468 100644 --- a/codex-rs/core/src/turn_metadata_tests.rs +++ b/codex-rs/core/src/turn_metadata_tests.rs @@ -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()), diff --git a/codex-rs/ext/image-generation/src/backend.rs b/codex-rs/ext/image-generation/src/backend.rs index 105667dbe4..cc18531775 100644 --- a/codex-rs/ext/image-generation/src/backend.rs +++ b/codex-rs/ext/image-generation/src/backend.rs @@ -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), 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), 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 } diff --git a/codex-rs/ext/image-generation/src/tool.rs b/codex-rs/ext/image-generation/src/tool.rs index d2777fb202..781d1e03ff 100644 --- a/codex-rs/ext/image-generation/src/tool.rs +++ b/codex-rs/ext/image-generation/src/tool.rs @@ -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| { (