mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
feat(azure-foundry): derive upstream URL from ANTHROPIC_FOUNDRY_RESOURCE (#1138)
## 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) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] 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 - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text --- 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 - [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 - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] 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. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
85786b33a3
commit
e5031b0121
3 changed files with 281 additions and 3 deletions
102
docs/content/docs/claude-code-azure-foundry.mdx
Normal file
102
docs/content/docs/claude-code-azure-foundry.mdx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
---
|
||||
title: Claude Code on Azure AI Foundry
|
||||
description: Run Claude Code against Claude models on Azure AI Foundry, with Headroom compressing your prompts — fewer input tokens, same answers, your own Azure credentials.
|
||||
---
|
||||
|
||||
If your Claude models live on **Azure AI Foundry**, you can still get Headroom's
|
||||
prompt compression. Headroom sits between Claude Code and Azure: it shrinks the
|
||||
big stuff in each request (file reads, logs, tool output) and forwards the rest to
|
||||
Azure using **your own Azure credentials**. You keep your Azure setup; Headroom
|
||||
just makes each call cheaper.
|
||||
|
||||
## What you get
|
||||
|
||||
- **Fewer input tokens** on every Claude Code request to Azure AI Foundry (often
|
||||
30–60% on agent workloads), so you pay for less.
|
||||
- **Same answers** — compression is reversible and content-aware.
|
||||
- **No new secrets** — Headroom never holds your Azure credentials. Claude Code
|
||||
keeps authenticating to Azure AI Foundry with its own `api-key` or Entra Bearer
|
||||
token; Headroom passes it through.
|
||||
|
||||
## Before you start
|
||||
|
||||
You should already have Claude Code working against Azure AI Foundry **without**
|
||||
Headroom. That means these are set in your shell (or your `~/.claude/settings.json`
|
||||
`env` block):
|
||||
|
||||
```bash
|
||||
export CLAUDE_CODE_USE_FOUNDRY=1
|
||||
export ANTHROPIC_FOUNDRY_RESOURCE=<your-azure-resource-name>
|
||||
# e.g. ANTHROPIC_FOUNDRY_RESOURCE=my-org-claude
|
||||
```
|
||||
|
||||
No `ANTHROPIC_API_KEY` is needed — Foundry mode uses your Azure credentials.
|
||||
|
||||
## Run it (one command)
|
||||
|
||||
```bash
|
||||
pip install headroom
|
||||
headroom wrap claude
|
||||
```
|
||||
|
||||
That's it. Because `CLAUDE_CODE_USE_FOUNDRY=1` is set, `headroom wrap claude`
|
||||
automatically:
|
||||
|
||||
1. derives your Azure AI Foundry endpoint from `ANTHROPIC_FOUNDRY_RESOURCE`,
|
||||
2. starts the Headroom proxy with that endpoint as the upstream,
|
||||
3. points Claude Code's Foundry endpoint at the proxy (`ANTHROPIC_FOUNDRY_BASE_URL`),
|
||||
4. leaves your Azure resource, model, and credentials untouched.
|
||||
|
||||
You'll see a line like:
|
||||
|
||||
```
|
||||
Foundry mode: ANTHROPIC_FOUNDRY_BASE_URL=http://127.0.0.1:8787/anthropic
|
||||
→ upstream https://my-org-claude.services.ai.azure.com/anthropic
|
||||
```
|
||||
|
||||
Use Claude Code exactly as you normally would.
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
Claude Code ──(Foundry request)──▶ Headroom ──(compressed)──▶ Azure AI Foundry (Claude)
|
||||
in Foundry mode compresses your resource
|
||||
(your api-key / Entra) ──────── passed through ───────────▶ authenticates you
|
||||
```
|
||||
|
||||
Claude Code sends its Foundry request (Anthropic API format) to Headroom. Headroom
|
||||
compresses the messages, then forwards to your Azure AI Foundry resource endpoint —
|
||||
`https://{ANTHROPIC_FOUNDRY_RESOURCE}.services.ai.azure.com/anthropic` — with your
|
||||
auth headers passed through unchanged.
|
||||
|
||||
## If you have ANTHROPIC_FOUNDRY_BASE_URL set explicitly
|
||||
|
||||
If your environment already has `ANTHROPIC_FOUNDRY_BASE_URL` set to the full Azure
|
||||
endpoint URL, Headroom uses it directly and `ANTHROPIC_FOUNDRY_RESOURCE` is not
|
||||
needed. Either configuration works.
|
||||
|
||||
## Check that compression is working
|
||||
|
||||
1. Open the dashboard: [http://localhost:8787/dashboard](http://localhost:8787/dashboard).
|
||||
"Tokens saved" should climb as you use Claude Code.
|
||||
2. Or check response headers: `x-headroom-tokens-before`, `x-headroom-tokens-after`,
|
||||
`x-headroom-tokens-saved`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Headroom says "ANTHROPIC_BASE_URL" instead of "Foundry mode"**
|
||||
|
||||
`CLAUDE_CODE_USE_FOUNDRY` is not set in the shell where you ran `headroom wrap
|
||||
claude`. Make sure to export it before running, or set it in your shell profile.
|
||||
|
||||
**Claude Code fails with a 401 / auth error**
|
||||
|
||||
Your Azure credentials are not being forwarded correctly. Verify that Claude Code
|
||||
works against Azure AI Foundry directly (without Headroom) before wrapping. If it
|
||||
works direct but not through Headroom, open an issue with the proxy log.
|
||||
|
||||
**"tokens saved" is always 0**
|
||||
|
||||
Check the [dashboard](http://localhost:8787/dashboard) — if requests are flowing
|
||||
but savings are 0, content may be below the compression threshold or the Rust
|
||||
extension may not be installed (`pip install "headroom-ai[proxy]"` includes it).
|
||||
|
|
@ -625,6 +625,34 @@ def _remove_claude_rtk_hooks(settings_path: Path | None = None) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
def _foundry_upstream_url(resource: str) -> str:
|
||||
"""Derive the Azure AI Foundry endpoint URL from a resource name.
|
||||
|
||||
When CLAUDE_CODE_USE_FOUNDRY=1 is set, Claude Code routes requests to the
|
||||
Azure AI Services endpoint it constructs from ANTHROPIC_FOUNDRY_RESOURCE.
|
||||
If ANTHROPIC_FOUNDRY_BASE_URL is not already set in the environment,
|
||||
we derive it here so the proxy knows where to forward compressed requests.
|
||||
|
||||
Azure AI Foundry (AI Services) hosts the Anthropic-format Claude API at:
|
||||
https://{resource}.services.ai.azure.com/anthropic
|
||||
This matches the URL Claude Code constructs internally from ANTHROPIC_FOUNDRY_RESOURCE,
|
||||
and what ANTHROPIC_FOUNDRY_BASE_URL must point to for the Anthropic SDK to reach Claude.
|
||||
"""
|
||||
return f"https://{resource.strip()}.services.ai.azure.com/anthropic"
|
||||
|
||||
|
||||
def _foundry_proxy_url(proxy_url: str) -> str:
|
||||
"""Return the local proxy URL that Claude Code should use in Foundry mode.
|
||||
|
||||
ANTHROPIC_FOUNDRY_BASE_URL is the full base URL the Anthropic SDK appends
|
||||
/v1/messages to, so it must include the /anthropic path component to match
|
||||
the Azure AI Foundry endpoint structure. _claude_proxy_base_url() returns
|
||||
the bare http://127.0.0.1:<port> — this helper appends /anthropic so the
|
||||
proxy URL Claude Code receives mirrors the real Foundry URL shape.
|
||||
"""
|
||||
return proxy_url.rstrip("/") + "/anthropic"
|
||||
|
||||
|
||||
def _write_claude_wrap_base_url(
|
||||
proxy_url: str,
|
||||
*,
|
||||
|
|
@ -3110,9 +3138,16 @@ def claude(
|
|||
|
||||
# Detect Foundry mode: Claude Code uses ANTHROPIC_FOUNDRY_BASE_URL instead of
|
||||
# ANTHROPIC_BASE_URL when CLAUDE_CODE_USE_FOUNDRY=1 is set.
|
||||
# Users typically set ANTHROPIC_FOUNDRY_RESOURCE (the resource name) rather
|
||||
# than the full ANTHROPIC_FOUNDRY_BASE_URL. When the URL is absent we derive
|
||||
# it from the resource name so the proxy has an upstream to forward to.
|
||||
foundry_upstream = None
|
||||
if os.environ.get("CLAUDE_CODE_USE_FOUNDRY"):
|
||||
foundry_upstream = os.environ.get("ANTHROPIC_FOUNDRY_BASE_URL")
|
||||
if not foundry_upstream:
|
||||
resource = os.environ.get("ANTHROPIC_FOUNDRY_RESOURCE", "").strip()
|
||||
if resource:
|
||||
foundry_upstream = _foundry_upstream_url(resource)
|
||||
|
||||
# Detect Vertex mode: with CLAUDE_CODE_USE_VERTEX=1, Claude Code IGNORES
|
||||
# ANTHROPIC_BASE_URL and authenticates to Google Vertex with GCP ADC. The
|
||||
|
|
@ -3176,7 +3211,7 @@ def claude(
|
|||
)
|
||||
elif foundry_upstream:
|
||||
click.echo(
|
||||
f" Foundry mode: ANTHROPIC_FOUNDRY_BASE_URL={proxy_url} → upstream {foundry_upstream}"
|
||||
f" Foundry mode: ANTHROPIC_FOUNDRY_BASE_URL={_foundry_proxy_url(proxy_url)} → upstream {foundry_upstream}"
|
||||
)
|
||||
else:
|
||||
click.echo(f" ANTHROPIC_BASE_URL={proxy_url}")
|
||||
|
|
@ -3192,7 +3227,10 @@ def claude(
|
|||
# we only redirect its Vertex endpoint to Headroom.
|
||||
env["ANTHROPIC_VERTEX_BASE_URL"] = proxy_url
|
||||
elif foundry_upstream:
|
||||
env["ANTHROPIC_FOUNDRY_BASE_URL"] = proxy_url
|
||||
# ANTHROPIC_FOUNDRY_BASE_URL is the base URL the Anthropic SDK
|
||||
# appends /v1/messages to. The real Foundry URL includes /anthropic,
|
||||
# so the proxy URL must mirror that structure.
|
||||
env["ANTHROPIC_FOUNDRY_BASE_URL"] = _foundry_proxy_url(proxy_url)
|
||||
else:
|
||||
env["ANTHROPIC_BASE_URL"] = proxy_url
|
||||
|
||||
|
|
@ -3201,7 +3239,8 @@ def claude(
|
|||
# daemon's environment) also route through Headroom.
|
||||
_settings_foundry[0] = bool(foundry_upstream)
|
||||
_saved_base_url[0] = _write_claude_wrap_base_url(
|
||||
proxy_url, foundry_mode=_settings_foundry[0]
|
||||
_foundry_proxy_url(proxy_url) if _settings_foundry[0] else proxy_url,
|
||||
foundry_mode=_settings_foundry[0],
|
||||
)
|
||||
|
||||
# Per-project savings attribution: tag every request with the launch
|
||||
|
|
|
|||
137
tests/test_azure_foundry_claude_compression.py
Normal file
137
tests/test_azure_foundry_claude_compression.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
"""Azure AI Foundry + Claude Code compression wiring.
|
||||
|
||||
Covers the gap fixed by feat(azure-foundry): when only ANTHROPIC_FOUNDRY_RESOURCE
|
||||
is set (no explicit ANTHROPIC_FOUNDRY_BASE_URL), wrap claude must derive the
|
||||
upstream URL and route Claude Code's Foundry requests through the proxy.
|
||||
|
||||
No real Azure endpoint is contacted — helpers are unit-tested directly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from headroom.cli import wrap as wrap_cli
|
||||
from headroom.providers.claude import proxy_base_url as _claude_proxy_base_url
|
||||
from headroom.providers.registry import resolve_api_overrides
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Upstream URL derivation from ANTHROPIC_FOUNDRY_RESOURCE
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_foundry_upstream_url_builds_services_endpoint() -> None:
|
||||
assert (
|
||||
wrap_cli._foundry_upstream_url("my-org-claude")
|
||||
== "https://my-org-claude.services.ai.azure.com/anthropic"
|
||||
)
|
||||
|
||||
|
||||
def test_foundry_upstream_url_strips_whitespace() -> None:
|
||||
assert (
|
||||
wrap_cli._foundry_upstream_url(" my-resource ")
|
||||
== "https://my-resource.services.ai.azure.com/anthropic"
|
||||
)
|
||||
|
||||
|
||||
def test_foundry_upstream_url_preserves_hyphens_and_digits() -> None:
|
||||
assert (
|
||||
wrap_cli._foundry_upstream_url("avanade-claude-42")
|
||||
== "https://avanade-claude-42.services.ai.azure.com/anthropic"
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Local proxy URL for Foundry mode includes /anthropic path component
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_foundry_proxy_url_appends_anthropic_path() -> None:
|
||||
proxy_url = _claude_proxy_base_url(8787) # http://127.0.0.1:8787
|
||||
assert wrap_cli._foundry_proxy_url(proxy_url) == "http://127.0.0.1:8787/anthropic"
|
||||
|
||||
|
||||
def test_foundry_proxy_url_strips_trailing_slash() -> None:
|
||||
assert (
|
||||
wrap_cli._foundry_proxy_url("http://127.0.0.1:8787/") == "http://127.0.0.1:8787/anthropic"
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# resolve_api_overrides picks up ANTHROPIC_FOUNDRY_BASE_URL as anthropic target
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_resolve_api_overrides_uses_foundry_base_url_as_anthropic_target() -> None:
|
||||
overrides = resolve_api_overrides(
|
||||
anthropic_api_url=None,
|
||||
openai_api_url=None,
|
||||
gemini_api_url=None,
|
||||
cloudcode_api_url=None,
|
||||
environ={
|
||||
"ANTHROPIC_FOUNDRY_BASE_URL": "https://my-resource.services.ai.azure.com/anthropic"
|
||||
},
|
||||
)
|
||||
assert overrides.anthropic == "https://my-resource.services.ai.azure.com/anthropic"
|
||||
|
||||
|
||||
def test_resolve_api_overrides_explicit_target_beats_foundry_base_url() -> None:
|
||||
# ANTHROPIC_TARGET_API_URL takes precedence; FOUNDRY_BASE_URL is the fallback.
|
||||
overrides = resolve_api_overrides(
|
||||
anthropic_api_url=None,
|
||||
openai_api_url=None,
|
||||
gemini_api_url=None,
|
||||
cloudcode_api_url=None,
|
||||
environ={
|
||||
"ANTHROPIC_TARGET_API_URL": "https://explicit-override.example.com",
|
||||
"ANTHROPIC_FOUNDRY_BASE_URL": "https://my-resource.services.ai.azure.com/anthropic",
|
||||
},
|
||||
)
|
||||
assert overrides.anthropic == "https://explicit-override.example.com"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# settings.json written with ANTHROPIC_FOUNDRY_BASE_URL in Foundry mode
|
||||
# Uses _claude_proxy_base_url + _foundry_proxy_url to cover the real wrap path
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _settings(tmp_path: Path) -> Path:
|
||||
return tmp_path / ".claude" / "settings.json"
|
||||
|
||||
|
||||
def test_write_foundry_mode_sets_foundry_key(tmp_path: Path) -> None:
|
||||
path = _settings(tmp_path)
|
||||
# Mirror the real production path: derive proxy_url then apply _foundry_proxy_url
|
||||
proxy_url = _claude_proxy_base_url(8787)
|
||||
foundry_url = wrap_cli._foundry_proxy_url(proxy_url)
|
||||
wrap_cli._write_claude_wrap_base_url(foundry_url, foundry_mode=True, settings_path=path)
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
assert payload["env"]["ANTHROPIC_FOUNDRY_BASE_URL"] == "http://127.0.0.1:8787/anthropic"
|
||||
assert "ANTHROPIC_BASE_URL" not in payload["env"]
|
||||
|
||||
|
||||
def test_write_non_foundry_mode_does_not_set_foundry_key(tmp_path: Path) -> None:
|
||||
path = _settings(tmp_path)
|
||||
proxy_url = _claude_proxy_base_url(8787)
|
||||
wrap_cli._write_claude_wrap_base_url(proxy_url, settings_path=path)
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
assert payload["env"]["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:8787"
|
||||
assert "ANTHROPIC_FOUNDRY_BASE_URL" not in payload["env"]
|
||||
|
||||
|
||||
def test_restore_foundry_mode_removes_foundry_key(tmp_path: Path) -> None:
|
||||
path = _settings(tmp_path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(
|
||||
json.dumps({"env": {"ANTHROPIC_FOUNDRY_BASE_URL": "http://127.0.0.1:8787/anthropic"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
wrap_cli._restore_claude_wrap_base_url(None, foundry_mode=True, settings_path=path)
|
||||
# The restore may delete the file entirely when the env dict becomes empty,
|
||||
# or leave a file with the key absent — both indicate correct removal.
|
||||
if path.exists():
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
assert "ANTHROPIC_FOUNDRY_BASE_URL" not in payload.get("env", {})
|
||||
# else: file deleted — key is gone, which is also correct
|
||||
Loading…
Add table
Add a link
Reference in a new issue