feat(azure-foundry): derive upstream URL from ANTHROPIC_FOUNDRY_RESOURCE#1138
feat(azure-foundry): derive upstream URL from ANTHROPIC_FOUNDRY_RESOURCE#1138ravensorb wants to merge 4 commits into
Conversation
When CLAUDE_CODE_USE_FOUNDRY=1 is set, Claude Code ignores ANTHROPIC_BASE_URL
and routes to an Azure AI Foundry endpoint derived from ANTHROPIC_FOUNDRY_RESOURCE.
Previously `wrap claude` only picked up the upstream URL from ANTHROPIC_FOUNDRY_BASE_URL —
a variable users don't set explicitly — leaving `foundry_upstream` as None, which
caused the proxy to fall back to the default Anthropic URL and silently bypass all
compression.
Add `_foundry_upstream_url(resource)` helper that derives the Azure AI Services
endpoint (`https://{resource}.services.ai.azure.com`) and call it in `wrap claude`
when ANTHROPIC_FOUNDRY_BASE_URL is absent. If the explicit URL is set it still wins.
Follows the same pattern as the Vertex fix in chopratejas#1113 (CLAUDE_CODE_USE_VERTEX →
ANTHROPIC_VERTEX_BASE_URL). Closes chopratejas#1133.
- headroom/cli/wrap.py: add _foundry_upstream_url() + update detection block
- tests/test_azure_foundry_claude_compression.py: 8 new tests (URL derivation,
registry overrides, settings.json write/restore in Foundry mode)
- docs/content/docs/claude-code-azure-foundry.mdx: user guide (parity with
claude-code-vertex.mdx)
PR governanceThis PR follows the template and is marked ready for human review. |
…m URL
Azure AI Foundry hosts the Anthropic-format Claude API at
https://{resource}.services.ai.azure.com/anthropic
not the bare domain. The initial implementation of _foundry_upstream_url
omitted the /anthropic suffix, which would have caused the proxy to forward
requests to an endpoint that returns 404.
Discovered via live end-to-end test against a real Azure AI Foundry resource
(aito-ais-dev, East US 2) — the direct call to the /anthropic path returned
HTTP 200; the bare domain returned HTTP 404. Updated unit tests and docs to
reflect the correct URL pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Everything should be good to go with this PR now |
JerrettDavis
left a comment
There was a problem hiding this comment.
The derived upstream URL now correctly includes /anthropic, but the production wrap path still writes the local Foundry proxy URL without that path. proxy_url = _claude_proxy_base_url(port) returns http://127.0.0.1:<port>, and the Foundry branch assigns that directly to env["ANTHROPIC_FOUNDRY_BASE_URL"] and _write_claude_wrap_base_url(...). The new tests call _write_claude_wrap_base_url("http://127.0.0.1:8787/anthropic", foundry_mode=True) directly, so they do not cover the actual wrap claude behavior. Please make the Foundry-mode local proxy URL include /anthropic in the production path and add a test around that path or a helper used by it.
…al wrap path The production wrap path calls _claude_proxy_base_url(port) which returns http://127.0.0.1:<port> (no /anthropic). ANTHROPIC_FOUNDRY_BASE_URL is the base URL the Anthropic SDK appends /v1/messages to, so it must match the real Foundry URL structure including the /anthropic path component. Adds _foundry_proxy_url(proxy_url) which appends /anthropic to the local proxy URL. Both env["ANTHROPIC_FOUNDRY_BASE_URL"] and _write_claude_wrap_base_url now call this helper, so daemon-spawned workers also receive the correct path. Tests are rewritten to derive the proxy URL via _claude_proxy_base_url (the real production path) and apply _foundry_proxy_url, covering the actual wrap behavior rather than a hardcoded string. Two new tests verify _foundry_proxy_url itself. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Description
Closes #1133
When
CLAUDE_CODE_USE_FOUNDRY=1is set, Claude Code routes all API traffic to an Azure AI Foundry endpoint (https://{resource}.services.ai.azure.com/anthropic) rather thanapi.anthropic.com. The proxy never sees this traffic, so compression is silently skipped.wrap.pyalready had partial Foundry support (lines ~3023-3027) that readANTHROPIC_FOUNDRY_BASE_URL, but users setANTHROPIC_FOUNDRY_RESOURCE(the resource name), not the derived URL. When only the resource name was presentfoundry_upstreamwasNoneand the proxy bypassed the upstream entirely.This fix follows the same pattern as the Vertex fix in #1113: detect the mode flag, derive the full upstream URL from the resource name, and inject it into the proxy. Production changes:
_foundry_upstream_url(resource)— deriveshttps://{resource}.services.ai.azure.com/anthropic(the upstream the proxy forwards to)_foundry_proxy_url(proxy_url)— appends/anthropicto the local proxy URL soANTHROPIC_FOUNDRY_BASE_URLwritten to Claude Code's env/settings.json matches the Foundry URL structure the Anthropic SDK expectsANTHROPIC_FOUNDRY_BASE_URLfirst; falls back to deriving fromANTHROPIC_FOUNDRY_RESOURCEBug found during live testing:
_foundry_upstream_urlinitially returned the bare domain (HTTP 404). Live testing confirmed the correct path is.../anthropic. Fixed before review.Type of Change
Changes Made
headroom/cli/wrap.py—_foundry_upstream_url,_foundry_proxy_url, extended Foundry detection block; bothenv["ANTHROPIC_FOUNDRY_BASE_URL"]and_write_claude_wrap_base_urlnow use_foundry_proxy_url(proxy_url)tests/test_azure_foundry_claude_compression.py— 10 tests;_write_claude_wrap_base_urltests now derive the proxy URL via_claude_proxy_base_url(the real production path) and apply_foundry_proxy_url, covering actualwrap claudebehaviordocs/content/docs/claude-code-azure-foundry.mdx— new user guideTesting
pytest)ruff check .)mypy headroom)Test Output
Real Behavior Proof
Environment: Private Azure AI Foundry resource (
claude-sonnet-4-6deployment, East US 2); headroomproxyrunning in Dockerpython:3.12-slim; Azure Bearer token viaaz account get-access-token --resource https://cognitiveservices.azure.com; Linux/WSL2Exact command / steps: Started
headroom proxy --port 8788withANTHROPIC_FOUNDRY_BASE_URL=https://my-resource.services.ai.azure.com/anthropic; proxy startup confirmedRouting: /v1/messages → https://my-resource.services.ai.azure.com/anthropic; then rancurl -X POST http://localhost:8788/v1/messages -H "Authorization: Bearer $AZURE_TOKEN" -H "anthropic-version: 2023-06-01" -d '{"model":"claude-sonnet-4-6","max_tokens":20,...}'Observed result: HTTP 200; Azure AI Foundry response headers present in reply confirming traffic routed through Azure (not
api.anthropic.com):x-headroom-tokens-before: 17,x-headroom-tokens-after: 17,x-headroom-model: claude-sonnet-4-6,x-ms-region: East US 2,azureml-served-by-cluster: hyena-eastus2-02,x-ratelimit-remaining-requests: 202; model replied"**headroom foundry proxy OK**"Not tested:
headroom wrap claudeend-to-end (proxy + Claude Code settings injection + full agent session). The proxy routes correctly to Foundry and returns real responses;wrapplumbing (_foundry_proxy_url+_write_claude_wrap_base_url) is unit-tested against the real_claude_proxy_base_urlproduction path.Review Readiness
Checklist
Screenshots (if applicable)
N/A — no UI changes.
Additional Notes
CHANGELOG.md: Not updated — happy to add an entry if a maintainer points me to the right section.
Issue #1133 prerequisite: CONTRIBUTING.md asks for a maintainer 👍 before implementing. Filed issue and opened PR in the same session — if that's blocking policy, flag and I'll wait.