From d61bbf2821c8eee63e4c1a3361c2dbb2411de10c Mon Sep 17 00:00:00 2001 From: jif-oai Date: Fri, 1 May 2026 11:04:54 +0100 Subject: [PATCH] push --- codex-rs/codex-api/src/endpoint/responses.rs | 2 +- codex-rs/codex-api/src/requests/headers.rs | 5 ++++- codex-rs/codex-api/tests/clients.rs | 4 ++++ codex-rs/core/src/client.rs | 11 ++++++----- codex-rs/core/tests/suite/client.rs | 8 +++++++- codex-rs/core/tests/suite/client_websockets.rs | 14 +++++++++++++- codex-rs/core/tests/suite/compact_remote.rs | 10 ++++++++++ 7 files changed, 45 insertions(+), 9 deletions(-) diff --git a/codex-rs/codex-api/src/endpoint/responses.rs b/codex-rs/codex-api/src/endpoint/responses.rs index af71e58ad5..cc1be2846a 100644 --- a/codex-rs/codex-api/src/endpoint/responses.rs +++ b/codex-rs/codex-api/src/endpoint/responses.rs @@ -91,7 +91,7 @@ impl ResponsesClient { if let Some(ref thread_id) = thread_id { insert_header(&mut headers, "x-client-request-id", thread_id); } - headers.extend(build_session_headers(session_id)); + headers.extend(build_session_headers(session_id, thread_id)); if let Some(subagent) = subagent_header(&session_source) { insert_header(&mut headers, "x-openai-subagent", &subagent); } diff --git a/codex-rs/codex-api/src/requests/headers.rs b/codex-rs/codex-api/src/requests/headers.rs index d7d555b6ca..d91d2a2bf1 100644 --- a/codex-rs/codex-api/src/requests/headers.rs +++ b/codex-rs/codex-api/src/requests/headers.rs @@ -2,11 +2,14 @@ use codex_protocol::protocol::SessionSource; use http::HeaderMap; use http::HeaderValue; -pub fn build_session_headers(session_id: Option) -> HeaderMap { +pub fn build_session_headers(session_id: Option, thread_id: Option) -> HeaderMap { let mut headers = HeaderMap::new(); if let Some(id) = session_id { insert_header(&mut headers, "session_id", &id); } + if let Some(id) = thread_id { + insert_header(&mut headers, "thread_id", &id); + } headers } diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs index 6fdf861d80..a2a29ba16d 100644 --- a/codex-rs/codex-api/tests/clients.rs +++ b/codex-rs/codex-api/tests/clients.rs @@ -462,6 +462,10 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> { req.headers.get("session_id").and_then(|v| v.to_str().ok()), Some("sess_123") ); + assert_eq!( + req.headers.get("thread_id").and_then(|v| v.to_str().ok()), + Some("thread_123") + ); assert_eq!( req.headers .get("x-client-request-id") diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index c64ac95d16..ad807dc5dc 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -191,7 +191,7 @@ impl RequestRouteTelemetry { /// A session-scoped client for model-provider API calls. /// /// This holds configuration and state that should be shared across turns within a Codex session -/// (auth, provider selection, conversation id, and transport fallback state). +/// (auth, provider selection, thread id, and transport fallback state). /// /// WebSocket fallback is session-scoped: once a turn activates the HTTP fallback, subsequent turns /// will also use HTTP for the remainder of the session. @@ -476,9 +476,10 @@ impl ModelClient { extra_headers.insert(X_CODEX_INSTALLATION_ID_HEADER, header_value); } extra_headers.extend(self.build_responses_identity_headers()); - extra_headers.extend(build_session_headers(Some( - self.state.session_id.to_string(), - ))); + extra_headers.extend(build_session_headers( + Some(self.state.session_id.to_string()), + Some(self.state.thread_id.to_string()), + )); let trace_attempt = compaction_trace.start_attempt(&payload); let result = client .compact_input(&payload, extra_headers) @@ -800,7 +801,7 @@ impl ModelClient { if let Ok(header_value) = HeaderValue::from_str(&thread_id) { headers.insert("x-client-request-id", header_value); } - headers.extend(build_session_headers(Some(session_id))); + headers.extend(build_session_headers(Some(session_id), Some(thread_id))); headers.extend(self.build_responses_identity_headers()); headers.insert( OPENAI_BETA_HEADER, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index cb0b7a131c..edd2c3f075 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -724,7 +724,7 @@ async fn resume_replays_image_tool_outputs_with_detail() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn includes_session_id_and_model_headers_in_request() { +async fn includes_session_id_thread_id_and_model_headers_in_request() { skip_if_no_network!(); // Mock server @@ -743,6 +743,7 @@ async fn includes_session_id_and_model_headers_in_request() { .expect("create new conversation"); let codex = test.codex.clone(); let expected_session_id = test.session_configured.session_id; + let expected_thread_id = test.session_configured.thread_id; codex .submit(Op::UserInput { @@ -762,6 +763,7 @@ async fn includes_session_id_and_model_headers_in_request() { let request = resp_mock.single_request(); assert_eq!(request.path(), "/v1/responses"); let request_session_id = request.header("session_id").expect("session_id header"); + let request_thread_id = request.header("thread_id").expect("thread_id header"); let request_authorization = request .header("authorization") .expect("authorization header"); @@ -772,6 +774,7 @@ async fn includes_session_id_and_model_headers_in_request() { .expect("read installation id"); assert_eq!(request_session_id, expected_session_id.to_string()); + assert_eq!(request_thread_id, expected_thread_id.to_string()); assert_eq!(request_originator, originator().value); assert_eq!(request_authorization, "Bearer Test API Key"); assert_eq!( @@ -1000,6 +1003,7 @@ async fn chatgpt_auth_sends_correct_request() { .expect("create new conversation"); let codex = test.codex.clone(); let expected_session_id = test.session_configured.session_id; + let expected_thread_id = test.session_configured.thread_id; codex .submit(Op::UserInput { @@ -1028,10 +1032,12 @@ async fn chatgpt_auth_sends_correct_request() { let request_body = request.body_json(); let request_session_id = request.header("session_id").expect("session_id header"); + let request_thread_id = request.header("thread_id").expect("thread_id header"); let installation_id = std::fs::read_to_string(test.codex_home_path().join(INSTALLATION_ID_FILENAME)) .expect("read installation id"); assert_eq!(request_session_id, expected_session_id.to_string()); + assert_eq!(request_thread_id, expected_thread_id.to_string()); assert_eq!(request_originator, originator().value); assert_eq!(request_authorization, "Bearer Access Token"); diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index 921b53515d..7ef571af37 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -15,6 +15,7 @@ use codex_otel::MetricsConfig; use codex_otel::SessionTelemetry; use codex_otel::TelemetryAuthMode; use codex_otel::current_span_w3c_trace_context; +use codex_protocol::SessionId; use codex_protocol::ThreadId; use codex_protocol::account::PlanType; use codex_protocol::config_types::ReasoningSummary; @@ -87,6 +88,7 @@ fn assert_request_trace_matches(body: &serde_json::Value, expected_trace: &W3cTr struct WebsocketTestHarness { _codex_home: TempDir, client: ModelClient, + session_id: SessionId, thread_id: ThreadId, model_info: ModelInfo, effort: Option, @@ -127,6 +129,14 @@ async fn responses_websocket_streams_request() { handshake.header(X_CLIENT_REQUEST_ID_HEADER), Some(harness.thread_id.to_string()) ); + assert_eq!( + handshake.header("session_id"), + Some(harness.session_id.to_string()) + ); + assert_eq!( + handshake.header("thread_id"), + Some(harness.thread_id.to_string()) + ); assert_eq!( handshake.header(USER_AGENT_HEADER), Some(codex_login::default_client::get_codex_user_agent()) @@ -1828,6 +1838,7 @@ async fn websocket_harness_with_provider_options( let config = Arc::new(config); let model_info = codex_core::test_support::construct_model_info_offline(MODEL, &config); let thread_id = ThreadId::new(); + let session_id = SessionId::new(); let auth_manager = codex_core::test_support::auth_manager_from_auth(CodexAuth::from_api_key("Test API Key")); let exporter = InMemoryMetricExporter::default(); @@ -1853,7 +1864,7 @@ async fn websocket_harness_with_provider_options( let summary = ReasoningSummary::Auto; let client = ModelClient::new( /*auth_manager*/ None, - thread_id.into(), + session_id, thread_id, /*installation_id*/ TEST_INSTALLATION_ID.to_string(), provider.clone(), @@ -1867,6 +1878,7 @@ async fn websocket_harness_with_provider_options( WebsocketTestHarness { _codex_home: codex_home, client, + session_id, thread_id, model_info, effort, diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index b145506d86..d941d194d9 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -272,6 +272,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { .await?; let codex = harness.test().codex.clone(); let session_id = harness.test().session_configured.session_id.to_string(); + let thread_id = harness.test().session_configured.thread_id.to_string(); let responses_mock = responses::mount_sse_sequence( harness.server(), @@ -340,6 +341,10 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { compact_request.header("session_id").as_deref(), Some(session_id.as_str()) ); + assert_eq!( + compact_request.header("thread_id").as_deref(), + Some(thread_id.as_str()) + ); let compact_body = compact_request.body_json(); assert_eq!( compact_body.get("model").and_then(|v| v.as_str()), @@ -515,6 +520,7 @@ async fn remote_compact_runs_automatically() -> Result<()> { .await?; let codex = harness.test().codex.clone(); let session_id = harness.test().session_configured.session_id.to_string(); + let thread_id = harness.test().session_configured.thread_id.to_string(); mount_sse_once( harness.server(), @@ -566,6 +572,10 @@ async fn remote_compact_runs_automatically() -> Result<()> { .as_deref(), Some(session_id.as_str()) ); + assert_eq!( + compact_mock.single_request().header("thread_id").as_deref(), + Some(thread_id.as_str()) + ); let follow_up_request = responses_mock.single_request(); let follow_up_body = follow_up_request.body_json().to_string(); assert!(follow_up_body.contains("REMOTE_COMPACTED_SUMMARY"));