mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description
Fix three related gaps in Bedrock support that prevented headroom from
working with Claude Code when `CLAUDE_CODE_USE_BEDROCK=0` and
`ANTHROPIC_BASE_URL` is pointed at the proxy:
1. **ARN passthrough used the wrong LiteLLM route** — application
inference profile ARNs (e.g.
`arn:aws:bedrock:ap-southeast-2:<account>:application-inference-profile/<id>`)
were forwarded as `bedrock/<arn>`, which LiteLLM rejects with HTTP 400
"Try calling via converse route". Fixed to `bedrock/converse/<arn>`.
2. **Named AWS profile not forwarded to completion calls** —
`--bedrock-profile` was wired through the CLI → config →
`LiteLLMBackend.__init__` and used to fetch the model map at startup,
but never stored on `self`. All four `acompletion()` call sites
(`send_message`, `stream_message`, `send_openai_message`,
`stream_openai_message`) passed only `aws_region_name` — the
actual Bedrock calls used ambient credentials regardless of the flag.
Fixed by storing `self.profile_name` and passing `aws_profile_name=` to
every `acompletion()` call.
3. **`ap-southeast-2` used the wrong region prefix** — Australia should
use `au.` for cross-region inference profile IDs, not `apac.`. Added
`ap-southeast-2 → "au"` to `_BEDROCK_REGION_PREFIXES` and `"au."` to the
strip list in `_normalize_bedrock_profile_id`.
Closes #
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `backends/litellm.py`: route `arn:aws:` model IDs via
`bedrock/converse/<arn>` in `map_model_id`
- `backends/litellm.py`: store `profile_name` as `self.profile_name` in
`LiteLLMBackend.__init__`; pass `aws_profile_name=` to `acompletion()`
in all four call sites; use
`boto3.Session(profile_name=...)` for startup discovery; cache key is
`region:profile_name` to prevent cross-profile collisions
- `backends/litellm.py`: add `ap-southeast-2 → "au"` to
`_BEDROCK_REGION_PREFIXES`; add `"au."` to prefix strip list in
`_normalize_bedrock_profile_id`
- `providers/registry.py`: pass `profile_name=bedrock_profile` to
`LiteLLMBackend`
- `proxy/server.py`: pass `config.bedrock_profile` to
`create_proxy_backend`
- `docs/claude-code-bedrock-headroom.md`: remove false claim that ARNs
in `ANTHROPIC_DEFAULT_*_MODEL` bypass the proxy; fix troubleshooting
table
- `tests/test_bedrock_region.py`: update `test_arn_passthrough` to
expect `bedrock/converse/<arn>`; update cache key format; add
`test_profile_cache_isolation`,
`test_ap_southeast_2_uses_au_prefix`, and
`TestBedrockProfileForwardedToCompletion` (3 async tests asserting
`aws_profile_name` appears in `acompletion()` kwargs for named profiles
and is
absent for the no-profile case)
- `tests/test_provider_registry*.py`,
`test_vertex_claude_compression.py`: update `litellm_backend_cls` stubs
to accept `profile_name=None`
## Testing
- [x] Unit tests pass (`pytest`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ pytest tests/test_bedrock_region.py tests/test_provider_registry.py tests/test_provider_registry_extended.py \
-k "not test_fallback_when_boto3_import_fails and not test_fallback_when_api_call_fails and not test_successful_fetch" -q
collected 51 items / 3 deselected / 48 selected
tests/test_bedrock_region.py ...........................
tests/test_provider_registry.py ...........
tests/test_provider_registry_extended.py .......
48 passed, 3 deselected in 2.00s
```
Note: 3 deselected tests use patch("builtins.__import__") which hangs
under Python 3.13 — pre-existing issue unrelated to these changes.
## Real Behavior Proof
- Environment: macOS, Python 3.13, Claude Code with
`CLAUDE_CODE_USE_BEDROCK=0`, `ANTHROPIC_BASE_URL=http://127.0.0.1:8787`,
AWS ap-southeast-2, application inference profile ARNs in
`ANTHROPIC_DEFAULT_*_MODEL`
- Exact command / steps: `headroom proxy --port 8787 --backend bedrock
--region ap-southeast-2 --bedrock-profile "my-sso-profile"`
- Observed result: Requests routed correctly to
`bedrock/converse/arn:aws:bedrock:ap-southeast-2:...:application-inference-profile/<id>`
as confirmed in LiteLLM logs
- Not tested: EU/APAC region ARN passthrough (logic is identical);
non-SSO credential flows
```text
15:29:44 - LiteLLM:INFO: utils.py:4090 -
LiteLLM completion() model= converse/arn:aws:bedrock:ap-southeast-2:<account>:application-inference-profile/<id>; provider = bedrock
2026-06-26 15:29:44,322 - LiteLLM - INFO -
LiteLLM completion() model= converse/arn:aws:bedrock:ap-southeast-2:<account>:application-inference-profile/<id>; provider = bedrock
15:31:09 - LiteLLM:INFO: utils.py:4090 -
LiteLLM completion() model= converse/arn:aws:bedrock:ap-southeast-2:<account>:application-inference-profile/<id>; provider = bedrock
2026-06-26 15:31:09,928 - LiteLLM - INFO -
LiteLLM completion() model= converse/arn:aws:bedrock:ap-southeast-2:<account>:application-inference-profile/<id>; provider = bedrock
15:34:26 - LiteLLM:INFO: utils.py:4090 -
LiteLLM completion() model= converse/arn:aws:bedrock:ap-southeast-2:<account>:application-inference-profile/<id>; provider = bedrock
2026-06-26 15:34:26,811 - LiteLLM - INFO -
LiteLLM completion() model= converse/arn:aws:bedrock:ap-southeast-2:<account>:application-inference-profile/<id>; provider = bedrock
```
## 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] 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
## Additional Notes
The 3 skipped tests (`test_fallback_when_boto3_import_fails`,
`test_fallback_when_api_call_fails`, `test_successful_fetch`) pre-exist
in the repo and use `patch("builtins.__import__")` which hangs under
Python 3.13. Not affected by these changes.
---------
Co-authored-by: Matt Haitana <mhaitana@costar.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
147 lines
5.7 KiB
Markdown
147 lines
5.7 KiB
Markdown
# Claude Code + AWS Bedrock, with Headroom compression
|
|
|
|
*Validated end-to-end on 2026-06-26 (Claude Code 2.1, Headroom 0.27.0, ap-southeast-2).*
|
|
|
|
This is the **working, tested** way to run **Claude Code** against **Claude models on
|
|
AWS Bedrock** with **Headroom compressing the context** in the middle.
|
|
|
|
## TL;DR
|
|
|
|
Run Claude Code in **normal Anthropic mode** (NOT Bedrock mode) pointed at a local
|
|
Headroom proxy, and let **Headroom** be the thing that talks to Bedrock:
|
|
|
|
```
|
|
Claude Code ──ANTHROPIC_BASE_URL──▶ Headroom proxy ──LiteLLM (bedrock)──▶ AWS Bedrock
|
|
(normal mode) (plain http) (compresses) (your AWS creds) (Claude)
|
|
```
|
|
|
|
One non-obvious requirement makes the difference between "works" and "silently bypasses
|
|
the proxy":
|
|
|
|
1. **`CLAUDE_CODE_USE_BEDROCK=0`** — Without this, Claude Code sees the
|
|
`CLAUDE_CODE_USE_BEDROCK=1` flag and calls Bedrock directly via the AWS SDK,
|
|
completely bypassing `ANTHROPIC_BASE_URL` and the proxy.
|
|
|
|
## Why not "just set CLAUDE_CODE_USE_BEDROCK=1"?
|
|
|
|
That approach **does not work** with Headroom. When `CLAUDE_CODE_USE_BEDROCK=1` is set,
|
|
Claude Code calls Bedrock directly using the AWS SDK — `ANTHROPIC_BASE_URL` is ignored
|
|
entirely and the proxy never receives a byte. Use the Anthropic-mode path below.
|
|
|
|
## Prerequisites
|
|
|
|
- **AWS credentials** configured for your environment (env vars, `~/.aws/credentials`,
|
|
instance profile, or SSO via `aws sso login`). Confirm direct access works before
|
|
involving Headroom:
|
|
```bash
|
|
aws bedrock-runtime invoke-model \
|
|
--region us-east-1 \
|
|
--model-id anthropic.claude-3-haiku-20240307-v1:0 \
|
|
--body '{"anthropic_version":"bedrock-2023-05-31","max_tokens":20,"messages":[{"role":"user","content":"hi"}]}' \
|
|
/tmp/out.json
|
|
```
|
|
- **boto3** in the proxy's Python environment (for dynamic inference profile discovery):
|
|
```bash
|
|
pip install boto3
|
|
```
|
|
- **IAM permissions** for the models you intend to use — at minimum
|
|
`bedrock:InvokeModel` and `bedrock:InvokeModelWithResponseStream`. For application
|
|
inference profiles, scope to the specific profile ARN:
|
|
```json
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
|
|
"Resource": ["arn:aws:bedrock:<region>:<account>:application-inference-profile/<id>"]
|
|
}
|
|
```
|
|
|
|
## Terminal 1 — start the Headroom proxy (Bedrock backend)
|
|
|
|
```bash
|
|
headroom proxy --port 8787 \
|
|
--backend bedrock \
|
|
--region us-east-1
|
|
```
|
|
|
|
With a named AWS SSO profile:
|
|
|
|
```bash
|
|
headroom proxy --port 8787 \
|
|
--backend bedrock \
|
|
--region us-east-1 \
|
|
--bedrock-profile my-sso-profile
|
|
```
|
|
|
|
On startup the proxy calls `list_inference_profiles` to build a model map. Confirm it
|
|
is routing correctly by checking the LiteLLM log lines — you should see:
|
|
|
|
```
|
|
LiteLLM completion() model= converse/arn:aws:... provider = bedrock
|
|
```
|
|
|
|
## Terminal 2 — run Claude Code (normal Anthropic mode) against the proxy
|
|
|
|
```bash
|
|
export CLAUDE_CODE_USE_BEDROCK=0 # REQUIRED — prevents Claude Code bypassing the proxy
|
|
export ANTHROPIC_BASE_URL=http://127.0.0.1:8787
|
|
export ANTHROPIC_API_KEY=headroom # Claude Code needs *a* key to start; value is ignored
|
|
export ANTHROPIC_MODEL=claude-opus-4-6
|
|
export ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-6
|
|
export ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-6
|
|
export ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-haiku-4-5-20251001
|
|
|
|
claude
|
|
```
|
|
|
|
Or via `~/.claude/settings.json`:
|
|
|
|
```json
|
|
{
|
|
"env": {
|
|
"CLAUDE_CODE_USE_BEDROCK": "0",
|
|
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8787",
|
|
"ANTHROPIC_API_KEY": "headroom",
|
|
"ANTHROPIC_MODEL": "claude-opus-4-6",
|
|
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-6",
|
|
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-6",
|
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5-20251001"
|
|
}
|
|
}
|
|
```
|
|
|
|
Claude Code now talks plain Anthropic `/v1/messages` to Headroom; Headroom compresses
|
|
and forwards to Bedrock via LiteLLM, then translates the answer back.
|
|
|
|
## Application inference profiles (account-specific ARNs)
|
|
|
|
If your IAM policy only permits **application inference profiles** (account-specific
|
|
ARNs) rather than system-defined cross-region profiles, pass the ARN directly as the
|
|
model value in `ANTHROPIC_DEFAULT_*_MODEL`. The proxy detects `arn:aws:` prefixed model
|
|
IDs and routes them via `bedrock/converse/<arn>` automatically — no extra configuration
|
|
required.
|
|
|
|
## Region prefix notes
|
|
|
|
| AWS region | Cross-region inference prefix |
|
|
|---|---|
|
|
| `us-*` | `us.` |
|
|
| `eu-*` | `eu.` |
|
|
| `ap-*` (except `ap-southeast-2`) | `apac.` |
|
|
| `ap-southeast-2` (Sydney) | `au.` |
|
|
|
|
The proxy uses the correct prefix automatically when constructing fallback model IDs.
|
|
|
|
## Verify compression is happening
|
|
|
|
- Dashboard: <http://localhost:8787/dashboard> — "tokens saved" climbs as you work.
|
|
- `curl -s localhost:8787/stats` → `tokens.saved` and `request_logs[].transforms_applied`.
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Cause | Fix |
|
|
|---|---|---|
|
|
| Proxy receives no requests | Claude Code is in Bedrock mode, bypassing proxy | Set `CLAUDE_CODE_USE_BEDROCK=0` |
|
|
| `400 The provided model identifier is invalid` | Bedrock rejected the model name format | Use standard cross-region profile names (`claude-sonnet-4-6`) or a valid application inference profile ARN |
|
|
| `403 AccessDeniedException` on system-defined profiles | IAM policy only permits application profiles | Use `--bedrock-profile` with an authorized profile and pass application inference profile ARNs as model values |
|
|
| `400 … Try calling via converse route` | Old proxy version routing ARNs to invoke path | Upgrade to headroom ≥ 0.27.1 |
|
|
| Model map empty at startup | boto3 not installed or wrong AWS profile | `pip install boto3`; check `--bedrock-profile` / `AWS_PROFILE` |
|