Compare commits

...

1 Commits

Author SHA1 Message Date
Jiaming Zhang
1b57648d33 Propagate Codex installation id in Responses headers 2026-05-29 17:30:36 -07:00
4 changed files with 95 additions and 2 deletions

View File

@@ -507,7 +507,7 @@ impl ModelClient {
};
let mut extra_headers = ApiHeaderMap::new();
if let Ok(header_value) = HeaderValue::from_str(&self.state.installation_id) {
if let Some(header_value) = installation_id_header_value(&self.state.installation_id) {
extra_headers.insert(X_CODEX_INSTALLATION_ID_HEADER, header_value);
}
extra_headers.extend(build_responses_headers(
@@ -637,6 +637,9 @@ impl ModelClient {
fn build_responses_identity_headers(&self) -> ApiHeaderMap {
let mut extra_headers = self.build_subagent_headers();
if let Some(header_value) = installation_id_header_value(&self.state.installation_id) {
extra_headers.insert(X_CODEX_INSTALLATION_ID_HEADER, header_value);
}
if let Some(parent_thread_id) = parent_thread_id_header_value(&self.state.session_source)
&& let Ok(val) = HeaderValue::from_str(&parent_thread_id)
{
@@ -1664,6 +1667,17 @@ fn parse_turn_metadata_header(turn_metadata_header: Option<&str>) -> Option<Head
turn_metadata_header.and_then(|value| HeaderValue::from_str(value).ok())
}
fn installation_id_header_value(installation_id: &str) -> Option<HeaderValue> {
let installation_id = installation_id.trim();
if installation_id.is_empty()
|| installation_id.eq_ignore_ascii_case("none")
|| installation_id.eq_ignore_ascii_case("null")
{
return None;
}
HeaderValue::from_str(installation_id).ok()
}
/// Stamp a ResponsesWsRequest with the current time.
///
/// Meant to be called just before sending the request over the socket, to capture realistic

View File

@@ -11,6 +11,7 @@ use crate::AttestationContext;
use crate::AttestationProvider;
use crate::GenerateAttestationFuture;
use codex_api::ApiError;
use codex_api::Compression;
use codex_api::ResponseEvent;
use codex_app_server_protocol::AuthMode;
use codex_login::AuthManager;
@@ -61,13 +62,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,
@@ -311,6 +319,61 @@ fn build_ws_client_metadata_includes_window_lineage_and_turn_metadata() {
);
}
#[tokio::test]
async fn responses_options_include_installation_id_header() {
let client = test_model_client(SessionSource::Cli);
let client_session = client.new_session();
let options = client_session
.build_responses_options(/*turn_metadata_header*/ None, Compression::None)
.await;
assert_eq!(
options
.extra_headers
.get(X_CODEX_INSTALLATION_ID_HEADER)
.and_then(|value| value.to_str().ok()),
Some("11111111-1111-4111-8111-111111111111"),
);
}
#[tokio::test]
async fn websocket_headers_include_installation_id_header() {
let client = test_model_client(SessionSource::Cli);
let headers = client
.build_websocket_headers(/*turn_state*/ None, /*turn_metadata_header*/ None)
.await;
assert_eq!(
headers
.get(X_CODEX_INSTALLATION_ID_HEADER)
.and_then(|value| value.to_str().ok()),
Some("11111111-1111-4111-8111-111111111111"),
);
}
#[tokio::test]
async fn absent_installation_ids_do_not_add_headers() {
for installation_id in ["", "None", "null"] {
let client = test_model_client_with_installation_id(SessionSource::Cli, installation_id);
let client_session = client.new_session();
let options = client_session
.build_responses_options(/*turn_metadata_header*/ None, Compression::None)
.await;
assert_eq!(
options.extra_headers.get(X_CODEX_INSTALLATION_ID_HEADER),
None,
);
let headers = 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 summarize_memories_returns_empty_for_empty_input() {
let client = test_model_client(SessionSource::Cli);
@@ -546,6 +609,12 @@ async fn websocket_handshake_includes_attestation_for_chatgpt_codex_responses()
.and_then(|value| value.to_str().ok()),
Some("v1.header-1"),
);
assert_eq!(
headers
.get(X_CODEX_INSTALLATION_ID_HEADER)
.and_then(|value| value.to_str().ok()),
Some("11111111-1111-4111-8111-111111111111"),
);
assert_eq!(attestation_calls.load(Ordering::Relaxed), 1);
}

View File

@@ -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(),

View File

@@ -368,6 +368,12 @@ 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")
.is_some_and(|id| !id.is_empty()),
"compact requests should include the installation id header"
);
let compact_metadata: Value = serde_json::from_str(
&compact_request
.header("x-codex-turn-metadata")