From 4aa52f2cb2cdffa9c826f9e4e421a5435a05af06 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Mon, 24 Aug 2026 18:07:13 +0000 Subject: [PATCH] Gate content item kinds behind a feature flag (#40450) ## What changed - Add the under-development `content_item_kinds` feature, disabled by default. - Strip content item classifications from Responses input when the feature is disabled while preserving other passthrough metadata. ## Testing - Add coverage that disables the feature and verifies requests omit content item kinds without removing the turn ID. GitOrigin-RevId: f938feb2895d766d979b1666fc7b97fd128fe8a7 --- codex-rs/core/config.schema.json | 6 ++++ codex-rs/core/src/client.rs | 6 ++++ codex-rs/core/src/client_tests.rs | 3 ++ codex-rs/core/src/session/session.rs | 1 + codex-rs/core/src/session/tests.rs | 3 ++ codex-rs/core/tests/common/test_codex.rs | 4 +++ codex-rs/core/tests/responses_headers.rs | 4 +++ codex-rs/core/tests/suite/agents_md.rs | 4 +++ codex-rs/core/tests/suite/client.rs | 2 ++ .../core/tests/suite/client_websockets.rs | 1 + .../core/tests/suite/context_annotations.rs | 36 +++++++++++++++++++ codex-rs/features/src/lib.rs | 8 +++++ codex-rs/memories/write/src/runtime.rs | 1 + codex-rs/protocol/src/models.rs | 15 ++++++++ 14 files changed, 94 insertions(+) diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 8e278bdcae..e492e0dcab 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -652,6 +652,9 @@ "connectors": { "type": "boolean" }, + "content_item_kinds": { + "type": "boolean" + }, "current_time_reminder": { "$ref": "#/definitions/FeatureToml_for_CurrentTimeReminderConfigToml" }, @@ -5628,6 +5631,9 @@ "connectors": { "type": "boolean" }, + "content_item_kinds": { + "type": "boolean" + }, "current_time_reminder": { "$ref": "#/definitions/FeatureToml_for_CurrentTimeReminderConfigToml" }, diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index dc0323cb9d..b3e85d18d6 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -208,6 +208,7 @@ struct ModelClientState { session_source: SessionSource, originator: String, model_verbosity: Option, + content_item_kinds_enabled: bool, enable_request_compression: bool, include_timing_metrics: bool, beta_features_header: Option, @@ -436,6 +437,7 @@ impl ModelClient { session_source: SessionSource, originator: String, model_verbosity: Option, + content_item_kinds_enabled: bool, enable_request_compression: bool, include_timing_metrics: bool, beta_features_header: Option, @@ -459,6 +461,7 @@ impl ModelClient { session_source, originator, model_verbosity, + content_item_kinds_enabled, enable_request_compression, include_timing_metrics, beta_features_header, @@ -963,6 +966,9 @@ impl ModelClient { if item.id().is_some_and(|id| !id.is_prefixed()) { item.set_id(/*new_id*/ None); } + if !self.state.content_item_kinds_enabled { + item.clear_content_item_kinds(); + } } } diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index 6249a7eba9..5b74e040b2 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -110,6 +110,7 @@ fn test_model_client_with_thread_id( session_source, "test_originator".to_string(), /*model_verbosity*/ None, + /*content_item_kinds_enabled*/ true, /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, @@ -155,6 +156,7 @@ async fn compact_uses_bearer_after_agent_identity_session_fallback() -> anyhow:: SessionSource::Cli, "test_originator".to_string(), /*model_verbosity*/ None, + /*content_item_kinds_enabled*/ true, /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, @@ -955,6 +957,7 @@ fn model_client_with_counting_attestation( SessionSource::Exec, "test_originator".to_string(), /*model_verbosity*/ None, + /*content_item_kinds_enabled*/ true, /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 5c27987a31..a20529a803 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -1432,6 +1432,7 @@ impl Session { session_configuration.session_source.clone(), session_configuration.originator.clone(), config.model_verbosity, + config.features.enabled(Feature::ContentItemKinds), config.features.enabled(Feature::EnableRequestCompression), config.features.enabled(Feature::RuntimeMetrics), Self::build_model_client_beta_features_header(config.as_ref()), diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 43491ab7af..1e9f1d552b 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -690,6 +690,7 @@ fn test_model_client_session() -> crate::client::ModelClientSession { codex_protocol::protocol::SessionSource::Exec, "test_originator".to_string(), /*model_verbosity*/ None, + /*content_item_kinds_enabled*/ true, /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, @@ -5991,6 +5992,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { session_configuration.session_source.clone(), session_configuration.originator.clone(), config.model_verbosity, + config.features.enabled(Feature::ContentItemKinds), config.features.enabled(Feature::EnableRequestCompression), config.features.enabled(Feature::RuntimeMetrics), Session::build_model_client_beta_features_header(config.as_ref()), @@ -8200,6 +8202,7 @@ where session_configuration.session_source.clone(), session_configuration.originator.clone(), config.model_verbosity, + config.features.enabled(Feature::ContentItemKinds), config.features.enabled(Feature::EnableRequestCompression), config.features.enabled(Feature::RuntimeMetrics), Session::build_model_client_beta_features_header(config.as_ref()), diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index de3a31b673..a25b835a90 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -1339,6 +1339,10 @@ pub fn test_codex() -> TestCodexBuilder { .features .disable(Feature::ShellSnapshot) .expect("test config should allow ShellSnapshot override"); + config + .features + .enable(Feature::ContentItemKinds) + .expect("test config should allow ContentItemKinds override"); })], auth: CodexAuth::from_api_key("dummy"), pre_build_hooks: vec![], diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 753cc04373..ff15c541c3 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use codex_core::ModelClient; use codex_core::Prompt; use codex_core::ResponseEvent; +use codex_features::Feature; use codex_login::CodexAuth; use codex_login::auth::AgentIdentityAuthPolicy; use codex_model_provider_info::ModelProviderInfo; @@ -128,6 +129,7 @@ async fn responses_stream_includes_subagent_header_on_review() { session_source.clone(), "test_originator".to_string(), config.model_verbosity, + config.features.enabled(Feature::ContentItemKinds), /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, @@ -264,6 +266,7 @@ async fn responses_stream_includes_subagent_header_on_other() { session_source.clone(), "test_originator".to_string(), config.model_verbosity, + config.features.enabled(Feature::ContentItemKinds), /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, @@ -385,6 +388,7 @@ async fn responses_respects_model_info_overrides_from_config() { session_source.clone(), "test_originator".to_string(), config.model_verbosity, + config.features.enabled(Feature::ContentItemKinds), /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, diff --git a/codex-rs/core/tests/suite/agents_md.rs b/codex-rs/core/tests/suite/agents_md.rs index fc9dd76325..297bc1d60f 100644 --- a/codex-rs/core/tests/suite/agents_md.rs +++ b/codex-rs/core/tests/suite/agents_md.rs @@ -1375,6 +1375,10 @@ async fn fork_injects_changed_agents_md_once() -> Result<()> { fork_config.model_provider = parent.config.model_provider.clone(); fork_config.model_catalog = parent.config.model_catalog.clone(); fork_config.codex_self_exe = parent.config.codex_self_exe.clone(); + fork_config + .features + .enable(Feature::ContentItemKinds) + .expect("test config should allow ContentItemKinds override"); let forked = parent .thread_manager .fork_thread( diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 4e9078082f..f459daccb0 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1524,6 +1524,7 @@ async fn send_request_with_provider(provider: ModelProviderInfo) { SessionSource::Exec, "test_originator".to_string(), config.model_verbosity, + config.features.enabled(Feature::ContentItemKinds), /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, @@ -3009,6 +3010,7 @@ async fn azure_responses_request_does_not_store_and_preserves_prefixed_item_ids( SessionSource::Exec, "test_originator".to_string(), config.model_verbosity, + config.features.enabled(Feature::ContentItemKinds), /*enable_request_compression*/ false, /*include_timing_metrics*/ false, /*beta_features_header*/ None, diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index c797cbdd4f..748cc07728 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -2514,6 +2514,7 @@ async fn websocket_harness_with_provider_options_and_auth( SessionSource::Exec, "test_originator".to_string(), config.model_verbosity, + config.features.enabled(Feature::ContentItemKinds), /*enable_request_compression*/ false, runtime_metrics_enabled, /*beta_features_header*/ None, diff --git a/codex-rs/core/tests/suite/context_annotations.rs b/codex-rs/core/tests/suite/context_annotations.rs index 7ef30023aa..ba43fd5bf8 100644 --- a/codex-rs/core/tests/suite/context_annotations.rs +++ b/codex-rs/core/tests/suite/context_annotations.rs @@ -215,3 +215,39 @@ async fn first_request_item_types_roles_and_content_annotations() -> Result<()> Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn content_item_kinds_are_omitted_when_feature_disabled() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let response = mount_sse_once( + &server, + sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), + ) + .await; + let test = test_codex() + .with_config(|config| { + config.developer_instructions = Some("Keep other metadata intact.".into()); + config + .features + .disable(Feature::ContentItemKinds) + .expect("test config should allow ContentItemKinds override"); + }) + .build_with_auto_env(&server) + .await?; + + test.submit_text_turn("inspect request metadata").await?; + + let input = response.single_request().input(); + assert!(input.iter().all(|item| { + item.pointer("/internal_chat_message_metadata_passthrough/content_item_kinds") + .is_none() + })); + assert!(input.iter().any(|item| { + item.pointer("/internal_chat_message_metadata_passthrough/turn_id") + .is_some() + })); + + Ok(()) +} diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 50a8921ad8..26443b6a8a 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -99,6 +99,8 @@ pub enum Feature { SecretAuthStorage, // Experimental + /// Send per-content-entry classifications in internal Responses metadata. + ContentItemKinds, /// Record model-attempted tool calls in internal Responses metadata. ExecutedToolCallMetadata, /// Enable JavaScript code mode backed by the standalone host process. @@ -905,6 +907,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Removed, default_enabled: false, }, + FeatureSpec { + id: Feature::ContentItemKinds, + key: "content_item_kinds", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::ExecutedToolCallMetadata, key: "executed_tool_call_metadata", diff --git a/codex-rs/memories/write/src/runtime.rs b/codex-rs/memories/write/src/runtime.rs index 4fc69e74b6..9ce806499e 100644 --- a/codex-rs/memories/write/src/runtime.rs +++ b/codex-rs/memories/write/src/runtime.rs @@ -257,6 +257,7 @@ impl MemoryStartupContext { session_source.clone(), config_snapshot.originator, config.model_verbosity, + config.features.enabled(Feature::ContentItemKinds), config.features.enabled(Feature::EnableRequestCompression), config.features.enabled(Feature::RuntimeMetrics), /*beta_features_header*/ None, diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index cc73736b27..e8bcab9c98 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -1316,6 +1316,21 @@ impl ResponseItem { } } + /// Removes content item classifications while preserving other passthrough metadata. + pub fn clear_content_item_kinds(&mut self) { + let Some(metadata) = self.internal_chat_message_metadata_passthrough_mut() else { + return; + }; + let Some(metadata_value) = metadata else { + return; + }; + + metadata_value.content_item_kinds = None; + if metadata_value == &InternalChatMessageMetadataPassthrough::default() { + *metadata = None; + } + } + fn internal_chat_message_metadata_passthrough( &self, ) -> Option<&InternalChatMessageMetadataPassthrough> {