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.
This commit is contained in:
jif-oai
2026-05-08 17:11:19 +02:00
committed by GitHub
Unverified
parent e6312d44f0
commit bd8fc9adb9
4 changed files with 38 additions and 5 deletions
@@ -6,9 +6,11 @@ pub fn build_session_headers(session_id: Option<String>, thread_id: Option<Strin
let mut headers = HeaderMap::new();
if let Some(id) = session_id {
insert_header(&mut headers, "session_id", &id);
insert_header(&mut headers, "session-id", &id);
}
if let Some(id) = thread_id {
insert_header(&mut headers, "thread_id", &id);
insert_header(&mut headers, "thread-id", &id);
}
headers
}
+8
View File
@@ -462,10 +462,18 @@ 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("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")
+14
View File
@@ -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");
@@ -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}",
);