mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
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.
This commit is contained in:
parent
0ba5065d40
commit
739f654bbd
2 changed files with 25 additions and 0 deletions
|
|
@ -474,6 +474,13 @@ async def _handle_chatgpt_codex_images(
|
|||
def register_provider_routes(app: FastAPI, proxy: Any) -> None:
|
||||
"""Register provider-specific proxy endpoints."""
|
||||
|
||||
def normalize_request_path(request: Request, path: str) -> None:
|
||||
request.scope["path"] = path
|
||||
if "raw_path" in request.scope:
|
||||
request.scope["raw_path"] = quote(path).encode("ascii")
|
||||
if hasattr(request, "_url"):
|
||||
delattr(request, "_url")
|
||||
|
||||
async def vertex_publisher_passthrough(request: Request, publisher: str, action: str):
|
||||
return await proxy.handle_passthrough(
|
||||
request,
|
||||
|
|
@ -486,6 +493,11 @@ def register_provider_routes(app: FastAPI, proxy: Any) -> None:
|
|||
async def anthropic_messages(request: Request):
|
||||
return await proxy.handle_anthropic_messages(request)
|
||||
|
||||
@app.post("/anthropic/v1/messages")
|
||||
async def foundry_anthropic_messages(request: Request):
|
||||
normalize_request_path(request, "/v1/messages")
|
||||
return await proxy.handle_anthropic_messages(request, _api_target(proxy, "anthropic"))
|
||||
|
||||
# AWS Bedrock InvokeModel passthrough. Registered ONLY when an upstream is
|
||||
# configured (`--bedrock-api-url` / BEDROCK_TARGET_API_URL): without it,
|
||||
# `/model/{id}/invoke` keeps falling through to the catch-all (verbatim,
|
||||
|
|
|
|||
|
|
@ -173,6 +173,14 @@ def test_provider_passthrough_routes_forward_expected_targets(monkeypatch) -> No
|
|||
"model": "claude-3-5-sonnet@20240620",
|
||||
"force_stream": False,
|
||||
}
|
||||
assert client.post("/anthropic/v1/messages?beta=true").json() == {
|
||||
"handler": "handle_anthropic_messages",
|
||||
"path": "/v1/messages",
|
||||
"upstream_base_url": "https://api.anthropic.test",
|
||||
"provider": "anthropic",
|
||||
"model": None,
|
||||
"force_stream": False,
|
||||
}
|
||||
non_anthropic_raw = client.post(
|
||||
"/projects/p/locations/us-central1/publishers/google/models/gemini-2.0-flash:rawPredict"
|
||||
).json()
|
||||
|
|
@ -336,6 +344,11 @@ def test_provider_specific_routes_delegate_to_expected_proxy_handlers(monkeypatc
|
|||
|
||||
with TestClient(_app()) as client:
|
||||
assert client.post("/v1/messages").json()["handler"] == "handle_anthropic_messages"
|
||||
assert client.post("/anthropic/v1/messages").json() == {
|
||||
"handler": "handle_anthropic_messages",
|
||||
"path": "/v1/messages",
|
||||
"args": ["https://api.anthropic.test"],
|
||||
}
|
||||
assert (
|
||||
client.post("/v1/messages/batches").json()["handler"] == "handle_anthropic_batch_create"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue