Skip to content

Commit 739f654

Browse files
authored
fix(proxy): route Foundry Anthropic messages (headroomlabs-ai#1878)
## Description Closes headroomlabs-ai#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.
1 parent 0ba5065 commit 739f654

2 files changed

Lines changed: 25 additions & 0 deletions

File tree

‎headroom/providers/proxy_routes.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,13 @@ async def _handle_chatgpt_codex_images(
474474
def register_provider_routes(app: FastAPI, proxy: Any) -> None:
475475
"""Register provider-specific proxy endpoints."""
476476

477+
def normalize_request_path(request: Request, path: str) -> None:
478+
request.scope["path"] = path
479+
if "raw_path" in request.scope:
480+
request.scope["raw_path"] = quote(path).encode("ascii")
481+
if hasattr(request, "_url"):
482+
delattr(request, "_url")
483+
477484
async def vertex_publisher_passthrough(request: Request, publisher: str, action: str):
478485
return await proxy.handle_passthrough(
479486
request,
@@ -486,6 +493,11 @@ async def vertex_publisher_passthrough(request: Request, publisher: str, action:
486493
async def anthropic_messages(request: Request):
487494
return await proxy.handle_anthropic_messages(request)
488495

496+
@app.post("/anthropic/v1/messages")
497+
async def foundry_anthropic_messages(request: Request):
498+
normalize_request_path(request, "/v1/messages")
499+
return await proxy.handle_anthropic_messages(request, _api_target(proxy, "anthropic"))
500+
489501
# AWS Bedrock InvokeModel passthrough. Registered ONLY when an upstream is
490502
# configured (`--bedrock-api-url` / BEDROCK_TARGET_API_URL): without it,
491503
# `/model/{id}/invoke` keeps falling through to the catch-all (verbatim,

‎tests/test_provider_proxy_routes.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ async def fake_gemini_count(
173173
"model": "claude-3-5-sonnet@20240620",
174174
"force_stream": False,
175175
}
176+
assert client.post("/anthropic/v1/messages?beta=true").json() == {
177+
"handler": "handle_anthropic_messages",
178+
"path": "/v1/messages",
179+
"upstream_base_url": "https://api.anthropic.test",
180+
"provider": "anthropic",
181+
"model": None,
182+
"force_stream": False,
183+
}
176184
non_anthropic_raw = client.post(
177185
"/projects/p/locations/us-central1/publishers/google/models/gemini-2.0-flash:rawPredict"
178186
).json()
@@ -336,6 +344,11 @@ async def fake(self, request, *args): # type: ignore[no-untyped-def]
336344

337345
with TestClient(_app()) as client:
338346
assert client.post("/v1/messages").json()["handler"] == "handle_anthropic_messages"
347+
assert client.post("/anthropic/v1/messages").json() == {
348+
"handler": "handle_anthropic_messages",
349+
"path": "/v1/messages",
350+
"args": ["https://api.anthropic.test"],
351+
}
339352
assert (
340353
client.post("/v1/messages/batches").json()["handler"] == "handle_anthropic_batch_create"
341354
)

0 commit comments

Comments
 (0)