diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 620645d8f6f..78dfee0b3c8 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -507,9 +507,6 @@ impl ModelClient { }; let mut extra_headers = ApiHeaderMap::new(); - if let Ok(header_value) = HeaderValue::from_str(&self.state.installation_id) { - extra_headers.insert(X_CODEX_INSTALLATION_ID_HEADER, header_value); - } extra_headers.extend(build_responses_headers( self.state.beta_features_header.as_deref(), /*turn_state*/ None, @@ -637,6 +634,11 @@ impl ModelClient { fn build_responses_identity_headers(&self) -> ApiHeaderMap { let mut extra_headers = self.build_subagent_headers(); + if !self.state.installation_id.is_empty() + && let Ok(val) = HeaderValue::from_str(&self.state.installation_id) + { + extra_headers.insert(X_CODEX_INSTALLATION_ID_HEADER, val); + } if let Some(parent_thread_id) = parent_thread_id_header_value(&self.state.session_source) && let Ok(val) = HeaderValue::from_str(&parent_thread_id) { diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index b9d9172c839..a57d3667bfe 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -61,13 +61,20 @@ use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::util::SubscriberInitExt; fn test_model_client(session_source: SessionSource) -> ModelClient { + test_model_client_with_installation_id(session_source, "11111111-1111-4111-8111-111111111111") +} + +fn test_model_client_with_installation_id( + session_source: SessionSource, + installation_id: &str, +) -> ModelClient { let provider = create_oss_provider_with_base_url("https://example.com/v1", WireApi::Responses); let thread_id = ThreadId::new(); ModelClient::new( /*auth_manager*/ None, thread_id.into(), thread_id, - /*installation_id*/ "11111111-1111-4111-8111-111111111111".to_string(), + /*installation_id*/ installation_id.to_string(), provider, session_source, /*model_verbosity*/ None, @@ -269,6 +276,23 @@ fn build_subagent_headers_sets_internal_memory_consolidation_label() { assert_eq!(value, Some("memory_consolidation")); } +#[test] +fn build_responses_identity_headers_includes_installation_id_when_present() { + let client = test_model_client(SessionSource::Exec); + let headers = client.build_responses_identity_headers(); + let value = headers + .get(X_CODEX_INSTALLATION_ID_HEADER) + .and_then(|value| value.to_str().ok()); + assert_eq!(value, Some("11111111-1111-4111-8111-111111111111")); +} + +#[test] +fn build_responses_identity_headers_omits_installation_id_when_empty() { + let client = test_model_client_with_installation_id(SessionSource::Exec, ""); + let headers = client.build_responses_identity_headers(); + assert_eq!(headers.get(X_CODEX_INSTALLATION_ID_HEADER), None); +} + #[test] fn build_ws_client_metadata_includes_window_lineage_and_turn_metadata() { let parent_thread_id = ThreadId::new(); @@ -549,6 +573,17 @@ async fn websocket_handshake_includes_attestation_for_chatgpt_codex_responses() assert_eq!(attestation_calls.load(Ordering::Relaxed), 1); } +#[tokio::test] +async fn websocket_handshake_omits_installation_id_when_empty() { + let model_client = test_model_client_with_installation_id(SessionSource::Exec, ""); + + let headers = model_client + .build_websocket_headers(/*turn_state*/ None, /*turn_metadata_header*/ None) + .await; + + assert_eq!(headers.get(X_CODEX_INSTALLATION_ID_HEADER), None); +} + #[tokio::test] async fn non_chatgpt_codex_endpoints_omit_attestation_generation() { let (model_client, attestation_calls) = diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 6f0429e6449..465103b9cb4 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -152,6 +152,10 @@ async fn responses_stream_includes_subagent_header_on_review() { request.header("x-codex-window-id").as_deref(), Some(expected_window_id.as_str()) ); + assert_eq!( + request.header("x-codex-installation-id").as_deref(), + Some(TEST_INSTALLATION_ID) + ); assert_eq!(request.header("x-codex-parent-thread-id"), None); assert_eq!( request.body_json()["client_metadata"]["x-codex-installation-id"].as_str(), diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index 09d0093f990..031261b8515 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -148,6 +148,10 @@ async fn responses_websocket_streams_request() { handshake.header(USER_AGENT_HEADER), Some(codex_login::default_client::get_codex_user_agent()) ); + assert_eq!( + handshake.header("x-codex-installation-id"), + Some(TEST_INSTALLATION_ID.to_string()) + ); assert_eq!( body["client_metadata"]["x-codex-installation-id"].as_str(), Some(TEST_INSTALLATION_ID) diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 1c5b80f35f9..99f13313049 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -368,6 +368,13 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { compact_request.header("thread-id").as_deref(), Some(thread_id.as_str()) ); + assert!( + compact_request + .header("x-codex-installation-id") + .as_deref() + .is_some_and(|value| !value.is_empty()), + "compact request should include a non-empty installation id header" + ); let compact_metadata: Value = serde_json::from_str( &compact_request .header("x-codex-turn-metadata")