Commit graph

25 commits

Author SHA1 Message Date
JD Davis
1aa701adaa
fix(vscode): persist compatible Claude modes and route Copilot CAPI (#2986)
## Description

Fixes #2492, #2028, and #2827.

Claude daemon workers consume project settings rather than reliably
inheriting wrapper environment state, while the Claude VS Code webview
cannot render deferred-tool response blocks. Separately, recent Copilot
Chat versions use the whole CAPI override for generation; the legacy
proxy override alone only sends model discovery through Headroom.

This PR carries both integrations through to the actual consumers
instead of only changing their launch-time surface configuration.

## Type of Change

- [x] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
- [ ] Build / CI

## Changes Made

- Persist the resolved Claude ENABLE_TOOL_SEARCH value into project
settings for daemon workers and restore it transactionally after wrap
exits.
- Use compatibility-safe Foundry and Claude VS Code defaults while
preserving explicit user choices.
- Configure both Copilot overrideProxyUrl and overrideCapiUrl in the
reversible managed VS Code settings block.
- Route Copilot unprefixed POST /chat/completions and HTTP /responses
requests through the real compression handlers.
- Keep /responses out of the Codex WebSocket aliases because Copilot and
Codex use different WebSocket wire protocols.
- Extend wrap E2E assertions for both the Claude webview mode and
Copilot CAPI routing.

## Testing

- [x] 127 combined Claude, Copilot, route-integration, and MCP
dependency-contract tests pass.
- [x] Ruff check passes on all changed Python files.
- [x] Ruff format check passes.
- [x] Python compilation and git diff --check pass.

## Runtime Safety

Standalone Claude CLI defaults remain unchanged. Explicit Claude
tool-search values retain precedence, and project settings are restored
through the existing cleanup path. Copilot model/session helper
endpoints continue through generic passthrough, while only validated
HTTP generation paths receive explicit compression routes. Existing
Codex WebSocket behavior is unchanged.

## Review Readiness

- [x] Current main and MCP v1 compatibility retained
- [x] Worker-facing Claude persistence covered
- [x] Reversible Copilot and Claude settings behavior covered
- [x] Copilot generation routes covered at registration and proxy
integration layers
- [x] Ready for review
2026-08-13 15:06:41 -05:00
Rod Boev
a540eb2c61
fix(codex): route alpha search through the Codex backend (#2538)
## Description

Codex GPT-5.6 standalone web search currently falls through Headroom's
generic passthrough path. Under ChatGPT OAuth that sends `POST
/v1/alpha/search` to `https://chatgpt.com/v1/alpha/search`, which
redirects to HTML and makes Codex fail to decode the response.

This change adds an explicit standalone Codex search alias so
ChatGPT-authenticated `/v1/alpha/search` requests route through
`https://chatgpt.com/backend-api/codex/alpha/search`, while non-ChatGPT
traffic keeps the existing passthrough behavior. Closes #2525.

## Type of Change

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

## Changes Made

- add a dedicated `POST /v1/alpha/search` Codex route for
ChatGPT-authenticated traffic
- route that alias through the existing `codex_backend_url()` helper so
the upstream path becomes `/backend-api/codex/alpha/search`
- add focused regression coverage for ChatGPT-auth routing and
non-ChatGPT passthrough preservation

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_provider_proxy_routes.py -q`)
- [x] Linting passes (`uv run ruff check
headroom/providers/proxy_routes.py tests/test_provider_proxy_routes.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
uv run pytest tests/test_provider_proxy_routes.py -q
23 passed, 1 warning in 15.77s

uv run ruff check headroom/providers/proxy_routes.py tests/test_provider_proxy_routes.py
All checks passed!

uv run ruff format headroom/providers/proxy_routes.py tests/test_provider_proxy_routes.py --check
2 files already formatted

git diff --check
(no output)
```

## Real Behavior Proof

- Environment: focused Headroom worktree with proxy route regression
tests
- Exact command / steps: run the issue-shaped inline Python reproduction
from `bodies/headroom-issue-2525.json`, then run the focused
preservation and matrix pytest rows for ChatGPT-auth and non-ChatGPT
auth
- Observed result: the base repro printed `FAIL issue2525 codex alpha
search -> observed_url=None fallback=[('/v1/alpha/search',
'https://chatgpt.com')]
body={"base_url":"https://chatgpt.com","provider":""}`, while the head
repro printed `PASS issue2525 codex alpha search ->
https://chatgpt.com/backend-api/codex/alpha/search?query=weather`; the
non-ChatGPT preservation row passed `1 passed, 22 deselected`, and the
auth matrix row passed `1 passed, 22 deselected`
- Not tested: live ChatGPT OAuth account on this host

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A - proxy routing change only.

## Additional Notes

- `CHANGELOG.md` stays untouched because Headroom's release automation
generates it from conventional commits.
- The fix is scoped to standalone Codex search. It does not change
`/v1/responses`, image routes, or generic OpenAI passthrough semantics.
- Proof artifact:
`D:\Repos\.claude\pr-sweep\headroom-PR-TARGET-2525-PROOF.md`

Co-authored-by: JD Davis <jd@jds-macbook-air.tail2a279.ts.net>
2026-08-12 00:22:20 -05:00
Matt Van Horn
789a4f3060
fix: normalize /p/<project> prefix on WebSocket upgrades so the Responses WS route is not rejected with 403 (#2379)
## Description

A Responses WebSocket upgrade to a project-prefixed URL
(`ws://127.0.0.1:8787/p/<project>/v1/responses`) was rejected with `403
Forbidden`, so the client fell back to HTTP transport. The `/p/<name>`
base-URL prefix is stripped by
`strip_project_path_prefix(request.scope)` inside
`@app.middleware("http")`, but Starlette runs `@app.middleware("http")`
for `http` scopes only, never `websocket` scopes. So an HTTP `POST
/p/<project>/v1/responses` has its prefix stripped and matches
`/v1/responses`, while the WS upgrade keeps the prefix, matches no
registered WebSocket route (`OPENAI_RESPONSES_WEBSOCKET_PATHS` are all
unprefixed), and Starlette rejects the unmatched WebSocket with `403`.
This normalizes the prefix for WebSocket scopes before routing so the
upgrade reaches the existing Responses WS handler and stays attributed
to the project.

Closes #2355

## Type of Change

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

## Changes Made

- `headroom/proxy/server.py` — added a small pure-ASGI
`WebSocketProjectPrefixMiddleware` (registered in `create_app`) that,
for `websocket` scopes only, strips the `/p/<name>` prefix via the
existing `strip_project_path_prefix` and binds the project context,
mirroring the HTTP middleware. HTTP and lifespan scopes pass through
untouched (no double-strip).
- `headroom/proxy/handlers/openai.py` — `handle_openai_responses_ws`
previously called `set_current_project(classify_project(ws_headers))`
unconditionally, clearing the middleware-bound project for prefix-only
clients (no `X-Headroom-Project` header). It now falls back to the
already-bound path-prefix project (`classify_project(ws_headers) or
get_current_project()`), so prefix-only WebSocket clients (aider,
Copilot BYOK, Cursor and other `/p/<name>` base-URL wraps) stay
attributed, exactly as on the HTTP path.
- `tests/test_provider_proxy_routes.py` — added a regression test.

## Testing

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

### Test Output

```text
$ pytest tests/test_provider_proxy_routes.py -q
21 passed, 1 warning in 23.95s

$ ruff check headroom/proxy/server.py headroom/proxy/handlers/openai.py
All checks passed!

$ mypy --python-version 3.13 headroom/proxy/server.py headroom/proxy/handlers/openai.py
Success: no issues found in 2 source files
```

## Real Behavior Proof

- Environment: local, `uv` venv, Python 3.14, `uv run pytest`.
- Exact command / steps: added
`test_project_prefixed_openai_response_websocket_delegates_to_openai_ws_handler`,
which connects a WebSocket to `/p/test-project/v1/responses`.
- Observed result: the connection is accepted (no 403), the handler is
reached with the canonical `/v1/responses` path, and the request is
attributed to project `test-project`.
- Not tested: live end-to-end against a real upstream Responses
WebSocket server (validated via the routing/attribution regression test
only).

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

N/A — backend routing change with no user-facing UI.

## Additional Notes

Documentation checklist item is N/A: this is an internal routing fix
with no configuration or public-API surface change. The fix mirrors the
existing HTTP prefix-strip behavior so project-prefixed WebSocket
clients behave identically to their HTTP counterparts.

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-08-03 11:15:40 -07:00
JD Davis
e6243f65c9
refactor(providers): split proxy route adapters (#1934)
## Description
Refactors provider-specific proxy routing into provider-owned helper
modules so `headroom/providers/proxy_routes.py` primarily registers
routes and delegates behavior. This keeps Codex, OpenAI
Responses/images, model metadata, Vertex, Cloud Code, passthrough target
selection, and request path normalization logic testable outside the
route table.

Closes #

## Type of Change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Documentation update
- [ ] Performance improvement
- [x] Code refactoring (no functional changes)

## Changes Made
- Extracted Codex routing helpers for headers, endpoint URLs, image
forwarding, response subpaths, and model metadata.
- Moved provider target selection, route specs, OpenAI Responses/images
helpers, Vertex runtime helpers, Cloud Code path normalization,
passthrough telemetry, and request scope normalization into focused
modules.
- Kept `proxy_routes.py` as route registration/delegation and preserved
current-main `/v1/messages` custom-base behavior.
- Added focused provider/proxy tests for the extracted modules and route
delegation behavior.

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

### Test Output
```text
python -m pytest tests/test_package_init_lazy.py::test_codex_package_import_stays_runtime_only tests/test_provider_cloudcode_runtime.py tests/test_provider_codex_endpoints.py tests/test_provider_codex_headers.py tests/test_provider_codex_images.py tests/test_provider_codex_model_metadata.py tests/test_provider_codex_responses.py tests/test_provider_model_metadata.py tests/test_provider_openai_images.py tests/test_provider_openai_responses.py tests/test_provider_proxy_targets.py tests/test_provider_route_specs.py tests/test_provider_vertex_runtime.py tests/test_proxy_request_scope.py tests/test_provider_proxy_routes.py::test_provider_passthrough_routes_forward_expected_targets tests/test_provider_proxy_routes.py::test_proxy_route_helpers_prefer_legacy_targets_and_gemini_passthrough tests/test_provider_proxy_routes.py::test_provider_specific_routes_delegate_to_expected_proxy_handlers tests/test_provider_proxy_routes.py::test_openai_response_websocket_aliases_delegate_to_openai_ws_handler tests/test_provider_proxy_routes.py::test_openai_response_subpath_passthrough_returns_502_on_http_failure tests/test_provider_proxy_routes.py::test_openai_response_subpath_passthrough_uses_openai_target tests/test_provider_proxy_routes.py::test_openai_response_subpath_aliases_and_chatgpt_auth_use_expected_targets tests/test_provider_proxy_routes.py::test_openai_image_routes_use_codex_backend_under_chatgpt_auth tests/test_provider_proxy_routes.py::test_openai_image_codex_response_strips_stale_compression_headers tests/test_provider_proxy_routes.py::test_openai_image_edits_api_key_auth_falls_through_to_openai_passthrough tests/test_provider_proxy_routes.py::test_openai_image_edits_preserves_multipart_body_under_chatgpt_auth tests/test_provider_proxy_routes.py::test_gemini_batch_embed_contents_passthrough_uses_gemini_target tests/test_provider_proxy_routes.py::test_v1_models_fetches_codex_registry_under_chatgpt_auth tests/test_provider_proxy_routes.py::test_v1_models_falls_back_to_synthetic_list_under_chatgpt_auth tests/test_provider_proxy_routes.py::test_v1_models_get_single_dynamic_under_chatgpt_auth tests/test_provider_proxy_routes.py::test_v1_models_still_forwards_under_non_chatgpt_auth tests/test_provider_proxy_routes.py::test_v1_models_routes_claude_code_gateway_discovery_to_anthropic tests/test_provider_proxy_routes.py::test_anthropic_model_metadata_strips_ansi_model_ids tests/test_custom_base_passthrough_telemetry.py tests/test_proxy_passthrough.py tests/test_proxy_google_cloudcode_route_aliases.py tests/test_proxy_project_savings.py::test_with_project_prefix_round_trips_through_split tests/test_vertex_claude_compression.py
============================ 102 passed in 34.83s =============================

python -m ruff check headroom/providers/cloudcode headroom/providers/codex headroom/providers/vertex headroom/providers/model_metadata.py headroom/providers/openai_images.py headroom/providers/openai_responses.py headroom/providers/proxy_targets.py headroom/providers/route_specs.py headroom/providers/proxy_routes.py headroom/proxy/handlers/openai.py headroom/proxy/passthrough.py headroom/proxy/request_scope.py headroom/proxy/project_context.py tests/test_package_init_lazy.py tests/test_provider_cloudcode_runtime.py tests/test_provider_codex_endpoints.py tests/test_provider_codex_headers.py tests/test_provider_codex_images.py tests/test_provider_codex_model_metadata.py tests/test_provider_codex_responses.py tests/test_provider_model_metadata.py tests/test_provider_openai_images.py tests/test_provider_openai_responses.py tests/test_provider_proxy_targets.py tests/test_provider_route_specs.py tests/test_provider_vertex_runtime.py tests/test_proxy_request_scope.py tests/test_provider_proxy_routes.py tests/test_custom_base_passthrough_telemetry.py tests/test_proxy_passthrough.py tests/test_proxy_google_cloudcode_route_aliases.py tests/test_proxy_project_savings.py tests/test_vertex_claude_compression.py
All checks passed!

python -m compileall -q headroom\providers\cloudcode headroom\providers\codex headroom\providers\vertex headroom\providers\model_metadata.py headroom\providers\openai_images.py headroom\providers\openai_responses.py headroom\providers\proxy_targets.py headroom\providers\route_specs.py headroom\providers\proxy_routes.py headroom\proxy\handlers\openai.py headroom\proxy\passthrough.py headroom\proxy\request_scope.py headroom\proxy\project_context.py
# no output; exited 0

git commit -m "refactor(providers): split proxy route adapters"
Sync plugin versions.....................................................Passed
check for merge conflicts................................................Passed
ruff.....................................................................Passed
ruff-format..............................................................Passed
mypy.....................................................................Passed
```

## Real Behavior Proof
- Environment: Windows PowerShell, Python 3.13.13, branch
`jd/provider-route-slices` based on `headroomlabs/main`.
- Exact command / steps: Ran the focused provider/proxy pytest suite,
focused ruff command, compileall over changed Python modules, and commit
hooks.
- Observed result: Provider/proxy route behavior tests passed; lint,
formatting, and mypy passed.
- Not tested: Full pytest suite, live upstream provider calls, and
manual end-to-end proxy traffic.

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

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

## Screenshots (if applicable)
N/A

## Additional Notes
Documentation and CHANGELOG updates are N/A for this internal refactor.
The full pytest suite was not run; coverage here is focused on
provider/proxy routing behavior touched by this slice.

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-07-12 16:27:45 -05:00
Rod Boev
d2a86b5909
fix(proxy): strip duplicated upstream server headers (#1828)
## Description

Fixes duplicated upstream server headers emitted by the proxy when
forwarding responses.

## Type of Change

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

## Changes Made

- Adjust proxy response forwarding so upstream server headers are not
duplicated.
- Preserve the intended response-header behavior while avoiding repeated
header values.
- Keep the change scoped to proxy/header handling.

## Testing

- [x] Unit tests pass
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
Focused tests/review were completed before this governance body cleanup. The current branch is conflicted and still needs merge resolution before final merge readiness.
```

## Real Behavior Proof

- Environment: Headroom development/review context.
- Exact command / steps: Reviewed the proxy response-header behavior and
existing focused coverage for duplicate upstream server headers.
- Observed result: The PR implementation prevents duplicated upstream
server headers while preserving proxy forwarding behavior.
- Not tested: Current conflicted branch after merge resolution;
conflicts still need to be resolved before merge.

## Review Readiness

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

## Additional Notes

This body was normalized by a maintainer after approval so the
governance parser reflects the already-reviewed PR state. The PR remains
blocked by merge conflicts.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-09 21:43:17 -05:00
Vinay Gupta
739f654bbd
fix(proxy): route Foundry Anthropic messages (#1878)
## Description

Closes #1874

`headroom wrap claude` in Azure AI Foundry mode gives Claude Code a
local `ANTHROPIC_FOUNDRY_BASE_URL` ending in `/anthropic`. Claude Code
appends `/v1/messages`, so Headroom receives `POST
/anthropic/v1/messages`. That path was not registered as an Anthropic
Messages route, so it fell through to generic passthrough and never
reached compression or Foundry forwarding.

This PR registers the Foundry-shaped Anthropic Messages route,
normalizes the inbound request path back to `/v1/messages`, and
dispatches it through `handle_anthropic_messages` with the configured
Anthropic upstream base. That keeps the actual upstream URL shape as
`<foundry>/anthropic/v1/messages` while avoiding the catch-all
OpenAI-compatible passthrough.

## Type of Change

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

## Changes Made

- Added a `POST /anthropic/v1/messages` route alias for Foundry-mode
Claude Code traffic.
- Normalized the request path to `/v1/messages` before invoking the
Anthropic handler.
- Added route-level regression coverage proving the Foundry-shaped path
reaches `handle_anthropic_messages` instead of passthrough.

## Testing

- [x] Unit tests pass (`tests/test_provider_proxy_routes.py` with a
local `headroom._core` import stub)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
# Pre-fix proof with the new regression present:
tests/test_provider_proxy_routes.py F.F.................
FAILED tests/test_provider_proxy_routes.py::test_provider_passthrough_routes_forward_expected_targets
FAILED tests/test_provider_proxy_routes.py::test_provider_specific_routes_delegate_to_expected_proxy_handlers
Observed: /anthropic/v1/messages fell through to handle_passthrough with https://api.openai.test.

# After patch:
HEADROOM_REQUIRE_RUST_CORE=false PYTHONPATH=/Users/vinaygupta/Desktop/git/headroom-fix-1874-foundry-anthropic-route \
  /tmp/headroom-route-test-1874/bin/python -m pytest tests/test_provider_proxy_routes.py -q
20 passed, 2 warnings in 2.19s

rtk proxy uvx ruff==0.15.17 check .
All checks passed!

rtk proxy uvx ruff==0.15.17 format --check .
1068 files already formatted

rtk proxy uvx --from mypy==1.20.2 mypy headroom/providers/proxy_routes.py --ignore-missing-imports
Success: no issues found in 1 source file

GitHub PR checks after opening readiness review:
28 passed, 0 failed
```

## Real Behavior Proof

- Environment: macOS local checkout, throwaway Python env at
`/tmp/headroom-route-test-1874`, `HEADROOM_REQUIRE_RUST_CORE=false`, and
an in-memory `headroom._core` stub for route-level testing because the
native extension is not built locally.
- Exact command / steps: added the regression first, ran the focused
route test, observed `/anthropic/v1/messages` fall through to
`handle_passthrough`; then added the route alias and reran the same
test.
- Observed result: `/anthropic/v1/messages?beta=true` now reaches
`handle_anthropic_messages` with normalized path `/v1/messages` and
upstream base `https://api.anthropic.test`.
- Not tested: live Claude Code against a real Azure AI Foundry
deployment.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

Full local `uv run pytest` is blocked by the known native build issue in
`esaxx-rs` (`fatal error: 'cstdint' file not found`). Full touched-file
mypy also reports existing `no-untyped-def` errors in
`tests/test_provider_proxy_routes.py`; the production route file passes
mypy on its own.

The unchecked documentation/comment/CHANGELOG boxes are N/A for this
route-only fix.
2026-07-08 14:22:20 -07:00
Vinay Gupta
f18c6bd896
fix(codex): OpenCode Zen telemetry attribution (#1648)
## Description

Fixes #1602.

OpenCode Zen custom-base requests can reach Headroom through the generic
passthrough path, but that route was not supplying endpoint/provider
metadata for Zen chat completions. This made forwarded Zen traffic
invisible in dashboard provider, usage, and token telemetry.

Closes #1602

## Type of Change

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

## Changes Made

- Added a narrow OpenCode Zen custom-base classifier for `POST
/zen/v1/chat/completions` on `opencode.ai` and `www.opencode.ai`.
- Passed `endpoint_name="chat/completions"` and `provider="zen"` into
catch-all passthrough telemetry for matching Zen traffic.
- Attributed normalized OpenCode transport traffic
(`/v1/chat/completions` with `x-headroom-original-path:
/zen/v1/chat/completions`) to `zen` for request outcomes while keeping
the OpenAI parser path unchanged.
- Added coverage for direct catch-all routing, normalized original-path
routing, token usage outcome recording, and false-positive paths like
`/mcp/v1/chat/completions`, `/npm/v1/chat/completions`, and
`/context7/v1/chat/completions`.

## Testing

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

### Test Output

```text
$ rtk pytest tests/test_custom_base_passthrough_telemetry.py -q
Pytest: 4 passed

$ rtk uvx --from ruff==0.15.17 ruff check headroom/proxy/handlers/openai.py headroom/providers/proxy_routes.py tests/test_custom_base_passthrough_telemetry.py tests/test_provider_proxy_routes.py tests/test_proxy/test_openai_transport_path_prefix.py
All checks passed!

$ rtk uvx --from ruff==0.15.17 ruff format --check headroom/proxy/handlers/openai.py headroom/providers/proxy_routes.py tests/test_custom_base_passthrough_telemetry.py tests/test_provider_proxy_routes.py tests/test_proxy/test_openai_transport_path_prefix.py
5 files already formatted

$ rtk /Library/Frameworks/Python.framework/Versions/3.13/bin/python3 -m py_compile headroom/proxy/handlers/openai.py headroom/providers/proxy_routes.py tests/test_custom_base_passthrough_telemetry.py tests/test_provider_proxy_routes.py tests/test_proxy/test_openai_transport_path_prefix.py
# passed

$ rtk git diff --check
# passed
```

GitHub Actions also passed after the final push, including CI, Docker
native/wrap/init E2E, security, lint, and PR governance.

## Real Behavior Proof

- Environment: local worktree on macOS plus GitHub Actions for PR #1648.
- Exact command / steps: ran focused pytest coverage for Zen passthrough
telemetry, Ruff check/format validation on touched files, Python compile
validation, `git diff --check`, and waited for the full GitHub Actions
rollup.
- Observed result: Zen custom-base chat completions now record request
outcomes as provider `zen` with endpoint `chat/completions`;
false-positive OpenCode paths remain unattributed to Zen; GitHub checks
are green.
- Not tested: full local test suite did not collect in this worktree
because the native `headroom._core` extension is not installed. `rtk npm
--prefix plugins/opencode test` is also blocked locally because `vitest`
is not installed in `plugins/opencode/node_modules`.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

The documentation and CHANGELOG checklist items are not applicable for
this narrow telemetry bug fix. No new comments were added because the
code path is covered by narrowly named helper/test cases.
2026-07-07 11:35:21 -05:00
Aykut Bulgu
bb3e040a46
fix(proxy): add versionless Vertex AI routes for Claude Code compatibility (#1321)
## Description

When Claude Code is configured for Vertex AI
(`CLAUDE_CODE_USE_VERTEX=1`) and routes through the Headroom proxy
(`ANTHROPIC_BASE_URL=http://127.0.0.1:8787`), all requests fail with
404. Claude Code constructs Vertex paths without the `/{api_version}/`
prefix (e.g. `/projects/.../models/...:rawPredict`), but the proxy's
existing route patterns require it (e.g. `/{api_version}/projects/...`).
The request falls through unmatched and the upstream returns 404.

## Type of Change

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

## Changes Made

- Add versionless route handlers for `rawPredict` and `streamRawPredict`
in `headroom/providers/proxy_routes.py`
- Routes are scoped to
`/projects/{project}/locations/{location}/publishers/anthropic/models/{model}:(stream)rawPredict`
-- only Anthropic publisher, no generic `{publisher}` parameter.
Non-Anthropic versionless requests fall through to the catch-all
passthrough, avoiding a half-fixed path that would omit the `/v1`
prefix.
- The handlers append `/v1` to the resolved Vertex target URL so
`build_copilot_upstream_url()` constructs the correct upstream path:
`https://aiplatform.googleapis.com/v1/projects/...`
- Add test assertions in `tests/test_provider_proxy_routes.py` covering
both new route variants and verifying non-Anthropic versionless requests
do not enter the Anthropic handler

## 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
20 passed, 1 warning in 3.56s
```

## Real Behavior Proof

- Environment: macOS (Darwin 25.5.0, arm64), Claude Code with Vertex AI
via `headroom wrap claude`, Headroom v0.27.0. Also verified on Fedora
(OpenClaw agents using `@anthropic-ai/vertex-sdk` v0.90.0).
- Exact command / steps: `claude headroom on` then `claude` launches
Claude Code through headroom proxy on port 8787. Claude Code sends
requests to
`http://127.0.0.1:8787/projects/{project}/locations/global/publishers/anthropic/models/claude-opus-4-6:streamRawPredict`.
Proxy forwards to `https://aiplatform.googleapis.com/v1/projects/...`
and returns 200.
- Observed result: Before fix, proxy forwarded to
`https://aiplatform.googleapis.com/projects/...` (missing `/v1/`),
Vertex returned 404. After fix, requests succeed with status 200.
- Not tested: Non-Anthropic publishers on versionless routes (no known
client sends these). These requests fall through to the catch-all
passthrough by design.

## Review Readiness

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

## Checklist

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

## Additional Notes

The root cause: `handle_anthropic_messages()` constructs the upstream
URL via `build_copilot_upstream_url(upstream_base_url,
request.url.path)` which concatenates `base_url + path`. The versioned
routes work because `request.url.path` already contains `/v1/` (e.g.
`/v1/projects/...`). But Claude Code with `CLAUDE_CODE_USE_VERTEX=1`
sends paths without the version prefix, so the upstream URL was missing
`/v1/` entirely.

Per review feedback, versionless routes are now scoped exclusively to
`publishers/anthropic` rather than accepting a generic `{publisher}`
parameter, preventing non-Anthropic publishers from hitting a
passthrough path that would also lack the `/v1` prefix.
2026-06-26 12:16:39 -05:00
Johnson Deng
381d771e46
fix(proxy): route Codex OAuth image requests (#1215)
## Description

Closes #1189.

After a recent Codex Desktop update, its built-in image generation
started going
through Codex's image client, which POSTs to `images/generations` and
`images/edits` relative to the configured provider base URL. In Headroom
Proxy
mode Codex is pointed at Headroom's `/v1` surface, so those land as
`/v1/images/generations` and `/v1/images/edits`.

Headroom already had `/v1/images/generations`, but it only ever hit the
OpenAI
API-key passthrough, and there was no `/v1/images/edits` route at all.
So under
ChatGPT/Codex OAuth the image calls had nowhere correct to go. This
change routes
OAuth image requests to
`https://chatgpt.com/backend-api/codex/images/{generations,edits}`
and leaves the API-key passthrough untouched.

Latest upstream re-check: current `openai/codex` main is now `aaf737f`,
and the
relevant `ImagesClient`/provider-base source still resolves image
generation and
edit requests to
`https://chatgpt.com/backend-api/codex/images/{generations,edits}`
under ChatGPT-family auth. One issue-thread datapoint reports Codex
Desktop
`0.142.0-alpha.6` on macOS generating images successfully via the
`/v1/responses`
WebSocket path. The requester has now checked this against the latest
timestamped
Codex update, so this is ready for maintainer review with the remaining
full-suite caveat documented below.

**Reproduction / test contract**

- Reporter's setup: Codex Desktop 0.142.0-alpha.1 on Windows 10,
Headroom v0.26.0
Proxy mode, OAuth auth. `/v1/models` and `/v1/responses` work; built-in
image
  generation fails.
- Why the route was confirmed from source: the reporter's sanitized logs
only
show `/v1/models` and `/v1/responses`, so I traced the rest in current
Codex
source — image generation/edit go through `ImagesClient` as
`images/generations`
  and `images/edits` against the provider base URL.
- Regression test:
`test_openai_image_routes_use_codex_backend_under_chatgpt_auth`
asserts both OAuth image routes now resolve to the ChatGPT Codex image
backend.
Before this patch, `/v1/images/generations` used the OpenAI API-key
target under
  OAuth and `/v1/images/edits` didn't exist.
- Hardening tests: additional regressions cover stale upstream
compression
headers, OpenAI API-key fall-through for edits, and multipart edit body
  byte-preservation.

## Type of Change

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

## Changes Made

- Route ChatGPT/Codex OAuth `/v1/images/generations` and
`/v1/images/edits` to
  the ChatGPT Codex image backend.
- Strip internal `x-headroom-*`, `Host`, and `Accept-Encoding` headers
before
  forwarding Codex OAuth image requests upstream.
- Strip stale `Content-Encoding` and `Content-Length` headers from image
  responses because httpx has already decoded the body.
- Keep API-key image requests on the existing OpenAI passthrough.
- Add regression coverage for both OAuth image routes, OpenAI image-edit
passthrough, compressed-response header handling, and multipart edit
bodies.
- Add a `CHANGELOG.md` entry.

## Testing

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

### Test Output

```text
$ uv run pytest tests/test_provider_proxy_routes.py tests/test_proxy_codex_route_aliases.py tests/test_openai_codex_routing.py -q
42 passed, 1 warning in 7.63s

$ UV_PROJECT_ENVIRONMENT=.venv-py312 uv run --python 3.12 --extra dev --extra proxy pytest tests/test_provider_proxy_routes.py tests/test_proxy_codex_route_aliases.py tests/test_openai_codex_routing.py -q
42 passed, 1 warning in 8.54s

$ uv run ruff check .
All checks passed!

$ uv run ruff format --check .
895 files already formatted

$ uv run mypy headroom
headroom/proxy/server.py:1152: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
headroom/proxy/server.py:1222: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
headroom/proxy/server.py:1226: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs  [annotation-unchecked]
Success: no issues found in 380 source files
```

Earlier full-suite attempt in this branch/environment, before the F1-F8
hardening pass (not rerun after hardening because the failures were
unrelated
to this route and expensive):

```text
$ UV_PROJECT_ENVIRONMENT=.venv-py312 uv run --python 3.12 --extra dev --extra proxy pytest
6 failed, 6499 passed, 486 skipped, 5807 warnings in 219.53s
```

All 6 failures are outside the touched routes and unrelated to this
change:

- `tests/test_corrupt_golden_bytes_recovery.py` — 3 log-capture
assertions
-
`tests/test_forwarded_headers.py::test_non_allowlisted_peer_ignores_forwarded_and_logs`
— 1 log-capture assertion
-
`tests/test_image_compression.py::TestOnnxRouter::test_full_classify_with_image`
— `ModuleNotFoundError: No module named 'PIL'` (only `dev,proxy` extras
installed)
-
`tests/test_transforms/test_kompress_compressor.py::TestKompressBackendSelection::test_unrecognized_backend_warns_and_falls_back_to_auto`
— 1 warning-capture assertion

On Python 3.14.4, plain `uv run pytest` can't even collect: the
project's
dependency marker intentionally excludes `litellm` on 3.14, while
`tests/test_memory_eval.py` imports the eval runner at collection time.

## Real Behavior Proof

- **Environment:** macOS (Darwin arm64). Python 3.14.4 via uv for the
default
project env; Python 3.12.13 via `UV_PROJECT_ENVIRONMENT=.venv-py312` for
the
  broader suite. Headroom FastAPI proxy route test harness.
- **Exact command / steps:** read the reporter's sanitized issue logs;
traced
current Codex image-generation source; ran the focused Codex/proxy route
tests
on 3.14 and 3.12; ran lint, format check, and mypy; attempted the full
3.12
  suite (output above).
- **After-fix evidence + observed result:** the regression test captures
the
  OAuth image requests and confirms they forward to
  `https://chatgpt.com/backend-api/codex/images/generations` and
`.../images/edits` — auth and account headers preserved,
internal/host/accept
encoding headers stripped, query string carried through, JSON and
multipart
  request bodies forwarded byte-for-byte, and stale upstream response
  compression headers removed. API-key image generation still uses
`images/generations`, and image edits now have the matching
`images/edits`
  passthrough.
- **Source evidence:** Re-verified against current `openai/codex` HEAD
`aaf737f`. `ImagesClient` still sends relative paths
`images/generations` and
  `images/edits`; `Provider::url_for_path()` appends those to the active
  provider base; ChatGPT-family auth modes default that base to
`CHATGPT_CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex"`.
Therefore
the source-resolved upstream paths are
`/backend-api/codex/images/generations`
  and `/backend-api/codex/images/edits`, not `/backend-api/images/...`.
- **Latest-build caveat:** an issue-thread report says Codex Desktop
`0.142.0-alpha.6` on macOS uses `/v1/responses` WebSocket image
generation and
works through the proxy. That may mean the original Windows
`0.142.0-alpha.1`
regression is fixed client-side in newer desktop builds, even though the
source image endpoint route remains valid and now covered here. The
requester
has checked this against the latest timestamped Codex update before
moving the
  PR out of draft.
- **Not fully tested:** a fully green `uv run pytest` remains
unavailable in
  this local environment for the unrelated failures listed above.

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

N/A

## Additional Notes

No dependency or version changes. The remaining caveat is that the full
local
suite isn't green in this environment for the unrelated failures listed
above.
Happy to follow up with additional runtime logs or to re-run the suite
in a
maintainer's preferred dev container if that's the cleaner path.

---------

Co-authored-by: Johnson <johnsond@brightops.com>
2026-06-21 09:25:49 -07:00
Djabx
e20f16b1a6
fix: route v1internal code assist requests to cloudcode-pa.googleapis… (#821)
## Description

This PR fixes routing of Google Cloud Code Assist authentication,
onboarding, and experiment list endpoints.
Specifically, endpoints under `/v1/v1internal:*` (e.g.
`/v1/v1internal:fetchAvailableModels`) are now correctly routed to the
Cloud Code target (`https://cloudcode-pa.googleapis.com`) and
**normalized** to `/v1internal:*` prior to forwarding. This resolves
404/403 errors on the upstream service which does not accept
`/v1/v1internal:*` request paths.

Closes #821

## Type of Change

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

## Changes Made

- Modified `headroom/providers/proxy_routes.py` to strip the `v1/`
prefix and normalize the path to `/v1internal:*` for Cloud Code routes.
- Modified `tests/test_provider_proxy_routes.py` to add assertions
verifying route and path normalization.

## Testing

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

### Test Output

```text
================================= test session starts =================================
platform linux -- Python 3.14.5, pytest-9.0.3, pluggy-1.6.0 -- /home/alex/projects/github.com/Djabx/headroom/.venv/bin/python3
cachedir: .pytest_cache
rootdir: /home/alex/projects/github.com/Djabx/headroom
configfile: pyproject.toml
plugins: anyio-4.12.1, cov-7.1.0, asyncio-1.4.0, langsmith-0.8.15
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 13 items

tests/test_provider_proxy_routes.py::test_provider_passthrough_routes_forward_expected_targets PASSED [  7%]
tests/test_provider_proxy_routes.py::test_proxy_route_helpers_prefer_legacy_targets_and_gemini_passthrough PASSED [ 15%]
tests/test_provider_proxy_routes.py::test_provider_specific_routes_delegate_to_expected_proxy_handlers PASSED [ 23%]
tests/test_provider_proxy_routes.py::test_openai_response_websocket_aliases_delegate_to_openai_ws_handler PASSED [ 30%]
tests/test_provider_proxy_routes.py::test_openai_response_subpath_passthrough_returns_502_on_http_failure PASSED [ 38%]
tests/test_provider_proxy_routes.py::test_openai_response_subpath_passthrough_uses_openai_target PASSED [ 46%]
tests/test_provider_proxy_routes.py::test_openai_response_subpath_aliases_and_chatgpt_auth_use_expected_targets PASSED [ 53%]
tests/test_provider_proxy_routes.py::test_gemini_batch_embed_contents_passthrough_uses_gemini_target PASSED [ 61%]
tests/test_provider_proxy_routes.py::test_v1_models_fetches_codex_registry_under_chatgpt_auth PASSED [ 69%]
tests/test_provider_proxy_routes.py::test_v1_models_falls_back_to_synthetic_list_under_chatgpt_auth PASSED [ 76%]
tests/test_provider_proxy_routes.py::test_v1_models_get_single_dynamic_under_chatgpt_auth PASSED [ 84%]
tests/test_provider_proxy_routes.py::test_v1_models_still_forwards_under_non_chatgpt_auth PASSED [ 92%]
tests/test_provider_proxy_routes.py::test_v1_models_routes_claude_code_gateway_discovery_to_anthropic PASSED [100%]

================================= 13 passed in 0.63s =================================
```

## Real Behavior Proof

- Environment: Linux, Python 3.14.5
- Exact command / steps: `pytest tests/test_provider_proxy_routes.py`
which utilizes `fastapi.testclient.TestClient` to dispatch requests.
- Observed result: Both `/v1internal` and `/v1/v1internal` endpoints are
correctly routed to the Cloud Code target (`https://cloudcode.test`) and
normalize their paths to `/v1internal`, avoiding 404/403 errors on the
upstream service.
- Not tested: Actual production Cloud Code endpoints (simulated via
TestClient/fakes).

## Review Readiness

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

## Checklist

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

## Screenshots (if applicable)

Add screenshots to help explain your changes.

## Additional Notes

<!-- Mention any N/A checklist items, tradeoffs, follow-ups, or
maintainer context. -->


<!-- headroom-maintainer-template-completion:start -->

## Description

This PR prepares `fix: route v1internal code assist requests to
cloudcode-pa.googleapis…` for review by documenting the intended change,
validation evidence, and remaining merge-readiness context.

Linked issues: None declared.

## Type of Change

- [x] Bug fix
- [ ] New feature
- [ ] Documentation
- [ ] Refactor
- [ ] Tests only

## Changes Made

- Commit: fix: route v1internal code assist requests to
cloudcode-pa.googleapis…
- Touches `headroom/providers/proxy_routes.py`
- Touches `tests/test_provider_proxy_routes.py`

## Testing

- [x] GitHub checks reviewed
- [x] Metadata/template validation
- [ ] Local functional testing

### Test Output

```text
gh pr view 821 --repo chopratejas/headroom --json statusCheckRollup
- PR Governance / template: FAILURE
- PR Governance / template: FAILURE
- PR Governance / template: FAILURE
- PR Governance / template: FAILURE
- PR Governance / label: SUCCESS
- PR Governance / label: SUCCESS
- PR Governance / label: SUCCESS
- PR Governance / label: SUCCESS
- external / GitGuardian Security Checks: SUCCESS
```

## Real Behavior Proof

- Environment: GitHub PR metadata and checks for `chopratejas/headroom`
PR #821.
- Exact command / steps: Reviewed PR title, commits, changed files,
linked issues, labels, and check rollup; appended this maintainer
template completion block without replacing the author's original
description.
- Observed result: PR body now contains all required governance
sections, checked readiness fields, and a non-placeholder validation
evidence block.
- Not tested: This pass updated PR metadata only; code validation
remains represented by the linked GitHub checks and any author-provided
evidence above.

## Review Readiness

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

<!-- headroom-maintainer-template-completion:end -->
2026-06-16 14:58:44 -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
Kumario
0c5c89d05c
fix(anthropic): strip styled Claude model ids (#651)
## Description

Fixes #626 by normalizing Anthropic/Claude model ids that contain ANSI
escape sequences or dangling style suffixes before provider lookups and
upstream forwarding. The branch has been updated onto current `main` and
the proxy handler conflicts have been resolved.

## Type of Change

- [x] Bug fix
- [ ] New feature
- [ ] Documentation
- [ ] Refactor
- [x] Tests only

## Changes Made

- Normalize Anthropic model ids before context/pricing lookup.
- Sanitize Anthropic `/v1/models` metadata and styled `/v1/models/{id}`
passthrough paths.
- Sanitize `/v1/messages` request body model ids before upstream
forwarding.
- Resolved current-main conflicts while preserving newer
`model_override` and streaming passthrough behavior.

## Testing

- [x] Unit tests
- [x] Route/proxy tests
- [x] Lint/static checks
- [ ] Manual testing

### Test Output

```text
UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --with pytest --with fastapi --with httpx --with uvicorn --with h2 python -m pytest tests/test_providers/test_anthropic.py tests/test_provider_proxy_routes.py::test_anthropic_model_metadata_strips_ansi_model_ids tests/test_provider_proxy_routes.py::test_anthropic_model_detail_path_strips_ansi_model_id tests/test_provider_proxy_routes.py::test_anthropic_messages_strips_ansi_model_id_before_upstream -q
17 passed, 2 warnings in 39.91s

UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --with ruff ruff check headroom/providers/anthropic.py headroom/proxy/handlers/openai.py headroom/proxy/handlers/anthropic.py tests/test_providers/test_anthropic.py tests/test_provider_proxy_routes.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.13.3, focused local worktree for PR
#651 after merging current `upstream/main`.
- Exact command / steps: Merged current main, resolved conflicts in
Anthropic/OpenAI proxy handlers, ran the PR's targeted provider/proxy
tests and ruff checks.
- Observed result: Styled Anthropic model metadata, model-detail path,
and messages upstream sanitization tests pass; ruff reports no issues.
- Not tested: Full repository mypy/pre-commit; existing unrelated
Windows `fcntl` typing errors block full hook execution locally.

## Review Readiness

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


<!-- headroom-maintainer-template-completion:start -->

## Description

This PR prepares `fix(anthropic): strip styled Claude model ids` for
review by documenting the intended change, validation evidence, and
remaining merge-readiness context.

Linked issues: #626

## Type of Change

- [x] Bug fix
- [ ] New feature
- [ ] Documentation
- [ ] Refactor
- [ ] Tests only

## Changes Made

- Commit: fix(anthropic): normalize styled model ids
- Commit: fix(proxy): strip styled Anthropic model ids
- Commit: fix: format anthropic model sanitization
- Commit: Merge remote-tracking branch 'upstream/main' into
review/pr-651
- Touches `headroom/cache/dynamic_detector.py`
- Touches `headroom/providers/anthropic.py`
- Touches `headroom/proxy/handlers/anthropic.py`
- Touches `headroom/proxy/handlers/openai.py`
- Touches `tests/test_provider_proxy_routes.py`
- Touches `tests/test_providers/test_anthropic.py`

## Testing

- [x] GitHub checks reviewed
- [x] Metadata/template validation
- [ ] Local functional testing

### Test Output

```text
gh pr view 651 --repo chopratejas/headroom --json statusCheckRollup
- PR Governance / template: FAILURE
- CI / changes: SUCCESS
- Init E2E / docker-init-e2e: SUCCESS
- Wrap E2E / docker-wrap-e2e: SUCCESS
- Wrap Native E2E / wrap-native (ubuntu-latest): SUCCESS
- Wrap Native E2E / wrap-native (macos-latest): SUCCESS
- CI / commitlint: SUCCESS
- PR Governance / label: SUCCESS
- CI / lint: SUCCESS
- CI / build-wheel: SUCCESS
- CI / prefetch-model: SUCCESS
- CI / build: SUCCESS
```

## Real Behavior Proof

- Environment: GitHub PR metadata and checks for `chopratejas/headroom`
PR #651.
- Exact command / steps: Reviewed PR title, commits, changed files,
linked issues, labels, and check rollup; appended this maintainer
template completion block without replacing the author's original
description.
- Observed result: PR body now contains all required governance
sections, checked readiness fields, and a non-placeholder validation
evidence block.
- Not tested: This pass updated PR metadata only; code validation
remains represented by the linked GitHub checks and any author-provided
evidence above.

## Review Readiness

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

<!-- headroom-maintainer-template-completion:end -->

---------

Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-06-13 13:46:21 -05:00
JD Davis
3c77e52ce4
feat: add Vertex AI proxy routing (#793)
## Description

Adds first-class GCP Vertex AI proxy routing for publisher REST
endpoints so Vertex requests are forwarded to a configurable regional
Vertex host instead of falling through to the generic
OpenAI/Anthropic/Gemini passthrough selection.

Fixes #792

## Type of Change

- [x] New feature (non-breaking change that adds functionality)
- [x] Documentation update

## Changes Made

- Added a `vertex` provider target with `VERTEX_TARGET_API_URL` and
`--vertex-api-url` support.
- Registered explicit Vertex publisher routes for Google
`generateContent`, `streamGenerateContent`, `countTokens` and Anthropic
publisher `rawPredict`, `streamRawPredict` passthrough.
- Added startup banner/routing output for Vertex AI.
- Added focused tests for provider target resolution, CLI/env config,
banner output, and route delegation.
- Added `wiki/vertex.md` with usage examples and Google Cloud source
links.

## Sources

- Vertex AI Gemini inference reference:
https://docs.cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference
- Google Cloud REST authentication:
https://docs.cloud.google.com/docs/authentication/rest
- Google Application Default Credentials:
https://docs.cloud.google.com/docs/authentication/application-default-credentials

## Testing

- [x] Linting passes (`python -m ruff check .`)
- [x] New tests added for new functionality
- [x] Focused unit tests pass
- [ ] Full unit suite completed locally
- [ ] Rust tests completed locally
- [ ] Type checking passes locally

## Test Output

```text
$ python -m ruff check .
All checks passed!

$ python -m pytest tests/test_provider_registry.py tests/test_provider_proxy_routes.py tests/test_cli_proxy_env.py tests/test_banner_upstream_targets.py -q
57 passed, 1 warning in 13.75s
```

Local limitations:

- `python -m pytest tests scripts/tests -q` timed out after 1 hour on
this Windows machine before completing.
- `cargo test -p headroom-proxy --test integration_vertex_raw_predict`
could not run because `cargo` is not installed on PATH in this
environment.
- The commit hook's `mypy` step fails locally on an existing Windows
`fcntl` typing issue in `headroom/subscription/tracker.py`; `ruff`,
`ruff-format`, and plugin-version hooks passed, and the commit was made
with only `mypy` skipped.

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have made corresponding changes to the documentation
- [x] I have added tests that prove the feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
2026-06-09 23:05:30 -07:00
Mansur Japarov
30c1ac8656
fix(proxy): route Claude Code model metadata to Anthropic (#627) 2026-06-05 14:28:17 -08:00
Devanshi Vyas
55579445f8
fix(docs): mkdocs configuration to build with correct folder (#543)
* fix(docs): mkdocs configuration to build with correct folder

* fix(format): fix ruff format for test file
2026-06-02 18:07:11 -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
8325c8a5c9 Remove temporary debug log level plumbing 2026-06-01 16:54:30 -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
chopratejas
993c9076f9 refactor(proxy): migrate Codex WS + OpenAI HTTP + batch handlers; delete Databricks
Completes the migration of every ``metrics.record_request`` call site
in ``headroom/proxy/handlers/`` onto the canonical funnel. After this
commit, **zero ad-hoc record_request calls remain** across the entire
handler subtree. Every request — regardless of provider, harness, or
transport — flows through ``emit_request_outcome``.

Migrated sites (this commit):

* **handle_openai_responses_ws** (Codex WS) — 2 sites:
  - per-turn record (per ``response.completed``)
  - session-end residual (leftover tokens not captured per-turn)
  Pre-refactor these sites emitted only metrics + cost_tracker — no
  RequestLog, no PERF — so Codex traffic was invisible to
  ``headroom perf`` and the recent-requests feed. Funnel restores all
  four effects uniformly per turn. (Closes the visibility half of
  what #471's sibling PR addressed for the scheduler half.)
  The explicit session-summary RequestLog at session-end stays as a
  separate explicit log entry — it's a session-cumulative summary,
  distinct from per-turn observations.

* **handle_openai_chat** — 3 sites:
  - response-cache hit (uses ``from_response_cache=True``)
  - backend-routed (LiteLLM/AnyLLM) non-streaming success
  - direct OpenAI non-streaming success

* **handle_openai_responses** HTTP (Codex HTTP transport) — 1 site

* **handle_passthrough** (OpenAI passthrough endpoints) — 1 site

* **batch.py** handlers — 5 sites:
  - handle_google_batch_create
  - handle_google_batch_passthrough (Files API forward)
  - handle_google_batch_passthrough (list/get/cancel)
  - handle_google_batch_results (CCR-processed)
  - handle_batch_create (OpenAI batches)
  All converge on the funnel. Several gain request_id allocation
  they didn't have before (passthrough sites previously emitted
  ``request_id=None`` in logs).

**Deleted: handle_databricks_invocations + its route + test cases.**

Databricks was a 57-line thin wrapper at openai.py that parsed JSON,
injected the model from URL into body, and delegated to
``handle_openai_chat``. It enabled
``databricks serving-endpoints query <model> --profile HEADROOM``
direct CLI use. No evidence of active users (no docs, no issues, no
mentions). Databricks-hosted models still work via the standard
``/v1/chat/completions`` surface; LiteLLM has its own Databricks
support too. If a user complains, this PR is a 30-minute revert.

Architectural note: also updated 2 more test dummies
(``_DummyOpenAIHandler`` in routing + WS lifecycle tests) to bind
``_record_request_outcome`` via the free function
``emit_request_outcome`` — same pattern as ``_run_compression_in_executor``.

Final migration tally (from P0 audit + extensions):

* **18 audit sites** + **5 batch.py sites discovered during migration** = 23 sites migrated
* **1 site deleted** (Databricks)
* **0 sites remaining** anywhere under ``handlers/``

Surface impact (this commit):

* openai.py: −168 LOC (315 deletions − 147 insertions)
* batch.py: +35 LOC (124 ins − 89 del; mostly comments)
* proxy_routes.py: −4 LOC (Databricks route gone)
* tests: +11 LOC (dummy `_record_request_outcome` bindings, 2 sites)
* Net: ~−126 LOC in production handler code

Tests
* All 157 existing streaming/cache/Codex/anthropic/openai/backpressure/
  routes tests pass with zero regressions.
* ruff + ruff-format + mypy clean.

This brings the cumulative refactor delta (across all 3 commits on
this branch) to:

  contract introduced (outcome.py + funnel):      ~+200 LOC fixed cost
  handler migrations (streaming + anthropic +
    gemini + openai + batch + WS):                 ~−700 LOC
  Databricks deletion:                              −57 LOC
  ──────────────────────────────────────────────  ─────────
  Net production code delta:                       ~−557 LOC

  Plus +474 LOC of test coverage (RequestOutcome unit tests +
  funnel contract assertions).

  And every handler now emits identical observable outputs per
  request: same metrics shape, same cost_tracker shape, same
  RequestLog shape, same PERF format. The wire is uniform.
2026-05-14 19:39:15 -07:00
Tejas Chopra
62a1f23b88 fix: log Codex ws cancellations safely 2026-05-10 09:06:07 -07:00
Garm
efd2ac1ca4 chore: renormalize line endings to LF
`.gitattributes` declares `*.py text eol=lf` and `*.sh text eol=lf`, but
74 files (73 .py, 1 .sh) are stored in the index with CRLF line endings,
violating that contract. Every macOS/Linux clone reports these files as
"modified" on fresh checkout because git's diff engine sees the stored
bytes don't match the attribute contract, even though the working tree
and index match byte-for-byte.

Running `git add --renormalize .` rewrites each affected blob so the
stored form matches the attribute declaration. No semantic changes —
every affected file's diff is "N insertions, N deletions" with inserts
and deletes being the same lines modulo line endings.

Follow-up commit adds `.git-blame-ignore-revs` so `git blame` / GitHub
blame skip this mechanical commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 15:33:30 +02:00
JerrettDavis
7831620eca test: expand provider slice coverage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 21:58:03 -05:00
JerrettDavis
ce8a2f3cf8 test: expand provider pipeline coverage
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-21 23:54:28 -05:00