mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Add Responses API client metadata patches for turn start and steer (#54053)
## Why Clients need a way to update individual turn metadata entries without replacing the existing client metadata map. ## What changed Add the experimental `responsesapiClientMetadataPatch` field to `turn/start` and `turn/steer`. Patches add or overwrite entries while retaining other keys and apply after `responsesapiClientMetadata` when both are supplied. The existing field retains replacement semantics, including clearing unprotected entries with an empty map. Continue protecting reserved metadata keys and preserving targeted `misalignment_override` continuations across updates. ## Testing Extend the app-server metadata integration test to cover replacement and patch steering, patch precedence at turn start, preservation of unrelated entries during patches, and protection of targeted continuations. GitOrigin-RevId: 8c4da3ba1b87329da20b5295846bdcf3a88126de
This commit is contained in:
committed by
copyberry
parent
9671d20fc8
commit
2b21129c6b
@@ -594,6 +594,7 @@ fn sample_turn_steer_request() -> ClientRequest {
|
||||
client_user_message_id: None,
|
||||
input: Vec::new(),
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata_patch: None,
|
||||
additional_context: None,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ fn sample_turn_steer_request(
|
||||
},
|
||||
],
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata_patch: None,
|
||||
additional_context: None,
|
||||
},
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
@@ -5089,6 +5089,7 @@ fn turn_start_params_preserve_explicit_null_service_tier() {
|
||||
root_turn_id: None,
|
||||
tool_output: None,
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata_patch: None,
|
||||
additional_context: None,
|
||||
environments: None,
|
||||
cwd: None,
|
||||
|
||||
@@ -198,6 +198,7 @@ pub struct TurnStartParams {
|
||||
#[ts(optional = nullable)]
|
||||
pub tool_output: Option<Box<TurnToolOutput>>,
|
||||
/// Optional metadata to enrich Codex's ResponsesAPI turn metadata.
|
||||
/// Replaces unprotected client-provided entries when supplied; an empty map clears them.
|
||||
///
|
||||
/// Entries are flattened into the JSON string sent as
|
||||
/// `client_metadata["x-codex-turn-metadata"]` on ResponsesAPI HTTP and websocket requests.
|
||||
@@ -207,6 +208,11 @@ pub struct TurnStartParams {
|
||||
#[experimental("turn/start.responsesapiClientMetadata")]
|
||||
#[ts(optional = nullable)]
|
||||
pub responsesapi_client_metadata: Option<HashMap<String, String>>,
|
||||
/// Merge entries into the ResponsesAPI turn metadata without removing existing entries.
|
||||
/// Applied after `responsesapiClientMetadata` if both are provided; reserved keys remain protected.
|
||||
#[experimental("turn/start.responsesapiClientMetadataPatch")]
|
||||
#[ts(optional = nullable)]
|
||||
pub responsesapi_client_metadata_patch: Option<HashMap<String, String>>,
|
||||
/// Optional client-provided context fragments keyed by an opaque source identifier.
|
||||
#[experimental("turn/start.additionalContext")]
|
||||
#[ts(optional = nullable)]
|
||||
@@ -314,6 +320,7 @@ pub struct TurnSteerParams {
|
||||
pub client_user_message_id: Option<String>,
|
||||
pub input: Vec<UserInput>,
|
||||
/// Optional metadata to enrich Codex's ResponsesAPI turn metadata.
|
||||
/// Replaces unprotected client-provided entries when supplied; an empty map clears them.
|
||||
///
|
||||
/// Entries are flattened into the JSON string sent as
|
||||
/// `client_metadata["x-codex-turn-metadata"]` on ResponsesAPI HTTP and websocket requests.
|
||||
@@ -323,6 +330,11 @@ pub struct TurnSteerParams {
|
||||
#[experimental("turn/steer.responsesapiClientMetadata")]
|
||||
#[ts(optional = nullable)]
|
||||
pub responsesapi_client_metadata: Option<HashMap<String, String>>,
|
||||
/// Merge entries into the ResponsesAPI turn metadata without removing existing entries.
|
||||
/// Applied after `responsesapiClientMetadata` if both are provided; reserved keys remain protected.
|
||||
#[experimental("turn/steer.responsesapiClientMetadataPatch")]
|
||||
#[ts(optional = nullable)]
|
||||
pub responsesapi_client_metadata_patch: Option<HashMap<String, String>>,
|
||||
/// Optional client-provided context fragments keyed by an opaque source identifier.
|
||||
#[experimental("turn/steer.additionalContext")]
|
||||
#[ts(optional = nullable)]
|
||||
|
||||
@@ -688,6 +688,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
|
||||
root_turn_id: None,
|
||||
tool_output: None,
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata_patch: None,
|
||||
additional_context: None,
|
||||
cwd: None,
|
||||
runtime_workspace_roots: None,
|
||||
|
||||
@@ -673,6 +673,7 @@ impl TurnRequestProcessor {
|
||||
})
|
||||
.with_additional_context(additional_context)
|
||||
.with_responses_metadata(params.responsesapi_client_metadata)
|
||||
.with_responses_metadata_patch(params.responsesapi_client_metadata_patch)
|
||||
.with_trace(self.request_trace_context(&request_id).await),
|
||||
)
|
||||
.await
|
||||
@@ -1088,7 +1089,8 @@ impl TurnRequestProcessor {
|
||||
client_id: params.client_user_message_id,
|
||||
})
|
||||
.with_additional_context(additional_context)
|
||||
.with_responses_metadata(params.responsesapi_client_metadata),
|
||||
.with_responses_metadata(params.responsesapi_client_metadata)
|
||||
.with_responses_metadata_patch(params.responsesapi_client_metadata_patch),
|
||||
params.expected_turn_id,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -449,8 +449,12 @@ async fn turn_start_sends_nested_subagent_lineage_after_cold_thread_resume_v2()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case::test_case(false; "replacement")]
|
||||
#[test_case::test_case(true; "patch")]
|
||||
#[tokio::test]
|
||||
async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2() -> Result<()> {
|
||||
async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2(
|
||||
patch_steer: bool,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -489,7 +493,8 @@ async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2()
|
||||
|
||||
let start_metadata = HashMap::from([
|
||||
("fiber_run_id".to_string(), "fiber-start-123".to_string()),
|
||||
("source".to_string(), "initial-source".to_string()),
|
||||
("source".to_string(), "snapshot-source".to_string()),
|
||||
("start_only".to_string(), "start-value".to_string()),
|
||||
(
|
||||
"misalignment_override".to_string(),
|
||||
r#"{"review_target":"RB._~:-opaque"}"#.to_string(),
|
||||
@@ -504,7 +509,11 @@ async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2()
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
turn_trigger: Some("user".to_string()),
|
||||
responsesapi_client_metadata: Some(start_metadata.clone()),
|
||||
responsesapi_client_metadata: Some(start_metadata),
|
||||
responsesapi_client_metadata_patch: Some(HashMap::from([(
|
||||
"source".to_string(),
|
||||
"initial-source".to_string(),
|
||||
)])),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
@@ -528,6 +537,11 @@ async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2()
|
||||
r#"{"review_target":"RC"}"#.to_string(),
|
||||
),
|
||||
]);
|
||||
let (responsesapi_client_metadata, responsesapi_client_metadata_patch) = if patch_steer {
|
||||
(None, Some(steer_metadata))
|
||||
} else {
|
||||
(Some(steer_metadata), None)
|
||||
};
|
||||
let steer_req = mcp
|
||||
.send_turn_steer_request(TurnSteerParams {
|
||||
thread_id: thread.id.clone(),
|
||||
@@ -536,7 +550,8 @@ async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2()
|
||||
text: "Focus on the failure".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
responsesapi_client_metadata: Some(steer_metadata.clone()),
|
||||
responsesapi_client_metadata,
|
||||
responsesapi_client_metadata_patch,
|
||||
additional_context: None,
|
||||
expected_turn_id: turn_id.clone(),
|
||||
})
|
||||
@@ -578,6 +593,10 @@ async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2()
|
||||
assert_eq!(second_metadata["turn_id"].as_str(), Some(turn_id.as_str()));
|
||||
assert_eq!(second_metadata["turn_trigger"].as_str(), Some("user"));
|
||||
assert_eq!(second_metadata["source"].as_str(), Some("steer-source"));
|
||||
assert_eq!(
|
||||
second_metadata["start_only"].as_str(),
|
||||
patch_steer.then_some("start-value")
|
||||
);
|
||||
assert_eq!(
|
||||
second_metadata["misalignment_override"],
|
||||
first_metadata["misalignment_override"],
|
||||
|
||||
@@ -1316,6 +1316,7 @@ async fn realtime_timeline_splits_accepted_steering_and_persists_promoted_artifa
|
||||
additional_context: None,
|
||||
client_user_message_id: None,
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata_patch: None,
|
||||
})
|
||||
.await?;
|
||||
let rejected = timeout(
|
||||
@@ -1348,6 +1349,7 @@ async fn realtime_timeline_splits_accepted_steering_and_persists_promoted_artifa
|
||||
additional_context: None,
|
||||
client_user_message_id: Some("accepted-steer".to_string()),
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata_patch: None,
|
||||
})
|
||||
.await?;
|
||||
let accepted: TurnSteerResponse =
|
||||
|
||||
@@ -3406,6 +3406,7 @@ async fn turn_start_explicit_local_environment_updates_legacy_cwd_between_turns(
|
||||
root_turn_id: None,
|
||||
tool_output: None,
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata_patch: None,
|
||||
additional_context: None,
|
||||
cwd: Some(first_cwd.clone()),
|
||||
runtime_workspace_roots: None,
|
||||
@@ -3461,6 +3462,7 @@ async fn turn_start_explicit_local_environment_updates_legacy_cwd_between_turns(
|
||||
root_turn_id: None,
|
||||
tool_output: None,
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata_patch: None,
|
||||
additional_context: None,
|
||||
cwd: None,
|
||||
runtime_workspace_roots: None,
|
||||
@@ -4682,6 +4684,7 @@ async fn direct_input_to_multi_agent_v2_subagent_is_rejected(
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata_patch: None,
|
||||
additional_context: None,
|
||||
expected_turn_id: "any-active-turn".to_string(),
|
||||
})
|
||||
|
||||
@@ -72,6 +72,7 @@ async fn turn_steer_requires_active_turn() -> Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata_patch: None,
|
||||
additional_context: None,
|
||||
expected_turn_id: "turn-does-not-exist".to_string(),
|
||||
})
|
||||
@@ -185,6 +186,7 @@ async fn turn_steer_rejects_oversized_text_input() -> Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata_patch: None,
|
||||
additional_context: None,
|
||||
expected_turn_id: turn.id.clone(),
|
||||
})
|
||||
@@ -300,6 +302,7 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata_patch: None,
|
||||
additional_context: None,
|
||||
expected_turn_id: turn.id.clone(),
|
||||
},
|
||||
@@ -435,6 +438,7 @@ async fn turn_steer_rejects_context_only_input_without_merging_context() -> Resu
|
||||
client_user_message_id: None,
|
||||
input: Vec::new(),
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata_patch: None,
|
||||
additional_context,
|
||||
expected_turn_id: turn.id,
|
||||
})
|
||||
|
||||
@@ -41,6 +41,7 @@ use codex_protocol::protocol::McpInvocation;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::turn_input::ResponsesApiClientMetadataUpdate;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use core_test_support::hooks::trusted_config_layer_stack;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
@@ -1087,15 +1088,19 @@ async fn mcp_tool_call_request_meta_includes_turn_metadata_for_custom_server() {
|
||||
let (_, turn_context) = make_session_and_context().await;
|
||||
let turn_context = Arc::new(turn_context);
|
||||
let step_context = StepContext::for_test(Arc::clone(&turn_context));
|
||||
let client_metadata = HashMap::from([
|
||||
(
|
||||
"node_repl_auto_review_required".to_string(),
|
||||
"true".to_string(),
|
||||
),
|
||||
("node_repl_disabled".to_string(), "true".to_string()),
|
||||
]);
|
||||
turn_context
|
||||
.turn_metadata_state
|
||||
.set_responsesapi_client_metadata(HashMap::from([
|
||||
(
|
||||
"node_repl_auto_review_required".to_string(),
|
||||
"true".to_string(),
|
||||
),
|
||||
("node_repl_disabled".to_string(), "true".to_string()),
|
||||
]));
|
||||
.update_responsesapi_client_metadata(ResponsesApiClientMetadataUpdate {
|
||||
replacement: Some(client_metadata),
|
||||
..Default::default()
|
||||
});
|
||||
let expected_turn_metadata = expected_mcp_turn_metadata(&turn_context);
|
||||
|
||||
let meta = build_mcp_tool_call_request_meta(
|
||||
|
||||
@@ -44,6 +44,7 @@ use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||
use codex_protocol::turn_input::AnnotatedResponseItem;
|
||||
use codex_protocol::turn_input::NotSubmittedReason;
|
||||
use codex_protocol::turn_input::ResponsesApiClientMetadataUpdate;
|
||||
use codex_protocol::turn_input::TurnInput as SubmittedTurnInput;
|
||||
use codex_protocol::turn_input::TurnInputMode;
|
||||
use codex_protocol::turn_input::TurnInputRequest;
|
||||
@@ -52,7 +53,6 @@ use codex_protocol::turn_input::TurnStartOptions;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -418,11 +418,9 @@ async fn start_or_steer(
|
||||
else {
|
||||
unreachable!("explicit user input can enter Plan mode");
|
||||
};
|
||||
if let Some(responsesapi_client_metadata) = responsesapi_client_metadata {
|
||||
turn_context
|
||||
.turn_metadata_state
|
||||
.set_responsesapi_client_metadata(responsesapi_client_metadata);
|
||||
}
|
||||
turn_context
|
||||
.turn_metadata_state
|
||||
.update_responsesapi_client_metadata(responsesapi_client_metadata);
|
||||
session
|
||||
.maybe_emit_model_warnings_for_turn(turn_context.as_ref())
|
||||
.await;
|
||||
@@ -572,11 +570,9 @@ async fn start_if_idle(
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
if let Some(responsesapi_client_metadata) = responsesapi_client_metadata {
|
||||
turn_context
|
||||
.turn_metadata_state
|
||||
.set_responsesapi_client_metadata(responsesapi_client_metadata);
|
||||
}
|
||||
turn_context
|
||||
.turn_metadata_state
|
||||
.update_responsesapi_client_metadata(responsesapi_client_metadata);
|
||||
session
|
||||
.maybe_emit_model_warnings_for_turn(turn_context.as_ref())
|
||||
.await;
|
||||
@@ -807,7 +803,7 @@ impl Session {
|
||||
additional_context: BTreeMap<String, AdditionalContextEntry>,
|
||||
expected_turn_id: Option<&str>,
|
||||
required_final_output_json_schema: Option<&Value>,
|
||||
responsesapi_client_metadata: Option<HashMap<String, String>>,
|
||||
responsesapi_client_metadata: ResponsesApiClientMetadataUpdate,
|
||||
origin: UserInputOrigin,
|
||||
) -> Result<(String, String), NotSubmittedReason> {
|
||||
let mut active = self.active_turn.lock().await;
|
||||
@@ -856,12 +852,10 @@ impl Session {
|
||||
}
|
||||
let mut pending_input = merge_additional_context_input(self, additional_context).await;
|
||||
|
||||
if let Some(responsesapi_client_metadata) = responsesapi_client_metadata {
|
||||
active_task
|
||||
.turn_context
|
||||
.turn_metadata_state
|
||||
.set_responsesapi_client_metadata(responsesapi_client_metadata);
|
||||
}
|
||||
active_task
|
||||
.turn_context
|
||||
.turn_metadata_state
|
||||
.update_responsesapi_client_metadata(responsesapi_client_metadata);
|
||||
|
||||
let input = match input {
|
||||
SubmittedTurnInput::UserInput { content, client_id } => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
@@ -38,6 +37,7 @@ use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
use codex_protocol::turn_input::ResponsesApiClientMetadataUpdate;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_git_discovery::GitRootDiscovery;
|
||||
|
||||
@@ -383,23 +383,31 @@ impl TurnMetadataState {
|
||||
self.root_turn_id.get().cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn set_responsesapi_client_metadata(
|
||||
pub(crate) fn update_responsesapi_client_metadata(
|
||||
&self,
|
||||
responsesapi_client_metadata: HashMap<String, String>,
|
||||
update: ResponsesApiClientMetadataUpdate,
|
||||
) {
|
||||
let mut metadata = filter_extra_metadata(responsesapi_client_metadata);
|
||||
let replacement = update.replacement.map(filter_extra_metadata);
|
||||
let patch = update.patch.map(filter_extra_metadata).unwrap_or_default();
|
||||
if replacement.is_none() && patch.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut current = self
|
||||
.responsesapi_client_metadata
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(continuation) = current
|
||||
let continuation = current
|
||||
.get("misalignment_override")
|
||||
.filter(|value| is_targeted_continuation(value))
|
||||
{
|
||||
.cloned();
|
||||
if let Some(replacement) = replacement {
|
||||
*current = replacement;
|
||||
}
|
||||
current.extend(patch);
|
||||
if let Some(continuation) = continuation {
|
||||
// Steering may update other metadata, but cannot replace the reviewed block.
|
||||
metadata.insert("misalignment_override".to_string(), continuation.clone());
|
||||
current.insert("misalignment_override".to_string(), continuation);
|
||||
}
|
||||
*current = metadata;
|
||||
}
|
||||
|
||||
pub(crate) fn set_responses_api_metadata(
|
||||
|
||||
@@ -684,7 +684,7 @@ fn turn_metadata_state_ignores_client_reserved_metadata_before_start() {
|
||||
/*auto_review_enabled*/ false,
|
||||
&model_info_from_slug("gpt-5.4"),
|
||||
);
|
||||
state.set_responsesapi_client_metadata(HashMap::from([
|
||||
let client_metadata = HashMap::from([
|
||||
(
|
||||
LEGACY_CODE_MODE_TOOL_NAMES_KEY.to_string(),
|
||||
"client-supplied".to_string(),
|
||||
@@ -722,7 +722,11 @@ fn turn_metadata_state_ignores_client_reserved_metadata_before_start() {
|
||||
"true".to_string(),
|
||||
),
|
||||
(NODE_REPL_DISABLED_KEY.to_string(), "true".to_string()),
|
||||
]));
|
||||
]);
|
||||
state.update_responsesapi_client_metadata(ResponsesApiClientMetadataUpdate {
|
||||
replacement: Some(client_metadata),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let header = test_turn_metadata_header(&state);
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
@@ -792,7 +796,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
state.set_parent_turn_id("parent-turn-a".to_string());
|
||||
state.set_root_turn_id("root-turn-a".to_string());
|
||||
state.set_turn_trigger("goal".to_string());
|
||||
state.set_responsesapi_client_metadata(HashMap::from([
|
||||
let client_metadata = HashMap::from([
|
||||
(
|
||||
"codex_security_surface".to_string(),
|
||||
"client-supplied".to_string(),
|
||||
@@ -861,7 +865,11 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
"turn_started_at_unix_ms".to_string(),
|
||||
"client-supplied".to_string(),
|
||||
),
|
||||
]));
|
||||
]);
|
||||
state.update_responsesapi_client_metadata(ResponsesApiClientMetadataUpdate {
|
||||
replacement: Some(client_metadata),
|
||||
..Default::default()
|
||||
});
|
||||
state.set_turn_started_at_unix_ms(/*turn_started_at_unix_ms*/ 1_700_000_000_123);
|
||||
let header = test_turn_metadata_header(&state);
|
||||
assert!(header.is_ascii());
|
||||
@@ -996,12 +1004,15 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
.into_iter()
|
||||
.collect(),
|
||||
);
|
||||
state.set_responsesapi_client_metadata(
|
||||
client
|
||||
.map(|source| ("source".to_string(), source))
|
||||
.into_iter()
|
||||
.collect(),
|
||||
);
|
||||
state.update_responsesapi_client_metadata(ResponsesApiClientMetadataUpdate {
|
||||
replacement: Some(
|
||||
client
|
||||
.map(|source| ("source".to_string(), source))
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
assert_eq!(state.codex_turn_source(), expected);
|
||||
}
|
||||
}
|
||||
@@ -1071,13 +1082,17 @@ fn turn_metadata_state_overlays_compaction_only_on_compaction_requests(
|
||||
serde_json::from_str(&test_turn_responses_metadata_json(&state, "thread-a:1"))
|
||||
.expect("json");
|
||||
assert!(ordinary_json.get("misalignment_override").is_none());
|
||||
state.set_responsesapi_client_metadata(HashMap::from([
|
||||
let client_metadata = HashMap::from([
|
||||
("compaction".to_string(), "client-supplied".to_string()),
|
||||
(
|
||||
"misalignment_override".to_string(),
|
||||
continuation.to_string(),
|
||||
),
|
||||
]));
|
||||
]);
|
||||
state.update_responsesapi_client_metadata(ResponsesApiClientMetadataUpdate {
|
||||
replacement: Some(client_metadata),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mcp_metadata = state
|
||||
.current_meta_value_for_mcp_request(test_mcp_turn_metadata_context())
|
||||
|
||||
@@ -1240,6 +1240,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
input: items.into_iter().map(Into::into).collect(),
|
||||
tool_output: None,
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata_patch: None,
|
||||
additional_context: None,
|
||||
environments: None,
|
||||
cwd: Some(default_cwd),
|
||||
|
||||
@@ -66,10 +66,19 @@ pub struct TurnInputRequest {
|
||||
pub thread_settings: ThreadSettingsOverrides,
|
||||
pub start: TurnStartOptions,
|
||||
pub additional_context: BTreeMap<String, AdditionalContextEntry>,
|
||||
pub responsesapi_client_metadata: Option<HashMap<String, String>>,
|
||||
pub responsesapi_client_metadata: ResponsesApiClientMetadataUpdate,
|
||||
pub trace: Option<W3cTraceContext>,
|
||||
}
|
||||
|
||||
/// An optional replacement followed by a patch to the turn's client metadata.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ResponsesApiClientMetadataUpdate {
|
||||
/// Replaces the existing map, including when empty.
|
||||
pub replacement: Option<HashMap<String, String>>,
|
||||
/// Adds or overwrites keys without removing other entries.
|
||||
pub patch: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
/// Attribution retained across recovery, sleep, and model-context compaction.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
pub struct TurnAttribution {
|
||||
@@ -114,7 +123,7 @@ impl TurnInputRequest {
|
||||
thread_settings: ThreadSettingsOverrides::default(),
|
||||
start: TurnStartOptions::default(),
|
||||
additional_context: BTreeMap::new(),
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata: ResponsesApiClientMetadataUpdate::default(),
|
||||
trace: None,
|
||||
}
|
||||
}
|
||||
@@ -153,12 +162,21 @@ impl TurnInputRequest {
|
||||
self
|
||||
}
|
||||
|
||||
/// Responses metadata attached whether this request starts or steers.
|
||||
/// Replaces Responses client metadata whether this request starts or steers.
|
||||
pub fn with_responses_metadata(
|
||||
mut self,
|
||||
responsesapi_client_metadata: Option<HashMap<String, String>>,
|
||||
) -> Self {
|
||||
self.responsesapi_client_metadata = responsesapi_client_metadata;
|
||||
self.responsesapi_client_metadata.replacement = responsesapi_client_metadata;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds or overwrites Responses client metadata entries after any replacement.
|
||||
pub fn with_responses_metadata_patch(
|
||||
mut self,
|
||||
responsesapi_client_metadata_patch: Option<HashMap<String, String>>,
|
||||
) -> Self {
|
||||
self.responsesapi_client_metadata.patch = responsesapi_client_metadata_patch;
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -1458,6 +1458,7 @@ impl AppServerSession {
|
||||
input: items,
|
||||
tool_output: None,
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata_patch: None,
|
||||
additional_context: None,
|
||||
environments: None,
|
||||
cwd: Some(cwd),
|
||||
@@ -1524,6 +1525,7 @@ impl AppServerSession {
|
||||
client_user_message_id: Some(client_user_message_id),
|
||||
input: items,
|
||||
responsesapi_client_metadata: None,
|
||||
responsesapi_client_metadata_patch: None,
|
||||
additional_context: None,
|
||||
expected_turn_id: turn_id,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user