From bd8fc9adb93fa5bc0a69b396bd5ac78a5ec14487 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Fri, 8 May 2026 17:11:19 +0200 Subject: [PATCH] api: send hyphenated session and thread headers (#21757) ## Why Some consumers expect conventional hyphenated HTTP headers. Codex already sends the session and thread IDs on outbound Responses requests, but it only uses the underscore spellings today, which makes those IDs harder to consume in systems that normalize or reject underscore header names. Full context here: https://openai.slack.com/archives/C08KCGLSPSQ/p1778248578422369 ## What changed - `build_session_headers` now emits both `session_id` and `session-id` when a session ID is present. - It does the same for `thread_id` and `thread-id`. - Added regression coverage in `codex-api/tests/clients.rs` and `core/tests/suite/client.rs` so both the lower-level client tests and the end-to-end request tests assert the two header spellings are present. ## Test plan - Added header assertions in `codex-api/tests/clients.rs`. - Added request-header assertions in `core/tests/suite/client.rs` for both the `/v1/responses` and `/api/codex/responses` request paths. --- codex-rs/codex-api/src/requests/headers.rs | 2 ++ codex-rs/codex-api/tests/clients.rs | 8 ++++++++ codex-rs/core/tests/suite/client.rs | 14 ++++++++++++++ .../tui/tests/suite/model_availability_nux.rs | 19 ++++++++++++++----- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/codex-rs/codex-api/src/requests/headers.rs b/codex-rs/codex-api/src/requests/headers.rs index d91d2a2bf..ed5a5ad64 100644 --- a/codex-rs/codex-api/src/requests/headers.rs +++ b/codex-rs/codex-api/src/requests/headers.rs @@ -6,9 +6,11 @@ pub fn build_session_headers(session_id: Option, thread_id: Option Result<()> { req.headers.get("session_id").and_then(|v| v.to_str().ok()), Some("sess_123") ); + assert_eq!( + 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("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/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index e72dfe3e1..d23af820f 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -764,7 +764,9 @@ async fn includes_session_id_thread_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_session_id_hyphenated = request.header("session-id").expect("session-id header"); let request_thread_id = request.header("thread_id").expect("thread_id header"); + let request_thread_id_hyphenated = request.header("thread-id").expect("thread-id header"); let request_authorization = request .header("authorization") .expect("authorization header"); @@ -776,7 +778,12 @@ async fn includes_session_id_thread_id_and_model_headers_in_request() { let thread_id_string = expected_thread_id.to_string(); assert_eq!(request_session_id, expected_session_id.to_string()); + assert_eq!( + request_session_id_hyphenated, + expected_session_id.to_string() + ); assert_eq!(request_thread_id, thread_id_string.as_str()); + assert_eq!(request_thread_id_hyphenated, thread_id_string.as_str()); assert_eq!(request_originator, originator().value); assert_eq!(request_authorization, "Bearer Test API Key"); assert_eq!( @@ -1038,12 +1045,19 @@ 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_session_id_hyphenated = request.header("session-id").expect("session-id header"); let request_thread_id = request.header("thread_id").expect("thread_id header"); + let request_thread_id_hyphenated = 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_session_id_hyphenated, + expected_session_id.to_string() + ); assert_eq!(request_thread_id, expected_thread_id.to_string()); + assert_eq!(request_thread_id_hyphenated, expected_thread_id.to_string()); assert_eq!(request_originator, originator().value); assert_eq!(request_authorization, "Bearer Access Token"); diff --git a/codex-rs/tui/tests/suite/model_availability_nux.rs b/codex-rs/tui/tests/suite/model_availability_nux.rs index 04a03a09f..5a384e828 100644 --- a/codex-rs/tui/tests/suite/model_availability_nux.rs +++ b/codex-rs/tui/tests/suite/model_availability_nux.rs @@ -177,15 +177,24 @@ trust_level = "trusted" } }; let output_text = String::from_utf8_lossy(&output); - let interrupt_only_output = { - let trimmed_output = output_text.trim(); - !trimmed_output.is_empty() - && trimmed_output + let rendered_output = { + let mut parser = vt100::Parser::new( + /*rows*/ 24, /*cols*/ 80, /*scrollback_len*/ 0, + ); + parser.process(&output); + parser.screen().contents() + }; + let interrupted_during_terminal_startup = { + let trimmed_output = rendered_output.trim(); + trimmed_output.is_empty() + || trimmed_output .chars() .all(|character| character == '^' || character == 'C' || character.is_whitespace()) }; anyhow::ensure!( - exit_code == 0 || exit_code == 130 || (exit_code == 1 && interrupt_only_output), + exit_code == 0 + || exit_code == 130 + || (exit_code == 1 && interrupted_during_terminal_startup), "unexpected exit code from codex resume: {exit_code}; output: {output_text}", );