mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
Merge d285b4981a into 8884d87378
This commit is contained in:
commit
7c2a76f650
3 changed files with 100 additions and 0 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -8,6 +8,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Codex: `CLIENT_UA_MAP` matched a User-Agent no shipped Codex build sends.**
|
||||
The only OpenAI needle was `codex-cli/`; measured across two `proxy.log`
|
||||
rotations the Codex User-Agents actually on the wire were `codex-tui/` (3,167
|
||||
requests), `codex-browser-use/` (1,338), `Codex Desktop/` (368),
|
||||
`codex_cli_rs/` (13) and `codex-computer-use/` (1) — 4,887 requests, zero
|
||||
matches. Every Codex request therefore classified as `None`, so the Codex
|
||||
fail-open in `decide_compression_failure_action()`, which keys on
|
||||
`client == "codex"`, never fired and a compression timeout returned 413.
|
||||
|
||||
### Features
|
||||
- **proxy:** opt-in cost-aware model routing ([#1706](https://github.com/headroomlabs-ai/headroom/issues/1706)). Set `HEADROOM_MODEL_ROUTER_ENABLED=1` and `HEADROOM_MODEL_ROUTES` (a JSON array of ordered rules) to rewrite the upstream model based on estimated input size and tool presence, complementary to content compression, e.g. send small, tool-free requests to a cheaper model. First matching rule wins, and each decision is logged with a reason so routing stays observable. Malformed rules fail open (the rule is skipped, never silently widened). Disabled by default so behavior is unchanged, skipped under `x-headroom-bypass`/passthrough, and currently applied on the Anthropic `/v1/messages` path.
|
||||
- **install:** `headroom install apply` now accepts `--code-aware/--no-code-aware`, `--intercept-tool-results`, `--protect-tool-results`, and `--bedrock-profile`, mirroring the equivalent flags already on `headroom proxy`. Previously the only way to run a persistent deployment with these settings was to hand-edit `manifest.json` after the fact, which silently reverts on the next `install apply`.
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ CLIENT_UA_MAP: tuple[tuple[str, str], ...] = (
|
|||
("claude-vscode/", "claude-vscode"),
|
||||
("anthropic-cli/", "anthropic-cli"),
|
||||
("codex-cli/", "codex"),
|
||||
("codex-tui/", "codex"),
|
||||
("codex_cli_rs/", "codex"),
|
||||
("codex-browser-use/", "codex"),
|
||||
("codex-computer-use/", "codex"),
|
||||
("codex desktop/", "codex"),
|
||||
("cursor/", "cursor"),
|
||||
("grok/", "grok_build"),
|
||||
("zed/", "zed"),
|
||||
|
|
|
|||
84
tests/test_auth_mode_codex_user_agents.py
Normal file
84
tests/test_auth_mode_codex_user_agents.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
"""Codex User-Agent classification regression tests (AQ-0610).
|
||||
|
||||
``CLIENT_UA_MAP`` carried a single OpenAI needle, ``codex-cli/``, which no
|
||||
shipped Codex build actually sends. Every Codex request therefore classified as
|
||||
``None``, so the Codex-specific fail-open in ``headroom/proxy/helpers.py`` —
|
||||
which keys on ``client == "codex"`` — never fired and the proxy answered a
|
||||
compression timeout with 413. Codex treats 413 as fatal, so sessions died at
|
||||
startup.
|
||||
|
||||
The User-Agent strings below are verbatim from ~/.headroom/logs/proxy.log, not
|
||||
invented. Counts across two rotations at the time of the fix:
|
||||
codex-tui/ 3,167
|
||||
codex-browser-use/ 1,338
|
||||
Codex Desktop/ 368
|
||||
codex_cli_rs/ 13
|
||||
codex-computer-use/ 1
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from headroom.proxy.auth_mode import classify_client
|
||||
|
||||
# Verbatim User-Agents observed on the wire.
|
||||
OBSERVED_CODEX_USER_AGENTS = [
|
||||
"codex-tui/0.147.0 (Mac OS 26.5.1; arm64) Apple_Terminal/470.2 (codex-tui; 0.147.0)",
|
||||
"codex-tui/0.148.0-alpha.9 (Mac OS 26.5.1; arm64) xterm-256color",
|
||||
"codex_cli_rs/0.147.0 (Mac OS 26.5.1; arm64) Apple_Terminal/470.2",
|
||||
"codex_cli_rs/0.148.0-alpha.9 (Mac OS 26.5.1; arm64) unknown",
|
||||
"codex-browser-use/0.147.0-alpha.1.2 (Mac OS 26.5.1; arm64) xterm-256color"
|
||||
" (codex-browser-use; 0.1.0)",
|
||||
"Codex Desktop/0.147.0-alpha.6.5 (Mac OS 26.5.1; arm64) unknown",
|
||||
"Codex Desktop/0.148.0-alpha.9 (Mac OS 26.5.1; arm64) unknown (Codex Desktop; 26.810.41047)",
|
||||
"codex-computer-use/0.147.0-alpha.1.2 (Mac OS 26.5.1; arm64) unknown",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("user_agent", OBSERVED_CODEX_USER_AGENTS)
|
||||
def test_observed_codex_user_agents_classify_as_codex(user_agent: str) -> None:
|
||||
"""Every Codex build seen in production must resolve to ``codex``.
|
||||
|
||||
This is what gates the compression-timeout fail-open. A ``None`` here means
|
||||
a real Codex session gets a fatal 413 instead of an uncompressed forward.
|
||||
"""
|
||||
assert classify_client({"user-agent": user_agent}) == "codex"
|
||||
|
||||
|
||||
def test_codex_desktop_matches_despite_capitals_and_space() -> None:
|
||||
"""``Codex Desktop/`` is the only client whose UA contains a space.
|
||||
|
||||
Matching lowercases the UA, so the needle must be lowercase too — a
|
||||
``"Codex Desktop/"`` needle would silently never match.
|
||||
"""
|
||||
assert classify_client({"user-agent": "Codex Desktop/0.148.0-alpha.9 (Mac OS)"}) == "codex"
|
||||
|
||||
|
||||
def test_codex_ua_carrying_a_claude_code_originator_still_classifies_as_codex() -> None:
|
||||
"""Real UA that embeds both harness names.
|
||||
|
||||
``codex_cli_rs/... (claude-code; 2.1.233)`` is the Codex binary reporting
|
||||
Claude Code as its originator. The Anthropic needles are checked first and
|
||||
require ``claude-code/`` with a slash, so the semicolon form must not steal
|
||||
this match — the process talking to us is Codex, and Codex is the one that
|
||||
dies on a 413.
|
||||
"""
|
||||
ua = "codex_cli_rs/0.147.0 (Mac OS 26.5.1; arm64) Apple_Terminal/470.2 (claude-code; 2.1.233)"
|
||||
|
||||
assert classify_client({"user-agent": ua}) == "codex"
|
||||
|
||||
|
||||
def test_claude_code_user_agents_are_unaffected() -> None:
|
||||
"""Negative control: the Anthropic needles must keep winning their own UAs."""
|
||||
assert classify_client({"user-agent": "claude-cli/2.1.233 (external, cli)"}) == "claude-code"
|
||||
assert (
|
||||
classify_client({"user-agent": "claude-cli/2.1.233 (external, sdk-py, agent-sdk/0.2.139)"})
|
||||
== "claude-code"
|
||||
)
|
||||
|
||||
|
||||
def test_unrelated_user_agent_still_returns_none() -> None:
|
||||
"""Negative control: broadening the Codex needles must not swallow everything."""
|
||||
assert classify_client({"user-agent": "Mozilla/5.0 (Macintosh)"}) is None
|
||||
assert classify_client({"user-agent": "curl/8.7.1"}) is None
|
||||
Loading…
Add table
Add a link
Reference in a new issue