diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 944e73e5c4..3772c18975 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread_sample.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread_sample.rs index 387a04c680..e2fc4c9878 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread_sample.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread_sample.rs @@ -6,6 +6,7 @@ use crate::TS; use codex_protocol::openai_models::ReasoningEffort; use serde::Deserialize; use serde::Serialize; +use std::collections::HashMap; #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] @@ -21,6 +22,9 @@ pub struct ThreadSampleParams { pub effort: Option, #[ts(optional = nullable)] pub turn_trigger: Option, + /// Optional metadata to enrich Codex's ResponsesAPI turn metadata. + #[ts(optional = nullable)] + pub responsesapi_client_metadata: Option>, /// Disable MCP servers when the inherited window uses incremental tools on Responses Lite. #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub disable_mcp: bool, diff --git a/codex-rs/app-server/src/thread_sample.rs b/codex-rs/app-server/src/thread_sample.rs index 6cc92d467b..2632fb0909 100644 --- a/codex-rs/app-server/src/thread_sample.rs +++ b/codex-rs/app-server/src/thread_sample.rs @@ -259,6 +259,7 @@ impl SampleTask { turn_trigger: params.turn_trigger, ..Default::default() }) + .with_responses_metadata(params.responsesapi_client_metadata) .with_trace(trace); if !matches!( fork.thread diff --git a/codex-rs/app-server/tests/suite/v2/thread_sample.rs b/codex-rs/app-server/tests/suite/v2/thread_sample.rs index 0f8ff10c49..38729e563c 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_sample.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_sample.rs @@ -40,6 +40,7 @@ use core_test_support::skip_if_remote; use pretty_assertions::assert_eq; use serde_json::Value; use serde_json::json; +use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use tempfile::TempDir; @@ -304,6 +305,13 @@ async fn sample_preserves_context_and_keeps_source_unchanged( prompt: "Continue identically".to_string(), effort: Some(ReasoningEffort::Medium), turn_trigger: Some("composer_predictions".to_string()), + responsesapi_client_metadata: Some(HashMap::from([ + ( + "generation_trigger".to_string(), + "composer_prediction".to_string(), + ), + ("turn_trigger".to_string(), "client-supplied".to_string()), + ])), ..Default::default() }, ) @@ -383,6 +391,19 @@ async fn sample_preserves_context_and_keeps_source_unchanged( .expect("turn metadata"), )?; assert_eq!(metadata["turn_trigger"], "composer_predictions"); + assert_eq!(metadata["generation_trigger"], "composer_prediction"); + let body_metadata: Value = serde_json::from_str( + prediction.body_json()["client_metadata"]["x-codex-turn-metadata"] + .as_str() + .expect("body turn metadata"), + )?; + assert_eq!(body_metadata, metadata); + let parent_metadata: Value = serde_json::from_str( + &requests[sample_index + 2] + .header("x-codex-turn-metadata") + .expect("parent turn metadata"), + )?; + assert!(parent_metadata.get("generation_trigger").is_none()); Ok(()) }