Skip to content

feat(azure-foundry): derive upstream URL from ANTHROPIC_FOUNDRY_RESOURCE#1138

Open
ravensorb wants to merge 4 commits into
chopratejas:mainfrom
ravensorb:feat/azure-foundry-claude-compression
Open

feat(azure-foundry): derive upstream URL from ANTHROPIC_FOUNDRY_RESOURCE#1138
ravensorb wants to merge 4 commits into
chopratejas:mainfrom
ravensorb:feat/azure-foundry-claude-compression

Conversation

@ravensorb

@ravensorb ravensorb commented Jun 18, 2026

Copy link
Copy Markdown

Description

Closes #1133

When CLAUDE_CODE_USE_FOUNDRY=1 is set, Claude Code routes all API traffic to an Azure AI Foundry endpoint (https://{resource}.services.ai.azure.com/anthropic) rather than api.anthropic.com. The proxy never sees this traffic, so compression is silently skipped.

wrap.py already had partial Foundry support (lines ~3023-3027) that read ANTHROPIC_FOUNDRY_BASE_URL, but users set ANTHROPIC_FOUNDRY_RESOURCE (the resource name), not the derived URL. When only the resource name was present foundry_upstream was None and 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) — derives https://{resource}.services.ai.azure.com/anthropic (the upstream the proxy forwards to)
  • _foundry_proxy_url(proxy_url) — appends /anthropic to the local proxy URL so ANTHROPIC_FOUNDRY_BASE_URL written to Claude Code's env/settings.json matches the Foundry URL structure the Anthropic SDK expects
  • Detection block — reads ANTHROPIC_FOUNDRY_BASE_URL first; falls back to deriving from ANTHROPIC_FOUNDRY_RESOURCE

Bug found during live testing: _foundry_upstream_url initially returned the bare domain (HTTP 404). Live testing confirmed the correct path is .../anthropic. Fixed before review.

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/cli/wrap.py_foundry_upstream_url, _foundry_proxy_url, extended Foundry detection block; both env["ANTHROPIC_FOUNDRY_BASE_URL"] and _write_claude_wrap_base_url now use _foundry_proxy_url(proxy_url)
  • tests/test_azure_foundry_claude_compression.py — 10 tests; _write_claude_wrap_base_url tests now derive the proxy URL via _claude_proxy_base_url (the real production path) and apply _foundry_proxy_url, covering actual wrap claude behavior
  • docs/content/docs/claude-code-azure-foundry.mdx — new user guide

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

--- ruff check ---
All checks passed!
--- ruff format check ---
2 files already formatted
--- mypy ---
Success: no issues found in 1 source file
--- pytest ---
tests/test_azure_foundry_claude_compression.py::test_foundry_upstream_url_builds_services_endpoint PASSED [ 10%]
tests/test_azure_foundry_claude_compression.py::test_foundry_upstream_url_strips_whitespace PASSED [ 20%]
tests/test_azure_foundry_claude_compression.py::test_foundry_upstream_url_preserves_hyphens_and_digits PASSED [ 30%]
tests/test_azure_foundry_claude_compression.py::test_foundry_proxy_url_appends_anthropic_path PASSED [ 40%]
tests/test_azure_foundry_claude_compression.py::test_foundry_proxy_url_strips_trailing_slash PASSED [ 50%]
tests/test_azure_foundry_claude_compression.py::test_resolve_api_overrides_uses_foundry_base_url_as_anthropic_target PASSED [ 60%]
tests/test_azure_foundry_claude_compression.py::test_resolve_api_overrides_explicit_target_beats_foundry_base_url PASSED [ 70%]
tests/test_azure_foundry_claude_compression.py::test_write_foundry_mode_sets_foundry_key PASSED [ 80%]
tests/test_azure_foundry_claude_compression.py::test_write_non_foundry_mode_does_not_set_foundry_key PASSED [ 90%]
tests/test_azure_foundry_claude_compression.py::test_restore_foundry_mode_removes_foundry_key PASSED [100%]

======================== 10 passed, 1 warning in 0.80s =========================

Environment: Docker python:3.12-slim, headroom-ai[proxy] from PyPI + patched wrap.py overlay

Real Behavior Proof

  • Environment: Private Azure AI Foundry resource (claude-sonnet-4-6 deployment, East US 2); headroom proxy running in Docker python:3.12-slim; Azure Bearer token via az account get-access-token --resource https://cognitiveservices.azure.com; Linux/WSL2

  • Exact command / steps: Started headroom proxy --port 8788 with ANTHROPIC_FOUNDRY_BASE_URL=https://my-resource.services.ai.azure.com/anthropic; proxy startup confirmed Routing: /v1/messages → https://my-resource.services.ai.azure.com/anthropic; then ran curl -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 claude end-to-end (proxy + Claude Code settings injection + full agent session). The proxy routes correctly to Foundry and returns real responses; wrap plumbing (_foundry_proxy_url + _write_claude_wrap_base_url) is unit-tested against the real _claude_proxy_base_url production path.

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

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.

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)
@github-actions

github-actions Bot commented Jun 18, 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 the status: needs author action Pull request body or readiness checklist still needs author updates label Jun 18, 2026
@ravensorb ravensorb marked this pull request as draft June 18, 2026 19:01
…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>
@ravensorb ravensorb marked this pull request as ready for review June 19, 2026 11:43
@ravensorb

Copy link
Copy Markdown
Author

Everything should be good to go with this PR now

@JerrettDavis JerrettDavis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@github-actions github-actions Bot added 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 Jun 19, 2026
@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 45.45455% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
headroom/cli/wrap.py 45.45% 4 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

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.

feat(azure-foundry): turnkey Claude Code + Azure AI Foundry compression

2 participants