Commit graph

13 commits

Author SHA1 Message Date
gglucass
b0cd0329c7
fix(proxy): stamp X-Client: codex on Responses endpoint for unidentified callers (#1036)
## Description

Codex Desktop (OpenAI's Codex GUI/IDE app) sends a `User-Agent` of the
form `Codex Desktop/<ver> (...)`, which is not in `CLIENT_UA_MAP`, so
`classify_client` returns `None`. On a compression timeout the backend
only takes the codex fail-open path when the client classifies as
`codex`; for an unidentified client it refuses with HTTP 413
(`compression_refused`), which Codex treats as a hard connection
failure.

This stamps `X-Client: codex` on requests to the Responses endpoint
(`/v1/responses`) only when the caller does not otherwise classify. The
stamp is scoped to the Responses endpoint and skipped for any caller
that already classifies through a recognized user-agent or explicit
`X-Client`, so non-Codex traffic is not relabeled.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [x] Tests only

## Changes Made

- Added `should_stamp_codex_client(path, headers)` in
`headroom.proxy.auth_mode` for narrow Responses-endpoint client
stamping.
- Applied the stamp in HTTP middleware before downstream request
classification.
- Applied the same stamp in the Responses WebSocket handler, which
bypasses HTTP middleware.
- Added unit coverage for the stamp/skip matrix, including Codex
Desktop, explicit clients, recognized user-agents, and WebSocket
behavior.
- Merged current `main` and kept both the new stamp coverage and the
existing Codex WebSocket image-generation regression coverage.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [x] Formatting passes (`ruff format --check`)
- [x] New tests added for new functionality

### Test Output

```text
python -m pytest tests/test_codex_client_stamp.py tests/test_auth_mode.py tests/test_openai_codex_ws_lifecycle.py -q
48 passed in 1.13s

ruff check headroom/proxy/auth_mode.py headroom/proxy/server.py headroom/proxy/handlers/openai.py tests/test_codex_client_stamp.py tests/test_auth_mode.py tests/test_openai_codex_ws_lifecycle.py
All checks passed!

ruff format --check headroom/proxy/auth_mode.py headroom/proxy/server.py headroom/proxy/handlers/openai.py tests/test_codex_client_stamp.py tests/test_auth_mode.py tests/test_openai_codex_ws_lifecycle.py
6 files already formatted
```

## Real Behavior Proof

- Environment: local Windows 11 development checkout, Python 3.13.13,
branch updated from `upstream/main`.
- Exact command / steps: Ran the focused unit suite for the new
client-stamp behavior and the overlapping Codex WebSocket lifecycle
tests, then ran `ruff check` and `ruff format --check` on the changed
modules and tests.
- Observed result: The focused suite passed with 48 tests, lint passed,
and formatting passed. The tests assert that unidentified
`/v1/responses` callers classify as Codex after stamping while explicit
or already-recognized clients are preserved.
- Not tested: a live end-to-end Codex Desktop session through a running
`headroom wrap codex` instance; verification is at the unit/integration
boundary for classification and request routing.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-06-17 11:45:20 -05:00
felixboenkost-droid
8662a82e8a
Fix Codex ChatGPT /v1/models compatibility metadata (#1048)
## Description

Fixes Codex ChatGPT/OAuth `/v1/models` metadata compatibility while
keeping Headroom's existing OpenAI-compatible response shape.

Headroom's ChatGPT/OAuth model-list route already returned:

- `object: "list"`
- `data[]`

Newer Codex clients also inspect a top-level `models[]` registry
metadata array. Without that shape, completions can still work, but
clients may emit non-fatal model metadata decode or missing-field
warnings before the follow-up `/v1/responses` call.

This PR keeps `object`/`data[]` unchanged and adds a Codex-compatible
`models[]` array. Upstream registry metadata is preserved where
available, and only missing fields are filled with defaults.

Closes: N/A

## Type of Change

- [x] Bug fix (non-breaking change fixes issue)
- [ ] New feature (non-breaking change adds functionality)
- [ ] Breaking change (fix or feature would cause existing functionality
change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Add Codex registry metadata generation for the ChatGPT/OAuth
`/v1/models` response.
- Preserve dynamic upstream registry entries instead of reducing them to
slug-only IDs.
- Add fallback metadata for known Codex models if upstream registry data
is unavailable.
- Fill required/default Codex fields when absent, including:
  - `display_name`
  - `default_reasoning_level`
  - `supported_reasoning_levels`
  - `context_window`
  - tool/runtime capability flags
- Keep the existing OpenAI-compatible `data[]` response shape.
- Add tests that assert both OpenAI-compatible `data[]` and
Codex-compatible `models[]` shapes.

Changed files:

- `headroom/providers/proxy_routes.py`
- `tests/test_provider_proxy_routes.py`
- `tests/test_proxy_codex_route_aliases.py`

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
ruff check headroom/providers/proxy_routes.py tests/test_provider_proxy_routes.py tests/test_proxy_codex_route_aliases.py
# pass

pytest tests/test_provider_proxy_routes.py::test_v1_models_fetches_codex_registry_under_chatgpt_auth
# pass

pytest tests/test_proxy_codex_route_aliases.py
# pass

pytest tests/test_provider_proxy_routes.py
# pass
```

## Real Behavior Proof

- Environment: isolated local Headroom proxy using the patched source.
- Exact command / steps:
  - call `/v1/models`
  - run one small Codex `/v1/responses` request through the proxy
  - compare Headroom `/stats`
  - check logs for Codex model metadata decode or missing-field warnings
- Observed result:
  - `/v1/models` succeeded
  - `/v1/responses` succeeded
  - `requests.failed` stayed flat
  - provider stats and proxy compression accounting increased
  - no Codex model metadata decode or missing-field warnings observed
- Not tested:
  - full repository `mypy headroom` pass was not run for this submission

## Review Readiness

- [x] I have performed a self-review before requesting human review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows project's style guidelines
- [x] I performed self-review my code
- [ ] I commented my code, particularly in hard-to-understand areas
- [ ] I made corresponding changes documentation
- [x] My changes generate no new warnings
- [x] I added tests prove fix is effective or feature works
- [x] New and existing unit tests pass locally my changes
- [ ] I updated CHANGELOG.md if applicable

## Screenshots (if applicable)

N/A.

## Additional Notes

Checklist items left unchecked are intentionally not applicable or not
run for this focused compatibility PR:

- no new comments were needed in the implementation
- no documentation or changelog update is included for this
compatibility fix to an existing route
- full-suite `mypy headroom` was not run in the submission pass

Co-authored-by: felixboenkost-droid <258905464+felixboenkost-droid@users.noreply.github.com>
2026-06-16 12:19:22 -07:00
ehendrix23
5f0c5f0fe1 Minimize Codex model metadata changes 2026-06-01 17:27:31 -06:00
ehendrix23
4be6717864 Remove Codex model registry cache 2026-06-01 17:20:12 -06:00
ehendrix23
e0b863cb6d Add codex model discovery and logging fixes 2026-06-01 14:10:15 -06:00
chopratejas
3ec549288a fix(proxy): thread tags into 13 outcome sites + synth /v1/models + free-fn _extract_tags
Three fixes bundled; all in admin / cache-hit paths where tests didn't
catch the regression.

## (A) 13 RequestOutcome sites missing tags=

An AST audit found that 13 of 21 ``RequestOutcome(...)`` construction
sites across the four handler files emitted outcomes without threading
``tags=``. Affected paths:

* ``handle_anthropic_messages`` — the ``from_response_cache=True``
  early-return outcome (Claude Code cache-hit turns dashboard-blind)
* ``handle_openai_chat`` — same cache-hit early-return (Codex +
  Cursor + Continue cache-hit turns dashboard-blind)
* ``handle_openai_responses_ws`` — the per-turn outcome inside the
  Codex WS session. The stale comment that said "ws_session_tags is
  not yet bound" was wrong — ``ws_tags`` was already extracted at
  handler entry
* ``handle_anthropic_batch_create / batch_passthrough / batch_results``
* ``handle_passthrough`` (OpenAI Models / Files / List-Batches)
* ``handle_google_batch_create / batch_passthrough / batch_results``
* ``_google_batch_passthrough`` (internal helper)
* ``handle_batch_create`` (OpenAI batch entry)
* ``handle_gemini_count_tokens`` (also fixed in #479; identical)

Pattern of the fix is uniform: pull tags from headers and thread
them into the ``RequestOutcome`` construction.

New contract test ``test_handler_outcome_tag_invariant.py`` walks each
handler file's AST and asserts every ``RequestOutcome`` site inside any
``handle_*`` or ``*_passthrough`` method passes both ``tags=`` and
``client=``. Future handlers get a clear test failure with file +
line + method name if they regress.

## (B) Issue #478 — /v1/models 403 under Codex ChatGPT auth

Codex Desktop with ChatGPT-subscription OAuth polls ``/v1/models`` to
populate its model picker. Forwarding to ``chatgpt.com/backend-api/
models`` returned 403 to OAuth tokens. Fix: synthesize an OpenAI-
compatible payload locally from a known-supported model set
(``gpt-5.5`` through ``gpt-5``). All other ChatGPT-auth paths still
forward as before — only model-metadata gets the local response.

## (C) Move _extract_tags to free function (mixin-isolation test compat)

Handlers called ``self._extract_tags(headers)``. That worked in
production where ``HeadroomProxy`` composes every mixin and defines
the method, but broke tests that instantiate a single mixin via
``object.__new__(OpenAIHandlerMixin)``. The free-function form
removes that coupling — handlers import ``extract_tags`` from
``headroom.proxy.helpers`` and call directly. ``HeadroomProxy.
_extract_tags`` is kept as a thin wrapper for any external caller
still using the method form. 17 call sites migrated.

## Zero behavior change for existing users

Claude Code, Codex, Cursor, Continue, Aider, Gemini-routed harnesses
all hit handlers that already extracted tags. Their wire bytes to
upstream LLMs are byte-identical. Only the dashboard view gains tags
on previously-blind paths.

Closes #478.
2026-05-15 19:15:48 -07:00
Tejas Chopra
62a1f23b88 fix: log Codex ws cancellations safely 2026-05-10 09:06:07 -07:00
JerrettDavis
c788b7d6c9 style: align proxy tests with current ruff formatter
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 19:19:43 -05:00
Kayzo
9f0a813004 fix: address PR review feedback 2026-04-17 23:09:12 +00:00
Kayzo
4bdc1482af feat: add Pi/Codex and Cloud Code Assist compatibility routes
- Adds /v1/codex/responses aliases for OpenAI Codex clients configured with /v1 base URLs
- Uses JWT-derived account routing for /v1/responses/* subpaths (compact, cancel, etc)
- Adds /v1internal:streamGenerateContent aliases for Cloud Code Assist / Antigravity
- Preserves upstream HTTP error status/body in StreamingResponse (fixes empty SSE drops)
2026-04-17 17:45:08 +00:00
JerrettDavis
eb302fa35e test(proxy): cover codex responses subpath aliases
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-08 21:22:05 -05:00
JerrettDavis
2ceceb4024 fix(proxy): alias nested codex responses routes
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-08 21:17:24 -05:00
JerrettDavis
8134939efb fix(openclaw): preserve upstream response paths 2026-04-08 21:15:52 -05:00