Always assign response item IDs (#34645)

## What changed

- Assign IDs to client-created response items in every session, including streamed items, forked history, compaction results, and non-OpenAI provider requests.
- Preserve server-provided IDs and stable generated IDs across persisted and resumed conversations while continuing to omit unprefixed server IDs from outbound requests.
- Retire `features.item_ids` as a configurable feature. Existing configuration and CLI toggles remain accepted as compatibility no-ops.

## Testing

- Cover default ID assignment, persistence and resume behavior, remote compaction, provider requests, WebSocket requests, and removed-feature compatibility.

GitOrigin-RevId: 7a09e2d07d2606c2ef1e8e2283d36cb555557eae
This commit is contained in:
pakrym-oai authored and copyberry committed 2026-07-22 01:23:44 +00:00
1 parent 690995b7c1
commit 4a443994bd
36 files changed
+256 -196

No files matched your search

@@ -16,6 +16,8 @@ use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::InitialHistory; use codex_protocol::protocol::InitialHistory;
use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutItem;
use core_test_support::responses; use core_test_support::responses;
use core_test_support::responses::strip_response_item_id;
use core_test_support::responses::strip_response_item_ids_from_json;
use serde_json::Value; use serde_json::Value;
use std::path::Path; use std::path::Path;
use tempfile::TempDir; use tempfile::TempDir;
@@ -89,7 +91,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
resumed_history resumed_history
.history .history
.iter() .iter()
.any(|item| matches!(item, RolloutItem::ResponseItem(response_item) if responses::strip_metadata(response_item.clone()) == injected_item)), .any(|item| matches!(item, RolloutItem::ResponseItem(response_item) if strip_response_item_id(responses::strip_metadata(response_item.clone())) == injected_item)),
"injected item should be persisted in rollout history" "injected item should be persisted in rollout history"
); );
@@ -116,7 +118,12 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu
.await??; .await??;
let injected_value = serde_json::to_value(&injected_item)?; let injected_value = serde_json::to_value(&injected_item)?;
let model_input = response_mock.single_request().input(); let model_input: Vec<Value> = response_mock
.single_request()
.input()
.into_iter()
.map(strip_response_item_ids_from_json)
.collect();
let environment_context_index = let environment_context_index =
response_item_text_position(&model_input, "<environment_context>") response_item_text_position(&model_input, "<environment_context>")
.expect("environment context should be injected before the first user turn"); .expect("environment context should be injected before the first user turn");
@@ -247,11 +254,19 @@ async fn thread_inject_items_adds_raw_response_items_after_a_turn() -> Result<()
let requests = response_mock.requests(); let requests = response_mock.requests();
assert_eq!(requests.len(), 2); assert_eq!(requests.len(), 2);
assert!( assert!(
!requests[0].input().contains(&injected_value), !requests[0]
.input()
.into_iter()
.map(strip_response_item_ids_from_json)
.any(|item| item == injected_value),
"injected item should not be sent before it is injected" "injected item should not be sent before it is injected"
); );
assert!( assert!(
requests[1].input().contains(&injected_value), requests[1]
.input()
.into_iter()
.map(strip_response_item_ids_from_json)
.any(|item| item == injected_value),
"injected item should be sent after being injected into existing history" "injected item should be sent after being injected into existing history"
); );
@@ -24,6 +24,7 @@ use codex_app_server_protocol::WebSearchAction;
use codex_app_server_protocol::WebSearchItem; use codex_app_server_protocol::WebSearchItem;
use codex_config::types::AuthCredentialsStoreMode; use codex_config::types::AuthCredentialsStoreMode;
use core_test_support::responses; use core_test_support::responses;
use core_test_support::responses::strip_response_item_ids_from_json;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serde_json::Value; use serde_json::Value;
use serde_json::json; use serde_json::json;
@@ -228,7 +229,9 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
); );
assert_eq!( assert_eq!(
responses::strip_metadata_from_json(requests[1].function_call_output(call_id)), strip_response_item_ids_from_json(responses::strip_metadata_from_json(
requests[1].function_call_output(call_id),
)),
json!({ json!({
"type": "function_call_output", "type": "function_call_output",
"call_id": call_id, "call_id": call_id,
+10
View File
@@ -4133,6 +4133,16 @@ mod tests {
assert_eq!(overrides, vec!["features.enable_fanout=true".to_string(),]); assert_eq!(overrides, vec!["features.enable_fanout=true".to_string(),]);
} }
#[test]
fn feature_toggles_accept_removed_item_ids_flag() {
let toggles = FeatureToggles {
enable: vec!["item_ids".to_string()],
disable: Vec::new(),
};
let overrides = toggles.to_overrides().expect("valid features");
assert_eq!(overrides, vec!["features.item_ids=true".to_string()]);
}
#[test] #[test]
fn feature_toggles_unknown_feature_errors() { fn feature_toggles_unknown_feature_errors() {
let toggles = FeatureToggles { let toggles = FeatureToggles {
+3 -2
View File
@@ -56,6 +56,7 @@ use codex_thread_store::LocalThreadStore;
use codex_thread_store::LocalThreadStoreConfig; use codex_thread_store::LocalThreadStoreConfig;
use codex_thread_store::ThreadStore; use codex_thread_store::ThreadStore;
use codex_utils_path_uri::PathUri; use codex_utils_path_uri::PathUri;
use core_test_support::responses::strip_response_item_ids;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use tempfile::TempDir; use tempfile::TempDir;
use tokio::time::Duration; use tokio::time::Duration;
@@ -1340,8 +1341,8 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
}, },
]; ];
assert_eq!( assert_eq!(
history.raw_items(), strip_response_item_ids(history.raw_items()),
&expected_history, strip_response_item_ids(&expected_history),
"full-history forked child history should replace parent usage hints with the child subagent hint while filtering non-final assistant/tool chatter" "full-history forked child history should replace parent usage hints with the child subagent hint while filtering non-final assistant/tool chatter"
); );
assert_eq!( assert_eq!(
+6 -19
View File
@@ -207,7 +207,6 @@ struct ModelClientState {
enable_request_compression: bool, enable_request_compression: bool,
include_timing_metrics: bool, include_timing_metrics: bool,
beta_features_header: Option<String>, beta_features_header: Option<String>,
item_ids_enabled: bool,
concurrent_reasoning_summaries_enabled: bool, concurrent_reasoning_summaries_enabled: bool,
include_attestation: bool, include_attestation: bool,
attestation_provider: Option<Arc<dyn AttestationProvider>>, attestation_provider: Option<Arc<dyn AttestationProvider>>,
@@ -421,7 +420,6 @@ impl ModelClient {
enable_request_compression: bool, enable_request_compression: bool,
include_timing_metrics: bool, include_timing_metrics: bool,
beta_features_header: Option<String>, beta_features_header: Option<String>,
item_ids_enabled: bool,
concurrent_reasoning_summaries_enabled: bool, concurrent_reasoning_summaries_enabled: bool,
attestation_provider: Option<Arc<dyn AttestationProvider>>, attestation_provider: Option<Arc<dyn AttestationProvider>>,
http_client_factory: HttpClientFactory, http_client_factory: HttpClientFactory,
@@ -445,7 +443,6 @@ impl ModelClient {
enable_request_compression, enable_request_compression,
include_timing_metrics, include_timing_metrics,
beta_features_header, beta_features_header,
item_ids_enabled,
concurrent_reasoning_summaries_enabled, concurrent_reasoning_summaries_enabled,
include_attestation, include_attestation,
attestation_provider, attestation_provider,
@@ -583,7 +580,7 @@ impl ModelClient {
text, text,
.. ..
} = request; } = request;
self.prepare_response_items_for_request(&mut input, /*store*/ false); self.prepare_response_items_for_request(&mut input);
let payload = ApiCompactionInput { let payload = ApiCompactionInput {
model: &model, model: &model,
input: &input, input: &input,
@@ -908,20 +905,12 @@ impl ModelClient {
Ok(request) Ok(request)
} }
fn prepare_response_items_for_request(&self, input: &mut [ResponseItem], store: bool) { fn prepare_response_items_for_request(&self, input: &mut [ResponseItem]) {
for item in input.iter_mut() { for item in input {
if item.id().is_some_and(|id| !id.is_prefixed()) { if item.id().is_some_and(|id| !id.is_prefixed()) {
item.set_id(/*new_id*/ None); item.set_id(/*new_id*/ None);
} }
} }
if self.state.item_ids_enabled || store {
return;
}
for item in input {
item.set_id(/*new_id*/ None);
}
} }
/// Returns whether the Responses-over-WebSocket transport is active for this session. /// Returns whether the Responses-over-WebSocket transport is active for this session.
@@ -1439,9 +1428,8 @@ impl ModelClientSession {
service_tier.clone(), service_tier.clone(),
responses_metadata, responses_metadata,
)?; )?;
let store = request.store;
self.client self.client
.prepare_response_items_for_request(&mut request.input, store); .prepare_response_items_for_request(&mut request.input);
let request_session_telemetry = let request_session_telemetry =
session_telemetry_for_request(session_telemetry, &request); session_telemetry_for_request(session_telemetry, &request);
let inference_trace_attempt = inference_trace.start_attempt(); let inference_trace_attempt = inference_trace.start_attempt();
@@ -1620,10 +1608,9 @@ impl ModelClientSession {
Some((response_id, items)) => (Some(response_id), Some(items)), Some((response_id, items)) => (Some(response_id), Some(items)),
None => (None, None), None => (None, None),
}; };
let store = request.store;
let original_item_ids = if let Some(incremental_items) = &mut incremental_items { let original_item_ids = if let Some(incremental_items) = &mut incremental_items {
self.client self.client
.prepare_response_items_for_request(incremental_items, store); .prepare_response_items_for_request(incremental_items);
None None
} else { } else {
let original_item_ids = request let original_item_ids = request
@@ -1632,7 +1619,7 @@ impl ModelClientSession {
.map(|item| item.id().cloned()) .map(|item| item.id().cloned())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
self.client self.client
.prepare_response_items_for_request(&mut request.input, store); .prepare_response_items_for_request(&mut request.input);
Some(original_item_ids) Some(original_item_ids)
}; };
let ws_payload = ResponseCreateWsRequest { let ws_payload = ResponseCreateWsRequest {
-3
View File
@@ -104,7 +104,6 @@ fn test_model_client_with_thread_id(
/*enable_request_compression*/ false, /*enable_request_compression*/ false,
/*include_timing_metrics*/ false, /*include_timing_metrics*/ false,
/*beta_features_header*/ None, /*beta_features_header*/ None,
/*item_ids_enabled*/ false,
/*concurrent_reasoning_summaries_enabled*/ false, /*concurrent_reasoning_summaries_enabled*/ false,
/*attestation_provider*/ None, /*attestation_provider*/ None,
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
@@ -150,7 +149,6 @@ async fn compact_uses_bearer_after_agent_identity_session_fallback() -> anyhow::
/*enable_request_compression*/ false, /*enable_request_compression*/ false,
/*include_timing_metrics*/ false, /*include_timing_metrics*/ false,
/*beta_features_header*/ None, /*beta_features_header*/ None,
/*item_ids_enabled*/ false,
/*concurrent_reasoning_summaries_enabled*/ false, /*concurrent_reasoning_summaries_enabled*/ false,
/*attestation_provider*/ None, /*attestation_provider*/ None,
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
@@ -825,7 +823,6 @@ fn model_client_with_counting_attestation(
/*enable_request_compression*/ false, /*enable_request_compression*/ false,
/*include_timing_metrics*/ false, /*include_timing_metrics*/ false,
/*beta_features_header*/ None, /*beta_features_header*/ None,
/*item_ids_enabled*/ false,
/*concurrent_reasoning_summaries_enabled*/ false, /*concurrent_reasoning_summaries_enabled*/ false,
Some(Arc::new(CountingAttestationProvider { Some(Arc::new(CountingAttestationProvider {
calls: attestation_calls.clone(), calls: attestation_calls.clone(),
-1
View File
@@ -368,7 +368,6 @@ async fn run_compact_task_inner_impl(
} }
}; };
sess.replace_compacted_history( sess.replace_compacted_history(
turn_context.as_ref(),
new_history, new_history,
reference_context_item, reference_context_item,
world_state_baseline, world_state_baseline,
-1
View File
@@ -279,7 +279,6 @@ async fn run_remote_compact_task_inner_impl(
}); });
} }
sess.replace_compacted_history( sess.replace_compacted_history(
compaction_turn_context.as_ref(),
new_history, new_history,
reference_context_item, reference_context_item,
world_state_baseline, world_state_baseline,
-1
View File
@@ -300,7 +300,6 @@ async fn run_remote_compact_task_inner_impl(
}); });
} }
sess.replace_compacted_history( sess.replace_compacted_history(
compaction_turn_context.as_ref(),
new_history, new_history,
reference_context_item, reference_context_item,
world_state_baseline, world_state_baseline,
+3 -15
View File
@@ -1324,9 +1324,7 @@ impl Session {
} }
InitialHistory::Forked(mut rollout_items) => { InitialHistory::Forked(mut rollout_items) => {
let turn_context = self.new_default_turn().await; let turn_context = self.new_default_turn().await;
if turn_context.item_ids_enabled() { Self::assign_missing_rollout_response_item_ids(&mut rollout_items);
Self::assign_missing_rollout_response_item_ids(&mut rollout_items);
}
self.apply_rollout_reconstruction(&turn_context, &rollout_items) self.apply_rollout_reconstruction(&turn_context, &rollout_items)
.await; .await;
@@ -2807,11 +2805,7 @@ impl Session {
for item in items.to_mut() { for item in items.to_mut() {
item.set_turn_id_if_missing(&turn_context.sub_id); item.set_turn_id_if_missing(&turn_context.sub_id);
} }
if turn_context.item_ids_enabled() { Self::assign_missing_response_item_ids(items)
Self::assign_missing_response_item_ids(items)
} else {
items
}
} }
fn assign_missing_response_item_ids(items: Cow<'_, [ResponseItem]>) -> Cow<'_, [ResponseItem]> { fn assign_missing_response_item_ids(items: Cow<'_, [ResponseItem]>) -> Cow<'_, [ResponseItem]> {
@@ -3057,17 +3051,12 @@ impl Session {
pub(crate) async fn replace_compacted_history( pub(crate) async fn replace_compacted_history(
&self, &self,
turn_context: &TurnContext,
items: Vec<ResponseItem>, items: Vec<ResponseItem>,
reference_context_item: Option<TurnContextItem>, reference_context_item: Option<TurnContextItem>,
world_state_baseline: Option<Arc<WorldState>>, world_state_baseline: Option<Arc<WorldState>>,
metadata: CompactedHistoryMetadata, metadata: CompactedHistoryMetadata,
) { ) {
let items = if turn_context.item_ids_enabled() { let items = Self::assign_missing_response_item_ids(Cow::Owned(items)).into_owned();
Self::assign_missing_response_item_ids(Cow::Owned(items)).into_owned()
} else {
items
};
let compacted_item = CompactedItem { let compacted_item = CompactedItem {
message: metadata.message, message: metadata.message,
replacement_history: Some(items.clone()), replacement_history: Some(items.clone()),
@@ -3556,7 +3545,6 @@ impl Session {
.await; .await;
let turn_context_item = turn_context.to_turn_context_item(); let turn_context_item = turn_context.to_turn_context_item();
self.replace_compacted_history( self.replace_compacted_history(
turn_context,
context_items, context_items,
Some(turn_context_item), Some(turn_context_item),
Some(world_state), Some(world_state),
+1 -12
View File
@@ -522,13 +522,7 @@ impl Session {
session_configuration.thread_source.as_ref(), session_configuration.thread_source.as_ref(),
Some(ThreadSource::Subagent) Some(ThreadSource::Subagent)
); );
if (config.features.enabled(Feature::ItemIds) if let InitialHistory::Forked(items) = &mut initial_history {
|| matches!(
session_configuration.history_mode,
ThreadHistoryMode::Paginated
))
&& let InitialHistory::Forked(items) = &mut initial_history
{
Self::assign_missing_rollout_response_item_ids(items); Self::assign_missing_rollout_response_item_ids(items);
} }
let multi_agent_version = multi_agent_version.map(OnceLock::from).unwrap_or_default(); let multi_agent_version = multi_agent_version.map(OnceLock::from).unwrap_or_default();
@@ -1120,11 +1114,6 @@ impl Session {
config.features.enabled(Feature::EnableRequestCompression), config.features.enabled(Feature::EnableRequestCompression),
config.features.enabled(Feature::RuntimeMetrics), config.features.enabled(Feature::RuntimeMetrics),
Self::build_model_client_beta_features_header(config.as_ref()), Self::build_model_client_beta_features_header(config.as_ref()),
/*item_ids_enabled*/ config.features.enabled(Feature::ItemIds)
|| matches!(
session_configuration.history_mode,
ThreadHistoryMode::Paginated
),
/*concurrent_reasoning_summaries_enabled*/ config /*concurrent_reasoning_summaries_enabled*/ config
.features .features
.enabled(Feature::ConcurrentReasoningSummaries), .enabled(Feature::ConcurrentReasoningSummaries),
+45 -37
View File
@@ -67,7 +67,6 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::NonSteerableTurnKind; use codex_protocol::protocol::NonSteerableTurnKind;
use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::ThreadHistoryMode;
use codex_protocol::protocol::TurnEnvironmentSelections; use codex_protocol::protocol::TurnEnvironmentSelections;
use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile; use codex_protocol::request_permissions::RequestPermissionProfile;
@@ -163,6 +162,8 @@ use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse; use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server; use core_test_support::responses::start_mock_server;
use core_test_support::responses::strip_metadata_from_items; use core_test_support::responses::strip_metadata_from_items;
use core_test_support::responses::strip_response_item_ids;
use core_test_support::responses::strip_response_item_ids_from_json;
use core_test_support::test_codex::local; use core_test_support::test_codex::local;
use core_test_support::test_codex::test_codex; use core_test_support::test_codex::test_codex;
use core_test_support::test_path_buf; use core_test_support::test_path_buf;
@@ -261,9 +262,8 @@ fn assign_missing_response_item_ids_assigns_additional_tools_ids() {
} }
#[tokio::test] #[tokio::test]
async fn paginated_turn_context_assigns_missing_response_item_ids_without_feature() { async fn default_turn_context_assigns_missing_response_item_ids() {
let (session, mut turn_context) = make_session_and_context().await; let (session, turn_context) = make_session_and_context().await;
turn_context.history_mode = ThreadHistoryMode::Paginated;
let response_item = user_message("hello"); let response_item = user_message("hello");
let items = session.prepare_conversation_items_for_history( let items = session.prepare_conversation_items_for_history(
@@ -516,7 +516,6 @@ fn test_model_client_session() -> crate::client::ModelClientSession {
/*enable_request_compression*/ false, /*enable_request_compression*/ false,
/*include_timing_metrics*/ false, /*include_timing_metrics*/ false,
/*beta_features_header*/ None, /*beta_features_header*/ None,
/*item_ids_enabled*/ false,
/*concurrent_reasoning_summaries_enabled*/ false, /*concurrent_reasoning_summaries_enabled*/ false,
/*attestation_provider*/ None, /*attestation_provider*/ None,
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
@@ -1844,8 +1843,10 @@ async fn record_initial_history_reconstructs_resumed_transcript() {
#[tokio::test] #[tokio::test]
async fn record_conversation_items_stamps_missing_turn_id_and_preserves_existing_turn_id() { async fn record_conversation_items_stamps_missing_turn_id_and_preserves_existing_turn_id() {
let (session, turn_context) = make_session_and_context().await; let (session, turn_context) = make_session_and_context().await;
let fresh_item = user_message("fresh"); let mut fresh_item = user_message("fresh");
fresh_item.set_id(Some(ResponseItemId::with_suffix("msg", "fresh")));
let mut existing_item = assistant_message("existing"); let mut existing_item = assistant_message("existing");
existing_item.set_id(Some(ResponseItemId::with_suffix("msg", "existing")));
existing_item.set_turn_id_if_missing("older-turn"); existing_item.set_turn_id_if_missing("older-turn");
session session
@@ -1918,8 +1919,8 @@ async fn record_inter_agent_communication_sets_turn_id_in_rollout_and_resume() {
.await; .await;
assert_eq!( assert_eq!(
session.clone_history().await.raw_items(), strip_response_item_ids(session.clone_history().await.raw_items()),
std::slice::from_ref(&expected_item) strip_response_item_ids(std::slice::from_ref(&expected_item))
); );
session.flush_rollout().await.expect("rollout should flush"); session.flush_rollout().await.expect("rollout should flush");
@@ -1949,8 +1950,8 @@ async fn record_inter_agent_communication_sets_turn_id_in_rollout_and_resume() {
RolloutItem::ResponseItem(expected_item.clone()), RolloutItem::ResponseItem(expected_item.clone()),
]; ];
assert_eq!( assert_eq!(
serde_json::to_value(persisted_items).unwrap(), strip_response_item_ids_from_json(serde_json::to_value(persisted_items).unwrap()),
serde_json::to_value(expected_persisted_items).unwrap() strip_response_item_ids_from_json(serde_json::to_value(expected_persisted_items).unwrap())
); );
let (resumed_session, _resumed_turn_context) = make_session_and_context().await; let (resumed_session, _resumed_turn_context) = make_session_and_context().await;
@@ -1958,8 +1959,8 @@ async fn record_inter_agent_communication_sets_turn_id_in_rollout_and_resume() {
.record_initial_history(InitialHistory::Resumed(resumed)) .record_initial_history(InitialHistory::Resumed(resumed))
.await; .await;
assert_eq!( assert_eq!(
resumed_session.clone_history().await.raw_items(), strip_response_item_ids(resumed_session.clone_history().await.raw_items()),
std::slice::from_ref(&expected_item) strip_response_item_ids(std::slice::from_ref(&expected_item))
); );
} }
@@ -1968,9 +1969,7 @@ async fn record_inter_agent_communication_preserves_item_id_in_rollout_and_resum
let (mut session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx( let (mut session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx(
CodexAuth::from_api_key("Test API Key"), CodexAuth::from_api_key("Test API Key"),
Vec::new(), Vec::new(),
|config| { |_| {},
let _ = config.features.enable(Feature::ItemIds);
},
) )
.await; .await;
let rollout_path = let rollout_path =
@@ -2017,9 +2016,7 @@ async fn record_inter_agent_communication_preserves_item_id_in_rollout_and_resum
make_session_and_context_with_auth_and_config_and_rx( make_session_and_context_with_auth_and_config_and_rx(
CodexAuth::from_api_key("Test API Key"), CodexAuth::from_api_key("Test API Key"),
Vec::new(), Vec::new(),
|config| { |_| {},
let _ = config.features.enable(Feature::ItemIds);
},
) )
.await; .await;
resumed_session resumed_session
@@ -2040,9 +2037,7 @@ async fn prepares_image_failures_before_history_insertion() {
let (session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx( let (session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx(
CodexAuth::from_api_key("Test API Key"), CodexAuth::from_api_key("Test API Key"),
Vec::new(), Vec::new(),
|config| { |_| {},
let _ = config.features.enable(Feature::ItemIds);
},
) )
.await; .await;
let item = ResponseItem::FunctionCallOutput { let item = ResponseItem::FunctionCallOutput {
@@ -2274,7 +2269,10 @@ async fn resumed_history_injects_initial_context_on_first_context_update_only()
let initial_context = build_initial_context(&session, &turn_context).await; let initial_context = build_initial_context(&session, &turn_context).await;
expected.extend(initial_context); expected.extend(initial_context);
let history_after_seed = session.clone_history().await; let history_after_seed = session.clone_history().await;
assert_eq!(expected, history_after_seed.raw_items()); assert_eq!(
strip_response_item_ids(&expected),
strip_response_item_ids(history_after_seed.raw_items())
);
session session
.record_context_updates_and_set_reference_context_item(&step_context) .record_context_updates_and_set_reference_context_item(&step_context)
@@ -2880,7 +2878,10 @@ async fn record_initial_history_reconstructs_forked_transcript() {
.await; .await;
let history = session.state.lock().await.clone_history(); let history = session.state.lock().await.clone_history();
assert_eq!(expected, history.raw_items()); assert_eq!(
strip_response_item_ids(&expected),
strip_response_item_ids(history.raw_items())
);
} }
#[tokio::test] #[tokio::test]
@@ -2888,9 +2889,7 @@ async fn start_new_context_window_assigns_and_persists_item_ids() {
let (mut session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx( let (mut session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx(
CodexAuth::from_api_key("Test API Key"), CodexAuth::from_api_key("Test API Key"),
Vec::new(), Vec::new(),
|config| { |_| {},
let _ = config.features.enable(Feature::ItemIds);
},
) )
.await; .await;
let rollout_path = let rollout_path =
@@ -2941,9 +2940,7 @@ async fn record_initial_history_assigns_and_persists_id_for_forked_response_item
let (mut session, _turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx( let (mut session, _turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx(
CodexAuth::from_api_key("Test API Key"), CodexAuth::from_api_key("Test API Key"),
Vec::new(), Vec::new(),
|config| { |_| {},
let _ = config.features.enable(Feature::ItemIds);
},
) )
.await; .await;
let rollout_path = let rollout_path =
@@ -3329,6 +3326,7 @@ async fn thread_rollback_fails_without_persisted_thread_history() {
let initial_context = build_initial_context(&sess, &tc).await; let initial_context = build_initial_context(&sess, &tc).await;
sess.record_conversation_items(tc.as_ref(), &initial_context) sess.record_conversation_items(tc.as_ref(), &initial_context)
.await; .await;
let history_before_rollback = sess.clone_history().await;
handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await; handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await;
@@ -3341,7 +3339,10 @@ async fn thread_rollback_fails_without_persisted_thread_history() {
error_event.codex_error_info, error_event.codex_error_info,
Some(CodexErrorInfo::ThreadRollbackFailed) Some(CodexErrorInfo::ThreadRollbackFailed)
); );
assert_eq!(sess.clone_history().await.raw_items(), initial_context); assert_eq!(
sess.clone_history().await.raw_items(),
history_before_rollback.raw_items()
);
} }
#[tokio::test] #[tokio::test]
@@ -3755,6 +3756,7 @@ async fn thread_rollback_fails_when_turn_in_progress() {
let initial_context = build_initial_context(&sess, &tc).await; let initial_context = build_initial_context(&sess, &tc).await;
sess.record_conversation_items(tc.as_ref(), &initial_context) sess.record_conversation_items(tc.as_ref(), &initial_context)
.await; .await;
let history_before_rollback = sess.clone_history().await;
*sess.active_turn.lock().await = Some(crate::state::ActiveTurn::default()); *sess.active_turn.lock().await = Some(crate::state::ActiveTurn::default());
handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await; handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 1).await;
@@ -3766,7 +3768,7 @@ async fn thread_rollback_fails_when_turn_in_progress() {
); );
let history = sess.clone_history().await; let history = sess.clone_history().await;
assert_eq!(initial_context, history.raw_items()); assert_eq!(history_before_rollback.raw_items(), history.raw_items());
} }
#[tokio::test] #[tokio::test]
@@ -3776,6 +3778,7 @@ async fn thread_rollback_fails_when_num_turns_is_zero() {
let initial_context = build_initial_context(&sess, &tc).await; let initial_context = build_initial_context(&sess, &tc).await;
sess.record_conversation_items(tc.as_ref(), &initial_context) sess.record_conversation_items(tc.as_ref(), &initial_context)
.await; .await;
let history_before_rollback = sess.clone_history().await;
handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 0).await; handlers::thread_rollback(&sess, "sub-1".to_string(), /*num_turns*/ 0).await;
@@ -3787,7 +3790,7 @@ async fn thread_rollback_fails_when_num_turns_is_zero() {
); );
let history = sess.clone_history().await; let history = sess.clone_history().await;
assert_eq!(initial_context, history.raw_items()); assert_eq!(history_before_rollback.raw_items(), history.raw_items());
} }
#[tokio::test] #[tokio::test]
@@ -5465,7 +5468,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
config.features.enabled(Feature::EnableRequestCompression), config.features.enabled(Feature::EnableRequestCompression),
config.features.enabled(Feature::RuntimeMetrics), config.features.enabled(Feature::RuntimeMetrics),
Session::build_model_client_beta_features_header(config.as_ref()), Session::build_model_client_beta_features_header(config.as_ref()),
/*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
/*concurrent_reasoning_summaries_enabled*/ /*concurrent_reasoning_summaries_enabled*/
config config
.features .features
@@ -7625,7 +7627,6 @@ where
config.features.enabled(Feature::EnableRequestCompression), config.features.enabled(Feature::EnableRequestCompression),
config.features.enabled(Feature::RuntimeMetrics), config.features.enabled(Feature::RuntimeMetrics),
Session::build_model_client_beta_features_header(config.as_ref()), Session::build_model_client_beta_features_header(config.as_ref()),
/*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
/*concurrent_reasoning_summaries_enabled*/ /*concurrent_reasoning_summaries_enabled*/
config config
.features .features
@@ -8946,7 +8947,10 @@ async fn record_context_updates_and_set_reference_context_item_injects_full_cont
.await; .await;
let history = session.clone_history().await; let history = session.clone_history().await;
let initial_context = build_initial_context(&session, &turn_context).await; let initial_context = build_initial_context(&session, &turn_context).await;
assert_eq!(history.raw_items().to_vec(), initial_context); assert_eq!(
strip_response_item_ids(history.raw_items()),
strip_response_item_ids(&initial_context)
);
let current_context = session.reference_context_item().await; let current_context = session.reference_context_item().await;
assert_eq!( assert_eq!(
@@ -8996,7 +9000,10 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co
let mut expected_history = vec![compacted_summary]; let mut expected_history = vec![compacted_summary];
let initial_context = build_initial_context(&session, &turn_context).await; let initial_context = build_initial_context(&session, &turn_context).await;
expected_history.extend(initial_context); expected_history.extend(initial_context);
assert_eq!(history.raw_items().to_vec(), expected_history); assert_eq!(
strip_response_item_ids(history.raw_items()),
strip_response_item_ids(&expected_history)
);
} }
#[tokio::test] #[tokio::test]
@@ -9748,7 +9755,8 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}; };
assert!( assert!(
strip_metadata_from_items(history.raw_items()).contains(&expected), strip_response_item_ids(&strip_metadata_from_items(history.raw_items()))
.contains(&expected),
"expected pending input to be persisted into history on turn completion" "expected pending input to be persisted into history on turn completion"
); );
+5 -7
View File
@@ -1930,7 +1930,9 @@ fn assign_missing_streamed_response_item_id(
return; return;
} }
let active_item_id = active_item.map(|item| ResponseItemId::from_server(item.id())); let active_item_id = active_item
.map(|item| ResponseItemId::from_server(item.id()))
.filter(|item_id| !item_id.is_empty());
item.set_id(active_item_id); item.set_id(active_item_id);
Session::assign_missing_response_item_id(item); Session::assign_missing_response_item_id(item);
} }
@@ -2051,9 +2053,7 @@ async fn try_run_sampling_request(
match event { match event {
ResponseEvent::Created => {} ResponseEvent::Created => {}
ResponseEvent::OutputItemDone(mut item) => { ResponseEvent::OutputItemDone(mut item) => {
if turn_context.item_ids_enabled() { assign_missing_streamed_response_item_id(&mut item, active_item.as_ref());
assign_missing_streamed_response_item_id(&mut item, active_item.as_ref());
}
if let Some((_, mut consumer)) = active_tool_argument_diff_consumer.take() if let Some((_, mut consumer)) = active_tool_argument_diff_consumer.take()
&& let Ok(Some(event)) = consumer.finish() && let Ok(Some(event)) = consumer.finish()
{ {
@@ -2148,9 +2148,7 @@ async fn try_run_sampling_request(
} }
} }
ResponseEvent::OutputItemAdded(mut item) => { ResponseEvent::OutputItemAdded(mut item) => {
if turn_context.item_ids_enabled() { assign_missing_streamed_response_item_id(&mut item, /*active_item*/ None);
assign_missing_streamed_response_item_id(&mut item, /*active_item*/ None);
}
if let ResponseItem::CustomToolCall { if let ResponseItem::CustomToolCall {
call_id, call_id,
name, name,
@@ -162,11 +162,6 @@ enum TurnMultiAgentRuntime {
} }
impl TurnContext { impl TurnContext {
pub(crate) fn item_ids_enabled(&self) -> bool {
self.config.features.enabled(Feature::ItemIds)
|| matches!(self.history_mode, ThreadHistoryMode::Paginated)
}
pub(crate) fn collaboration_mode(&self) -> CollaborationMode { pub(crate) fn collaboration_mode(&self) -> CollaborationMode {
CollaborationMode { CollaborationMode {
mode: self.mode, mode: self.mode,
+10 -6
View File
@@ -31,6 +31,7 @@ use codex_utils_path_uri::PathUri;
use core_test_support::PathBufExt; use core_test_support::PathBufExt;
use core_test_support::PathExt; use core_test_support::PathExt;
use core_test_support::responses::mount_models_once; use core_test_support::responses::mount_models_once;
use core_test_support::responses::strip_response_item_ids_from_json;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use std::time::Duration; use std::time::Duration;
use tempfile::tempdir; use tempfile::tempdir;
@@ -1782,8 +1783,9 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
rollout_items rollout_items
.iter() .iter()
.filter(|item| { .filter(|item| {
serde_json::to_value(item).expect("serialize rollout item") strip_response_item_ids_from_json(
== interrupted_marker_json serde_json::to_value(item).expect("serialize rollout item"),
) == interrupted_marker_json
}) })
.count(), .count(),
1, 1,
@@ -1986,8 +1988,9 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
forked_rollout_items forked_rollout_items
.iter() .iter()
.filter(|item| { .filter(|item| {
serde_json::to_value(item).expect("serialize forked rollout item") strip_response_item_ids_from_json(
== interrupted_marker_json serde_json::to_value(item).expect("serialize forked rollout item"),
) == interrupted_marker_json
}) })
.count(), .count(),
1, 1,
@@ -2021,8 +2024,9 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
reforked_rollout_items reforked_rollout_items
.iter() .iter()
.filter(|item| { .filter(|item| {
serde_json::to_value(item).expect("serialize re-forked rollout item") strip_response_item_ids_from_json(
== interrupted_marker_json serde_json::to_value(item).expect("serialize re-forked rollout item"),
) == interrupted_marker_json
}) })
.count(), .count(),
1, 1,
@@ -183,6 +183,8 @@ mod tests {
use codex_tools::ExtensionTurnItem; use codex_tools::ExtensionTurnItem;
use codex_utils_absolute_path::test_support::PathExt; use codex_utils_absolute_path::test_support::PathExt;
use codex_utils_absolute_path::test_support::test_path_buf; use codex_utils_absolute_path::test_support::test_path_buf;
use core_test_support::responses::strip_response_item_id;
use core_test_support::responses::strip_response_item_ids;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serde_json::json; use serde_json::json;
use tokio::sync::Mutex; use tokio::sync::Mutex;
@@ -357,7 +359,10 @@ mod tests {
let EventMsg::RawResponseItem(raw_history_item) = raw_history_event.msg else { let EventMsg::RawResponseItem(raw_history_item) = raw_history_event.msg else {
panic!("expected raw response item event"); panic!("expected raw response item event");
}; };
assert_eq!(raw_history_item.item, expected_history_item); assert_eq!(
strip_response_item_id(raw_history_item.item),
expected_history_item
);
let step_context = StepContext::for_test(Arc::clone(&turn)); let step_context = StepContext::for_test(Arc::clone(&turn));
let invocation = ToolInvocation { let invocation = ToolInvocation {
session, session,
@@ -397,8 +402,8 @@ mod tests {
expected_sandbox_cwds expected_sandbox_cwds
); );
assert_eq!( assert_eq!(
captured_call.conversation_history.items(), strip_response_item_ids(captured_call.conversation_history.items()),
std::slice::from_ref(&expected_history_item) vec![expected_history_item]
); );
match captured_call.payload { match captured_call.payload {
ToolPayload::Function { arguments } => { ToolPayload::Function { arguments } => {
+4 -3
View File
@@ -26,6 +26,7 @@ use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ToolName; use codex_tools::ToolName;
use codex_tools::ToolSpec; use codex_tools::ToolSpec;
use codex_tools::default_namespace_description; use codex_tools::default_namespace_description;
use core_test_support::responses::strip_response_item_ids_from_json;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serde_json::json; use serde_json::json;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@@ -439,13 +440,13 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
let value: serde_json::Value = let value: serde_json::Value =
serde_json::from_str(&text).expect("extension tool output should be json"); serde_json::from_str(&text).expect("extension tool output should be json");
assert_eq!( assert_eq!(
value, strip_response_item_ids_from_json(value),
json!({ strip_response_item_ids_from_json(json!({
"arguments": { "message": "hello" }, "arguments": { "message": "hello" },
"callId": "call-extension", "callId": "call-extension",
"conversationHistory": [expected_history_item], "conversationHistory": [expected_history_item],
"ok": true, "ok": true,
}) }))
); );
} }
other => panic!("expected function call output, got {other:?}"), other => panic!("expected function call output, got {other:?}"),
@@ -5,6 +5,7 @@ use similar::TextDiff;
use std::sync::OnceLock; use std::sync::OnceLock;
use crate::responses::ResponsesRequest; use crate::responses::ResponsesRequest;
use crate::responses::strip_response_item_ids_from_json;
use codex_protocol::protocol::APPS_INSTRUCTIONS_OPEN_TAG; use codex_protocol::protocol::APPS_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_OPEN_TAG; use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG; use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
@@ -25,6 +26,7 @@ pub struct ContextSnapshotOptions {
render_mode: ContextSnapshotRenderMode, render_mode: ContextSnapshotRenderMode,
strip_capability_instructions: bool, strip_capability_instructions: bool,
strip_agents_md_user_context: bool, strip_agents_md_user_context: bool,
strip_response_item_ids: bool,
} }
impl Default for ContextSnapshotOptions { impl Default for ContextSnapshotOptions {
@@ -33,6 +35,7 @@ impl Default for ContextSnapshotOptions {
render_mode: ContextSnapshotRenderMode::RedactedText, render_mode: ContextSnapshotRenderMode::RedactedText,
strip_capability_instructions: false, strip_capability_instructions: false,
strip_agents_md_user_context: false, strip_agents_md_user_context: false,
strip_response_item_ids: false,
} }
} }
} }
@@ -52,6 +55,11 @@ impl ContextSnapshotOptions {
self.strip_agents_md_user_context = true; self.strip_agents_md_user_context = true;
self self
} }
pub fn strip_response_item_ids(mut self) -> Self {
self.strip_response_item_ids = true;
self
}
} }
pub fn format_request_input_snapshot( pub fn format_request_input_snapshot(
@@ -267,6 +275,11 @@ fn format_request_body_snapshot(
options: &ContextSnapshotOptions, options: &ContextSnapshotOptions,
) -> String { ) -> String {
let mut body = crate::responses::strip_metadata_from_json(request.body_json()); let mut body = crate::responses::strip_metadata_from_json(request.body_json());
if options.strip_response_item_ids
&& let Some(input) = body.get_mut("input")
{
*input = strip_response_item_ids_from_json(std::mem::take(input));
}
canonicalize_json_snapshot_value(&mut body, options); canonicalize_json_snapshot_value(&mut body, options);
serde_json::to_string_pretty(&body).expect("request body should serialize") serde_json::to_string_pretty(&body).expect("request body should serialize")
} }
+37
View File
@@ -109,6 +109,43 @@ pub fn strip_metadata_from_items(items: &[ResponseItem]) -> Vec<ResponseItem> {
items.iter().cloned().map(strip_metadata).collect() items.iter().cloned().map(strip_metadata).collect()
} }
/// Returns a response item without its Responses API item ID for semantic assertions.
pub fn strip_response_item_id(mut item: ResponseItem) -> ResponseItem {
item.set_id(/*new_id*/ None);
item
}
/// Returns response items without their Responses API item IDs for semantic assertions.
pub fn strip_response_item_ids(items: &[ResponseItem]) -> Vec<ResponseItem> {
items.iter().cloned().map(strip_response_item_id).collect()
}
/// Returns JSON without IDs on recognized Responses API items.
pub fn strip_response_item_ids_from_json(value: Value) -> Value {
match value {
Value::Array(values) => Value::Array(
values
.into_iter()
.map(strip_response_item_ids_from_json)
.collect(),
),
Value::Object(mut map) => {
let is_response_item =
serde_json::from_value::<ResponseItem>(Value::Object(map.clone()))
.is_ok_and(|item| !matches!(item, ResponseItem::Other));
if is_response_item {
map.remove("id");
}
Value::Object(
map.into_iter()
.map(|(key, value)| (key, strip_response_item_ids_from_json(value)))
.collect(),
)
}
value => value,
}
}
/// Returns JSON without internal transport metadata for semantic assertions. /// Returns JSON without internal transport metadata for semantic assertions.
pub fn strip_metadata_from_json(value: Value) -> Value { pub fn strip_metadata_from_json(value: Value) -> Value {
match value { match value {
-3
View File
@@ -130,7 +130,6 @@ async fn responses_stream_includes_subagent_header_on_review() {
/*enable_request_compression*/ false, /*enable_request_compression*/ false,
/*include_timing_metrics*/ false, /*include_timing_metrics*/ false,
/*beta_features_header*/ None, /*beta_features_header*/ None,
/*item_ids_enabled*/ false,
/*concurrent_reasoning_summaries_enabled*/ false, /*concurrent_reasoning_summaries_enabled*/ false,
/*attestation_provider*/ None, /*attestation_provider*/ None,
config.http_client_factory(), config.http_client_factory(),
@@ -266,7 +265,6 @@ async fn responses_stream_includes_subagent_header_on_other() {
/*enable_request_compression*/ false, /*enable_request_compression*/ false,
/*include_timing_metrics*/ false, /*include_timing_metrics*/ false,
/*beta_features_header*/ None, /*beta_features_header*/ None,
/*item_ids_enabled*/ false,
/*concurrent_reasoning_summaries_enabled*/ false, /*concurrent_reasoning_summaries_enabled*/ false,
/*attestation_provider*/ None, /*attestation_provider*/ None,
config.http_client_factory(), config.http_client_factory(),
@@ -387,7 +385,6 @@ async fn responses_respects_model_info_overrides_from_config() {
/*enable_request_compression*/ false, /*enable_request_compression*/ false,
/*include_timing_metrics*/ false, /*include_timing_metrics*/ false,
/*beta_features_header*/ None, /*beta_features_header*/ None,
/*item_ids_enabled*/ false,
/*concurrent_reasoning_summaries_enabled*/ false, /*concurrent_reasoning_summaries_enabled*/ false,
/*attestation_provider*/ None, /*attestation_provider*/ None,
config.http_client_factory(), config.http_client_factory(),
@@ -257,6 +257,7 @@ async fn websocket_v2_test_codex_shell_chain() -> Result<()> {
let call_id = "shell-command-call"; let call_id = "shell-command-call";
let mut shell_command_call = ev_shell_command_call(call_id, "echo websocket"); let mut shell_command_call = ev_shell_command_call(call_id, "echo websocket");
shell_command_call["item"]["id"] = serde_json::json!("fc_shell_command_call");
shell_command_call["item"]["internal_chat_message_metadata_passthrough"] = shell_command_call["item"]["internal_chat_message_metadata_passthrough"] =
serde_json::json!({"turn_id": "turn-123"}); serde_json::json!({"turn_id": "turn-123"});
let server = start_websocket_server(vec![vec![ let server = start_websocket_server(vec![vec![
+9 -19
View File
@@ -71,6 +71,7 @@ use core_test_support::responses::mount_sse_sequence;
use core_test_support::responses::sse; use core_test_support::responses::sse;
use core_test_support::responses::sse_failed; use core_test_support::responses::sse_failed;
use core_test_support::responses::strip_metadata_from_json; use core_test_support::responses::strip_metadata_from_json;
use core_test_support::responses::strip_response_item_ids_from_json;
use core_test_support::responses_metadata as test_responses_metadata; use core_test_support::responses_metadata as test_responses_metadata;
use core_test_support::skip_if_no_network; use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::TestCodex;
@@ -250,7 +251,7 @@ async fn openai_stateless_responses_requests_preserve_item_turn_metadata_across_
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn non_openai_responses_requests_omit_item_passthrough_metadata() { async fn non_openai_responses_requests_include_item_ids_without_passthrough_metadata() {
let server = MockServer::start().await; let server = MockServer::start().await;
let response_mock = mount_sse_once( let response_mock = mount_sse_once(
&server, &server,
@@ -299,8 +300,8 @@ async fn non_openai_responses_requests_omit_item_passthrough_metadata() {
"input item should omit internal chat message metadata passthrough: {item}" "input item should omit internal chat message metadata passthrough: {item}"
); );
assert!( assert!(
item.get("id").is_none(), item.get("id").and_then(serde_json::Value::as_str).is_some(),
"input item should omit generated IDs: {item}" "input item should include a generated ID: {item}"
); );
} }
} }
@@ -433,9 +434,7 @@ async fn response_item_ids_persist_across_resume_and_preserve_server_ids() -> an
], ],
) )
.await; .await;
let mut builder = test_codex().with_config(|config| { let mut builder = test_codex();
let _ = config.features.enable(Feature::ItemIds);
});
let initial = builder.build(&server).await?; let initial = builder.build(&server).await?;
let home = Arc::clone(&initial.home); let home = Arc::clone(&initial.home);
let rollout_path = initial let rollout_path = initial
@@ -451,9 +450,6 @@ async fn response_item_ids_persist_across_resume_and_preserve_server_ids() -> an
}) })
.await; .await;
builder = builder.with_config(|config| {
let _ = config.features.enable(Feature::ItemIds);
});
let resumed = builder.resume(&server, home, rollout_path).await?; let resumed = builder.resume(&server, home, rollout_path).await?;
resumed.submit_turn("after resume").await?; resumed.submit_turn("after resume").await?;
@@ -532,9 +528,7 @@ async fn synthetic_call_output_id_is_stable_across_resumes() -> anyhow::Result<(
) )
.await; .await;
let codex_home = Arc::new(TempDir::new()?); let codex_home = Arc::new(TempDir::new()?);
let mut builder = test_codex().with_config(|config| { let mut builder = test_codex();
let _ = config.features.enable(Feature::ItemIds);
});
let first = builder let first = builder
.resume(&server, Arc::clone(&codex_home), session_path.clone()) .resume(&server, Arc::clone(&codex_home), session_path.clone())
.await?; .await?;
@@ -550,9 +544,6 @@ async fn synthetic_call_output_id_is_stable_across_resumes() -> anyhow::Result<(
"prompt-only repair should not be persisted to the rollout" "prompt-only repair should not be persisted to the rollout"
); );
builder = builder.with_config(|config| {
let _ = config.features.enable(Feature::ItemIds);
});
let second = builder.resume(&server, codex_home, session_path).await?; let second = builder.resume(&server, codex_home, session_path).await?;
second.submit_turn("second resume").await?; second.submit_turn("second resume").await?;
@@ -606,7 +597,6 @@ async fn response_item_ids_are_sent_for_all_remote_v2_compaction_requests() -> a
let test = test_codex() let test = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(|config| { .with_config(|config| {
let _ = config.features.enable(Feature::ItemIds);
let _ = config.features.enable(Feature::RemoteCompactionV2); let _ = config.features.enable(Feature::RemoteCompactionV2);
}) })
.build(&server) .build(&server)
@@ -1503,7 +1493,6 @@ async fn send_request_with_provider(provider: ModelProviderInfo) {
/*enable_request_compression*/ false, /*enable_request_compression*/ false,
/*include_timing_metrics*/ false, /*include_timing_metrics*/ false,
/*beta_features_header*/ None, /*beta_features_header*/ None,
/*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
/*concurrent_reasoning_summaries_enabled*/ /*concurrent_reasoning_summaries_enabled*/
config config
.features .features
@@ -3236,7 +3225,6 @@ async fn azure_responses_request_includes_store_and_prefixed_item_ids() {
/*enable_request_compression*/ false, /*enable_request_compression*/ false,
/*include_timing_metrics*/ false, /*include_timing_metrics*/ false,
/*beta_features_header*/ None, /*beta_features_header*/ None,
/*item_ids_enabled*/ false,
/*concurrent_reasoning_summaries_enabled*/ false, /*concurrent_reasoning_summaries_enabled*/ false,
/*attestation_provider*/ None, /*attestation_provider*/ None,
config.http_client_factory(), config.http_client_factory(),
@@ -4107,7 +4095,9 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() {
let tail_len = r3_tail_expected.as_array().unwrap().len(); let tail_len = r3_tail_expected.as_array().unwrap().len();
let actual_tail = &r3_input_array[r3_input_array.len() - tail_len..]; let actual_tail = &r3_input_array[r3_input_array.len() - tail_len..];
assert_eq!( assert_eq!(
strip_metadata_from_json(serde_json::Value::Array(actual_tail.to_vec())), strip_response_item_ids_from_json(strip_metadata_from_json(serde_json::Value::Array(
actual_tail.to_vec(),
))),
r3_tail_expected, r3_tail_expected,
"request 3 tail mismatch", "request 3 tail mismatch",
); );
@@ -161,8 +161,7 @@ async fn responses_websocket_streams_request() {
let harness = websocket_harness(&server).await; let harness = websocket_harness(&server).await;
let mut client_session = harness.client.new_session(); let mut client_session = harness.client.new_session();
let mut prompt = prompt_with_input(vec![message_item("hello")]); let prompt = prompt_with_input(vec![message_item("hello")]);
prompt.input[0].set_id(Some(ResponseItemId::with_suffix("msg", "existing")));
stream_until_complete(&mut client_session, &harness, &prompt).await; stream_until_complete(&mut client_session, &harness, &prompt).await;
@@ -174,7 +173,6 @@ async fn responses_websocket_streams_request() {
assert_eq!(body["model"].as_str(), Some(MODEL)); assert_eq!(body["model"].as_str(), Some(MODEL));
assert_eq!(body["stream"], serde_json::Value::Bool(true)); assert_eq!(body["stream"], serde_json::Value::Bool(true));
assert_eq!(body["input"].as_array().map(Vec::len), Some(1)); assert_eq!(body["input"].as_array().map(Vec::len), Some(1));
assert_eq!(body["input"][0].get("id"), None);
let handshake = server.single_handshake(); let handshake = server.single_handshake();
assert_eq!( assert_eq!(
handshake.header(OPENAI_BETA_HEADER), handshake.header(OPENAI_BETA_HEADER),
@@ -225,7 +223,7 @@ async fn responses_websocket_omits_unprefixed_item_ids_without_mutating_prompt()
websocket_provider(&server), websocket_provider(&server),
/*runtime_metrics_enabled*/ false, /*runtime_metrics_enabled*/ false,
/*concurrent_reasoning_summaries_enabled*/ false, /*concurrent_reasoning_summaries_enabled*/ false,
/*enabled_features*/ &[Feature::ItemIds], /*enabled_features*/ &[],
) )
.await; .await;
let mut client_session = harness.client.new_session(); let mut client_session = harness.client.new_session();
@@ -2389,7 +2387,6 @@ async fn websocket_harness_with_provider_options(
/*enable_request_compression*/ false, /*enable_request_compression*/ false,
runtime_metrics_enabled, runtime_metrics_enabled,
/*beta_features_header*/ None, /*beta_features_header*/ None,
/*item_ids_enabled*/ config.features.enabled(Feature::ItemIds),
/*concurrent_reasoning_summaries_enabled*/ /*concurrent_reasoning_summaries_enabled*/
config config
.features .features
+6 -3
View File
@@ -34,6 +34,7 @@ use core_test_support::hooks::trust_discovered_hooks;
use core_test_support::responses; use core_test_support::responses;
use core_test_support::responses::ev_reasoning_item; use core_test_support::responses::ev_reasoning_item;
use core_test_support::responses::mount_models_once; use core_test_support::responses::mount_models_once;
use core_test_support::responses::strip_response_item_ids_from_json;
use core_test_support::skip_if_no_network; use core_test_support::skip_if_no_network;
use core_test_support::test_codex::local_selections; use core_test_support::test_codex::local_selections;
use core_test_support::test_codex::test_codex; use core_test_support::test_codex::test_codex;
@@ -1225,6 +1226,7 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
values values
.iter() .iter()
.filter_map(|value| { .filter_map(|value| {
let value = strip_response_item_ids_from_json(value.clone());
if value if value
.get("type") .get("type")
.and_then(|ty| ty.as_str()) .and_then(|ty| ty.as_str())
@@ -1251,9 +1253,9 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
return None; return None;
} }
if role == Some("user") { if role == Some("user") {
return strip_agents_parts_from_user_message(value); return strip_agents_parts_from_user_message(&value);
} }
Some(value.clone()) Some(value)
}) })
.collect() .collect()
} }
@@ -4078,7 +4080,8 @@ async fn auto_compact_allows_multiple_attempts_when_interleaved_with_other_turn_
let mut builder = test_codex().with_config(move |config| { let mut builder = test_codex().with_config(move |config| {
config.model_provider = model_provider; config.model_provider = model_provider;
set_test_compact_prompt(config); set_test_compact_prompt(config);
config.model_auto_compact_token_limit = Some(200); // Leave enough headroom for per-item request metadata before the second compaction.
config.model_auto_compact_token_limit = Some(300);
}); });
let codex = builder.build(&server).await.unwrap().codex; let codex = builder.build(&server).await.unwrap().codex;
+1 -1
View File
@@ -872,7 +872,7 @@ async fn assert_remote_manual_compact_request_parity(
&normal_request, &normal_request,
"Remote /responses/compact Request", "Remote /responses/compact Request",
&compact_request, &compact_request,
&ContextSnapshotOptions::default(), &ContextSnapshotOptions::default().strip_response_item_ids(),
) )
); );
@@ -14,6 +14,7 @@ use codex_protocol::user_input::UserInput;
use core_test_support::hooks::trust_discovered_hooks; use core_test_support::hooks::trust_discovered_hooks;
use core_test_support::responses; use core_test_support::responses;
use core_test_support::responses::ResponseMock; use core_test_support::responses::ResponseMock;
use core_test_support::responses::strip_response_item_ids_from_json;
use core_test_support::skip_if_no_network; use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodexHarness; use core_test_support::test_codex::TestCodexHarness;
use core_test_support::test_codex::test_codex; use core_test_support::test_codex::test_codex;
@@ -762,17 +763,17 @@ fn compact_request_view(body: &Value, mode: Mode) -> Value {
} }
let mut selected = selected_request_fields(body, SelectedFieldsMode::Compact); let mut selected = selected_request_fields(body, SelectedFieldsMode::Compact);
selected["input"] = normalize_value(Value::Array(input)); selected["input"] = normalize_value(strip_response_item_ids_from_json(Value::Array(input)));
canonical_json(&normalize_value(selected)) canonical_json(&normalize_value(selected))
} }
fn follow_up_request_view(body: &Value) -> Value { fn follow_up_request_view(body: &Value) -> Value {
let mut selected = selected_request_fields(body, SelectedFieldsMode::FollowUp); let mut selected = selected_request_fields(body, SelectedFieldsMode::FollowUp);
selected["input"] = normalize_value( selected["input"] = normalize_value(strip_response_item_ids_from_json(
body.get("input") body.get("input")
.cloned() .cloned()
.expect("follow-up request should include input"), .expect("follow-up request should include input"),
); ));
canonical_json(&normalize_value(selected)) canonical_json(&normalize_value(selected))
} }
@@ -800,7 +801,9 @@ fn replacement_history_from_rollout(path: &Path) -> Result<Value> {
} }
let replacement_history = let replacement_history =
replacement_history.expect("expected compacted rollout replacement history"); replacement_history.expect("expected compacted rollout replacement history");
Ok(canonical_json(&normalize_value(replacement_history))) Ok(canonical_json(&normalize_value(
strip_response_item_ids_from_json(replacement_history),
)))
} }
fn write_manual_compact_hooks(home: &Path) { fn write_manual_compact_hooks(home: &Path) {
+4 -2
View File
@@ -16,6 +16,8 @@ use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_response_created; use core_test_support::responses::ev_response_created;
use core_test_support::responses::sse; use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server; use core_test_support::responses::start_mock_server;
use core_test_support::responses::strip_metadata;
use core_test_support::responses::strip_response_item_id;
use core_test_support::skip_if_no_network; use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::local_selections; use core_test_support::test_codex::local_selections;
@@ -181,7 +183,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}; };
assert_eq!(responses::strip_metadata(actual), expected); assert_eq!(strip_response_item_id(strip_metadata(actual)), expected);
Ok(()) Ok(())
} }
@@ -272,7 +274,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()>
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}; };
assert_eq!(responses::strip_metadata(actual), expected); assert_eq!(strip_response_item_id(strip_metadata(actual)), expected);
Ok(()) Ok(())
} }
+1 -6
View File
@@ -253,12 +253,7 @@ async fn missing_streamed_reasoning_id_is_reused_for_completion() -> anyhow::Res
skip_if_no_network!(Ok(())); skip_if_no_network!(Ok(()));
let server = start_mock_server().await; let server = start_mock_server().await;
let TestCodex { codex, .. } = test_codex() let TestCodex { codex, .. } = test_codex().build_with_auto_env(&server).await?;
.with_config(|config| {
let _ = config.features.enable(Feature::ItemIds);
})
.build_with_auto_env(&server)
.await?;
let mut reasoning_added = ev_reasoning_item_added("unused", &[]); let mut reasoning_added = ev_reasoning_item_added("unused", &[]);
reasoning_added["item"] reasoning_added["item"]
@@ -681,11 +681,6 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> {
1, 1,
"expected generated image history to be replayed as an image_generation_call" "expected generated image history to be replayed as an image_generation_call"
); );
assert_eq!(
image_generation_calls[0]["id"].as_str(),
None,
"expected the image generation call id to be omitted"
);
assert_eq!( assert_eq!(
image_generation_calls[0]["result"].as_str(), image_generation_calls[0]["result"].as_str(),
Some("Zm9v"), Some("Zm9v"),
@@ -786,11 +781,6 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima
image_generation_calls.len() == 1, image_generation_calls.len() == 1,
"second request should preserve the generated image call for text-only models" "second request should preserve the generated image call for text-only models"
); );
assert_eq!(
image_generation_calls[0]["id"].as_str(),
None,
"second request should omit the generated image call id"
);
assert_eq!( assert_eq!(
image_generation_calls[0]["result"].as_str(), image_generation_calls[0]["result"].as_str(),
Some(""), Some(""),
+16 -12
View File
@@ -26,6 +26,7 @@ use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse; use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server; use core_test_support::responses::start_mock_server;
use core_test_support::responses::strip_metadata_from_json; use core_test_support::responses::strip_metadata_from_json;
use core_test_support::responses::strip_response_item_ids_from_json;
use core_test_support::skip_if_no_network; use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::local_selections; use core_test_support::test_codex::local_selections;
@@ -55,10 +56,10 @@ fn text_user_input_parts(texts: Vec<String>) -> serde_json::Value {
}) })
} }
fn assert_eq_without_metadata(left: serde_json::Value, right: serde_json::Value) { fn assert_eq_without_metadata_or_item_ids(left: serde_json::Value, right: serde_json::Value) {
assert_eq!( assert_eq!(
strip_metadata_from_json(left), strip_response_item_ids_from_json(strip_metadata_from_json(left)),
strip_metadata_from_json(right) strip_response_item_ids_from_json(strip_metadata_from_json(right))
); );
} }
@@ -392,15 +393,18 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
Some("input_text"), Some("input_text"),
"expected environment context bundled after UI message in cached contextual message" "expected environment context bundled after UI message in cached contextual message"
); );
assert_eq_without_metadata(input1[2].clone(), text_user_input("hello 1".to_string())); assert_eq_without_metadata_or_item_ids(
input1[2].clone(),
text_user_input("hello 1".to_string()),
);
let body2 = req2.single_request().body_json(); let body2 = req2.single_request().body_json();
let input2 = body2["input"].as_array().expect("input array"); let input2 = body2["input"].as_array().expect("input array");
assert_eq_without_metadata( assert_eq_without_metadata_or_item_ids(
serde_json::Value::Array(input2[..input1.len()].to_vec()), serde_json::Value::Array(input2[..input1.len()].to_vec()),
serde_json::Value::Array(input1.to_vec()), serde_json::Value::Array(input1.to_vec()),
); );
assert_eq_without_metadata( assert_eq_without_metadata_or_item_ids(
input2[input1.len()].clone(), input2[input1.len()].clone(),
text_user_input("hello 2".to_string()), text_user_input("hello 2".to_string()),
); );
@@ -533,7 +537,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
expected_body2.push(expected_permissions_msg_2); expected_body2.push(expected_permissions_msg_2);
expected_body2.push(expected_env_msg_2); expected_body2.push(expected_env_msg_2);
expected_body2.push(expected_user_message_2); expected_body2.push(expected_user_message_2);
assert_eq_without_metadata( assert_eq_without_metadata_or_item_ids(
body2["input"].clone(), body2["input"].clone(),
serde_json::Value::Array(expected_body2), serde_json::Value::Array(expected_body2),
); );
@@ -823,7 +827,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
expected_body2.push(expected_permissions_update_msg); expected_body2.push(expected_permissions_update_msg);
expected_body2.push(expected_env_msg_2); expected_body2.push(expected_env_msg_2);
expected_body2.push(expected_user_message_2); expected_body2.push(expected_user_message_2);
assert_eq_without_metadata( assert_eq_without_metadata_or_item_ids(
body2["input"].clone(), body2["input"].clone(),
serde_json::Value::Array(expected_body2), serde_json::Value::Array(expected_body2),
); );
@@ -955,7 +959,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
expected_contextual_user_msg_1.clone(), expected_contextual_user_msg_1.clone(),
expected_user_message_1.clone(), expected_user_message_1.clone(),
]); ]);
assert_eq_without_metadata(body1["input"].clone(), expected_input_1); assert_eq_without_metadata_or_item_ids(body1["input"].clone(), expected_input_1);
let expected_user_message_2 = text_user_input("hello 2".to_string()); let expected_user_message_2 = text_user_input("hello 2".to_string());
let expected_input_2 = serde_json::Value::Array(vec![ let expected_input_2 = serde_json::Value::Array(vec![
@@ -964,7 +968,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
expected_user_message_1, expected_user_message_1,
expected_user_message_2, expected_user_message_2,
]); ]);
assert_eq_without_metadata(body2["input"].clone(), expected_input_2); assert_eq_without_metadata_or_item_ids(body2["input"].clone(), expected_input_2);
Ok(()) Ok(())
} }
@@ -1094,7 +1098,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
expected_contextual_user_msg_1.clone(), expected_contextual_user_msg_1.clone(),
expected_user_message_1.clone(), expected_user_message_1.clone(),
]); ]);
assert_eq_without_metadata(body1["input"].clone(), expected_input_1); assert_eq_without_metadata_or_item_ids(body1["input"].clone(), expected_input_1);
let body1_input = body1["input"].as_array().expect("input array"); let body1_input = body1["input"].as_array().expect("input array");
let expected_settings_update_msg = body2["input"][body1_input.len()].clone(); let expected_settings_update_msg = body2["input"][body1_input.len()].clone();
@@ -1139,7 +1143,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
expected_env_update_msg, expected_env_update_msg,
expected_user_message_2, expected_user_message_2,
]); ]);
assert_eq_without_metadata(body2["input"].clone(), expected_input_2); assert_eq_without_metadata_or_item_ids(body2["input"].clone(), expected_input_2);
Ok(()) Ok(())
} }
@@ -9,6 +9,7 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem; use codex_protocol::models::ResponseItem;
use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput;
use core_test_support::responses::strip_metadata; use core_test_support::responses::strip_metadata;
use core_test_support::responses::strip_response_item_id;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use tempfile::TempDir; use tempfile::TempDir;
@@ -53,7 +54,11 @@ async fn build_prompt_input_includes_context_and_user_message() -> Result<()> {
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}; };
assert_eq!( assert_eq!(
input.last().cloned().map(strip_metadata), input
.last()
.cloned()
.map(strip_metadata)
.map(strip_response_item_id),
Some(expected_user_message) Some(expected_user_message)
); );
assert!(input.iter().any(|item| { assert!(input.iter().any(|item| {
@@ -31,6 +31,7 @@ use core_test_support::responses::sse;
use core_test_support::responses::sse_response; use core_test_support::responses::sse_response;
use core_test_support::responses::start_mock_server; use core_test_support::responses::start_mock_server;
use core_test_support::responses::strip_metadata_from_json; use core_test_support::responses::strip_metadata_from_json;
use core_test_support::responses::strip_response_item_ids_from_json;
use core_test_support::skip_if_no_network; use core_test_support::skip_if_no_network;
use core_test_support::test_codex::TestCodex; use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::local_selections; use core_test_support::test_codex::local_selections;
@@ -1435,7 +1436,9 @@ async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result
sleep(Duration::from_millis(10)).await; sleep(Duration::from_millis(10)).await;
}; };
assert_eq!( assert_eq!(
strip_metadata_from_json(Value::Array(child_request.inputs_of_type("agent_message"))), strip_response_item_ids_from_json(strip_metadata_from_json(Value::Array(
child_request.inputs_of_type("agent_message"),
))),
Value::Array(vec![json!({ Value::Array(vec![json!({
"type": "agent_message", "type": "agent_message",
"author": "/root", "author": "/root",
@@ -1623,7 +1626,9 @@ async fn plaintext_multi_agent_v2_completion_sends_agent_message(
.pop() .pop()
.expect("agent message request"); .expect("agent message request");
assert_eq!( assert_eq!(
strip_metadata_from_json(Value::Array(request.inputs_of_type("agent_message"))), strip_response_item_ids_from_json(strip_metadata_from_json(Value::Array(
request.inputs_of_type("agent_message"),
))),
Value::Array(vec![json!({ Value::Array(vec![json!({
"type": "agent_message", "type": "agent_message",
"author": "/root/worker", "author": "/root/worker",
+7 -3
View File
@@ -27,6 +27,7 @@ use core_test_support::responses::mount_sse_once;
use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::mount_sse_sequence;
use core_test_support::responses::sse; use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server; use core_test_support::responses::start_mock_server;
use core_test_support::responses::strip_response_item_ids_from_json;
use core_test_support::skip_if_no_network; use core_test_support::skip_if_no_network;
use core_test_support::skip_if_sandbox; use core_test_support::skip_if_sandbox;
use core_test_support::test_codex::local; use core_test_support::test_codex::local;
@@ -226,9 +227,12 @@ async fn namespaced_custom_tool_call_preserves_namespace_through_dispatch_and_re
.map(str::to_string) .map(str::to_string)
.expect("custom tool call should include turn metadata"); .expect("custom tool call should include turn metadata");
assert_eq!( assert_eq!(
(custom_tool_calls, request.custom_tool_call_output(call_id),),
( (
vec![json!({ strip_response_item_ids_from_json(Value::Array(custom_tool_calls)),
strip_response_item_ids_from_json(request.custom_tool_call_output(call_id)),
),
(
Value::Array(vec![json!({
"type": "custom_tool_call", "type": "custom_tool_call",
"call_id": call_id, "call_id": call_id,
"namespace": namespace, "namespace": namespace,
@@ -237,7 +241,7 @@ async fn namespaced_custom_tool_call_preserves_namespace_through_dispatch_and_re
"internal_chat_message_metadata_passthrough": { "internal_chat_message_metadata_passthrough": {
"turn_id": turn_id, "turn_id": turn_id,
}, },
})], })]),
json!({ json!({
"type": "custom_tool_call_output", "type": "custom_tool_call_output",
"call_id": call_id, "call_id": call_id,
+4 -4
View File
@@ -206,7 +206,7 @@ pub enum Feature {
ImageGeneration, ImageGeneration,
/// Removed compatibility flag for always-on centralized image preparation. /// Removed compatibility flag for always-on centralized image preparation.
ResizeAllImages, ResizeAllImages,
/// Generate Responses API item IDs for client-created history items. /// Removed compatibility flag for always-on response item IDs.
ItemIds, ItemIds,
/// Request sequential cutoff reasoning summary delivery. /// Request sequential cutoff reasoning summary delivery.
ConcurrentReasoningSummaries, ConcurrentReasoningSummaries,
@@ -483,7 +483,7 @@ impl Features {
"tool_search" | "tool_search_always_defer_mcp_tools" | "apps_mcp_path_override" => { "tool_search" | "tool_search_always_defer_mcp_tools" | "apps_mcp_path_override" => {
continue; continue;
} }
"image_detail_original" | "resize_all_images" => { "image_detail_original" | "resize_all_images" | "item_ids" => {
continue; continue;
} }
"plugin_hooks" => { "plugin_hooks" => {
@@ -1198,8 +1198,8 @@ pub const FEATURES: &[FeatureSpec] = &[
FeatureSpec { FeatureSpec {
id: Feature::ItemIds, id: Feature::ItemIds,
key: "item_ids", key: "item_ids",
stage: Stage::UnderDevelopment, stage: Stage::Removed,
default_enabled: false, default_enabled: true,
}, },
FeatureSpec { FeatureSpec {
id: Feature::ConcurrentReasoningSummaries, id: Feature::ConcurrentReasoningSummaries,
+17
View File
@@ -274,6 +274,23 @@ fn from_sources_ignores_removed_resize_all_images_feature_key() {
assert_eq!(features, Features::with_defaults()); assert_eq!(features, Features::with_defaults());
} }
#[test]
fn from_sources_ignores_removed_item_ids_feature_key() {
let features_toml = FeaturesToml::from(BTreeMap::from([("item_ids".to_string(), false)]));
let features = Features::from_sources(
FeatureConfigSource {
features: Some(&features_toml),
..Default::default()
},
FeatureConfigSource::default(),
FeatureOverrides::default(),
);
assert_eq!(features, Features::with_defaults());
assert_eq!(features.enabled(Feature::ItemIds), true);
}
#[test] #[test]
fn from_sources_ignores_removed_undo_feature_key() { fn from_sources_ignores_removed_undo_feature_key() {
let features_toml = FeaturesToml::from(BTreeMap::from([("undo".to_string(), true)])); let features_toml = FeaturesToml::from(BTreeMap::from([("undo".to_string(), true)]));
-1
View File
@@ -260,7 +260,6 @@ impl MemoryStartupContext {
config.features.enabled(Feature::EnableRequestCompression), config.features.enabled(Feature::EnableRequestCompression),
config.features.enabled(Feature::RuntimeMetrics), config.features.enabled(Feature::RuntimeMetrics),
/*beta_features_header*/ None, /*beta_features_header*/ None,
config.features.enabled(Feature::ItemIds),
/*concurrent_reasoning_summaries_enabled*/ false, /*concurrent_reasoning_summaries_enabled*/ false,
/*attestation_provider*/ None, /*attestation_provider*/ None,
config.http_client_factory(), config.http_client_factory(),