Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions headroom/proxy/handlers/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -3059,6 +3059,14 @@ async def handle_openai_responses(

_pre_strip_count_resp = sum(1 for k in headers if k.lower().startswith("x-headroom-"))
headers = _strip_internal_headers(headers)
# Mirror the WS handler: never forward Codex's client-only lite header
# upstream. OpenAI rejects newer Codex models when it leaks, and the HTTP
# POST path (unlike the WS path) otherwise forwards request headers verbatim.
headers = {
key: value
for key, value in headers.items()
if key.lower() != _CODEX_RESPONSES_LITE_HEADER
}
log_outbound_headers(
forwarder="openai_responses",
stripped_count=_pre_strip_count_resp,
Expand Down
33 changes: 33 additions & 0 deletions tests/test_openai_codex_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,39 @@ def test_handle_openai_responses_routes_chatgpt_auth_to_backend_api(monkeypatch)
assert response.status_code == 200


def test_handle_openai_responses_strips_codex_lite_header_upstream(monkeypatch):
# OpenAI rejects newer Codex models when the client-only lite header leaks
# upstream. The HTTP POST path must drop it like the WS handler does, while
# leaving adjacent headers intact.
token = _jwt(
{
"https://api.openai.com/auth": {
"chatgpt_account_id": "acct-from-jwt",
}
}
)
request = _build_request(
{"model": "gpt-5.4", "input": "hello"},
{
"Authorization": f"Bearer {token}",
"X-OpenAI-Internal-Codex-Responses-Lite": "true",
"X-OpenAI-Debug": "keep-me",
},
)
handler = _DummyOpenAIHandler()

monkeypatch.setattr("headroom.tokenizers.get_tokenizer", lambda model: _DummyTokenizer())

response = anyio.run(handler.handle_openai_responses, request)

assert response.status_code == 200
assert handler.captured_request is not None
_method, _url, headers, _body = handler.captured_request
lowered = {k.lower(): v for k, v in headers.items()}
assert "x-openai-internal-codex-responses-lite" not in lowered
assert lowered.get("x-openai-debug") == "keep-me"


def test_handle_openai_responses_chatgpt_codex_timeout_fails_open(monkeypatch):
token = _jwt(
{
Expand Down
Loading