Skip to content

fix(proxy): honor x-headroom-base-url on /v1/messages route#1763

Open
adryanev wants to merge 2 commits into
headroomlabs-ai:mainfrom
adryanev:feature/anthropic-base-url-override
Open

fix(proxy): honor x-headroom-base-url on /v1/messages route#1763
adryanev wants to merge 2 commits into
headroomlabs-ai:mainfrom
adryanev:feature/anthropic-base-url-override

Conversation

@adryanev

@adryanev adryanev commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Description

The Anthropic Messages route (POST /v1/messages) ignored the x-headroom-base-url per-request upstream override and unconditionally forwarded to api.anthropic.com. handle_anthropic_messages already accepts upstream_base_url (it builds the upstream URL via build_copilot_upstream_url), but the route never passed it. Clients that speak the Anthropic Messages wire format while authenticating against a non-Anthropic gateway (e.g. OpenCode Zen's "Go" tier) were forwarded to the real Anthropic API, which rejected the gateway key with 401 invalid x-api-key.

The route now reads and trims x-headroom-base-url and passes it through as upstream_base_url, mirroring the OpenAI-compatible routes and the generic passthrough route (proxy_routes.py:996).

Closes #1760

Type of Change

  • 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/providers/proxy_routes.py: the /v1/messages route reads x-headroom-base-url; when present it strips whitespace and a trailing slash and passes the value as upstream_base_url to handle_anthropic_messages. Absent or whitespace-only headers keep the previous default (api.anthropic.com).
  • tests/test_proxy/test_anthropic_upstream_header.py: new test module pinning the route contract (header present, absent, empty, whitespace-only, trimming + trailing-slash stripping).
  • docs/content/docs/configuration.mdx: new "Proxy upstream override (x-headroom-base-url)" subsection under Per-Request Overrides documenting the header across the OpenAI, Anthropic Messages, and passthrough routes.
  • CHANGELOG.md: Unreleased > Fixed entry for the /v1/messages override.

Testing

  • Unit tests pass (pytest)
  • Linting passes (ruff check .)
  • Type checking passes (mypy headroom)
  • New tests added for new functionality
  • Manual testing performed

Test Output

$ python -m pytest tests/test_proxy/ -k "anthropic or passthrough or bedrock"
collected 140 items / 91 deselected / 49 selected
tests/test_proxy/test_anthropic_upstream_header.py ....                  [ 65%]
...
49 passed, 91 deselected, 1 warning in 79.68s

$ ruff check headroom/providers/proxy_routes.py tests/test_proxy/test_anthropic_upstream_header.py
All checks passed!

$ mypy headroom/providers/proxy_routes.py
Success: no issues found in 1 source file

Real Behavior Proof

Ran the actual headroom proxy against a local mock upstream (a tiny HTTP server on 127.0.0.1:9911 that logs the path it receives) to reproduce the issue's before/after.

  • Environment: local, macOS, Python 3.12; ran headroom proxy --port 8799 against a local mock upstream (a tiny HTTP server on 127.0.0.1:9911 that logs the path it receives).

  • Exact command / steps: started the proxy and the mock upstream, then sent one POST /v1/messages with the override header and one without it (negative control), using these two curl commands.

    # WITH the override header — expect routing to the mock at 127.0.0.1:9911
    curl http://127.0.0.1:8799/v1/messages \
      -H "content-type: application/json" -H "anthropic-version: 2023-06-01" \
      -H "x-headroom-base-url: http://127.0.0.1:9911" \
      -H "x-api-key: zen-test-key" \
      -d '{"model":"glm-5.2","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
    
    # WITHOUT the override header — expect routing to the real api.anthropic.com
    curl http://127.0.0.1:8799/v1/messages \
      -H "content-type: application/json" -H "anthropic-version: 2023-06-01" \
      -H "x-api-key: sk-ant-fake" \
      -d '{"model":"claude-3-5-sonnet-20241022","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
  • Observed result: with the header, the mock upstream logged HIT path=/v1/messages x-api-key=zen-test-key and the proxy returned HTTP 200, confirming the request was routed to <x-headroom-base-url>/v1/messages carrying the gateway key. Without the header, the request went to the real api.anthropic.com (returned HTTP 401 with a genuine request_id and {"type":"authentication_error","message":"invalid x-api-key"}) and the mock received no additional hit — matching the pre-fix behavior in the issue. Also verified by TDD: the two override unit cases failed before the route change (assert None == 'https://opencode.ai/zen/go') and passed after it; all 4 new cases and 49 related proxy tests are green.

  • Not tested: a request against the real OpenCode Zen gateway (no credentials); the gateway path is verified with a local mock upstream instead.

Review Readiness

  • I have performed a self-review
  • This PR is ready for human review

Checklist

  • My code follows the project's style guidelines
  • 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
  • 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

Additional Notes

  • Manual testing against the real OpenCode Zen gateway is N/A (no credentials); a local mock upstream is used instead to prove the routing (see Real Behavior Proof).
  • Scope is limited to /v1/messages. The related /v1/messages/count_tokens route uses a fixed passthrough target and is out of scope for this issue.

The Anthropic Messages route ignored the x-headroom-base-url per-request
upstream override and unconditionally forwarded to api.anthropic.com.
handle_anthropic_messages already supports upstream_base_url; the route
just never passed it. Clients speaking the Anthropic wire format against
a non-Anthropic gateway (e.g. OpenCode Zen) were rejected upstream.

Read and trim the header at the route and pass it through, matching the
OpenAI-compatible and generic passthrough routes.

Fixes headroomlabs-ai#1760
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

PR governance

This PR follows the template and is marked ready for human review.

@github-actions github-actions Bot added status: needs author action Pull request body or readiness checklist still needs author updates status: ready for review Pull request body is complete and the author marked it ready for human review and removed status: needs author action Pull request body or readiness checklist still needs author updates labels Jul 3, 2026
Add a Per-Request Overrides subsection describing the x-headroom-base-url
proxy header and a CHANGELOG entry for the /v1/messages fix (headroomlabs-ai#1760).
@github-actions github-actions Bot added status: needs author action Pull request body or readiness checklist still needs author updates status: ready for review Pull request body is complete and the author marked it ready for human review and removed status: ready for review Pull request body is complete and the author marked it ready for human review status: needs author action Pull request body or readiness checklist still needs author updates labels Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: ready for review Pull request body is complete and the author marked it ready for human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Anthropic Messages route (/v1/messages) ignores x-headroom-base-url override

1 participant