feat(wrap): auto-detect CLAUDE_CODE_USE_BEDROCK and re-sign with SigV4#1220
feat(wrap): auto-detect CLAUDE_CODE_USE_BEDROCK and re-sign with SigV4#1220didhd wants to merge 1 commit into
Conversation
PR governanceThis PR follows the template and is marked ready for human review. |
|
Hi - thanks for this change - can we resolve some conflicts. |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
45e4db8 to
3b0bce9
Compare
|
Thanks for the review! I've rebased onto the latest Conflicts resolved (4 files)
CI failures fixed
Local verification
Note: the CI checks show |
JerrettDavis
left a comment
There was a problem hiding this comment.
Thanks for the thorough implementation and the live AWS validation notes. The signing flow itself is headed in the right direction, but I think this needs one packaging/install fix before it can land.
headroom wrap claude now automatically enables --bedrock-sign whenever CLAUDE_CODE_USE_BEDROCK=1 is present, and headroom/proxy/bedrock_signer.py imports boto3 on the first signed request. However, boto3 is only declared under the optional bedrock extra in pyproject.toml, and even [all] currently omits that extra. The new guide also tells users to run pip install headroom before headroom wrap claude.
That means a normal wrapper/proxy install can enter the new turnkey Bedrock path and then fail at runtime with BedrockSigningError: boto3 is required for Bedrock SigV4 signing. Because this feature is auto-detected rather than an explicit advanced mode, the dependency story needs to be made consistent with the behavior before merge.
Concrete fixes that would satisfy this:
- Add
boto3/botocoreto the install surface that supportswrap claude+ proxy, or includebedrockfrom the relevant extras such asproxyandall; and update the lockfile if this repo tracks it for dependency changes. - Update the Bedrock guide install command to the real PyPI package/extra, e.g.
pip install "headroom-ai[proxy,bedrock]"if Bedrock remains optional. - Add a small test or packaging assertion so
bedrockis not silently excluded from[all]again if that is the intended comprehensive install.
Once that is addressed, I’d be comfortable taking another pass on the signing/routing details.
`headroom wrap claude` special-cases Vertex (CLAUDE_CODE_USE_VERTEX) and Foundry (CLAUDE_CODE_USE_FOUNDRY) but had no Bedrock branch: with CLAUDE_CODE_USE_BEDROCK=1 it fell through to setting ANTHROPIC_BASE_URL, which Claude Code ignores in Bedrock mode — so traffic went straight to AWS and Headroom compressed nothing. Claude Code's Bedrock path reads ANTHROPIC_BEDROCK_BASE_URL and signs each InvokeModel request with SigV4 (the signature hashes the body). Compressing the body invalidates that signature, so a Vertex-style "just repoint the endpoint" branch alone would 403. This adds direct-to-AWS re-signing so the flow is turnkey, with no re-signing gateway required. - proxy: new BedrockSigner (boto3 default credential chain) re-signs the post-compression body; `--bedrock-sign` / HEADROOM_BEDROCK_SIGN enables it. - handler: forward to the regional endpoint and re-sign when signing; an explicit --bedrock-api-url gateway still wins (forwarded verbatim). - routes register when either --bedrock-api-url or --bedrock-sign is set. - wrap claude: detect CLAUDE_CODE_USE_BEDROCK, set ANTHROPIC_BEDROCK_BASE_URL (not ANTHROPIC_BASE_URL), start the proxy with --bedrock-sign, resolve the region from AWS_REGION/AWS_DEFAULT_REGION, persist+restore the right settings.json key. - packaging: boto3 is required by the auto-detected signing path, so add the bedrock extra to [all], document the real install (pip install "headroom-ai[proxy,bedrock]"), point the BedrockSigningError at the [bedrock] extra, and add tests/test_bedrock_packaging.py to keep bedrock from being silently dropped from [all]. - docs: Claude Code on Amazon Bedrock guide; README matrix note. - tests: signer unit tests, handler signing-mode tests, wrap-branch CLI tests.
3b0bce9 to
2b65e30
Compare
|
Thanks @JerrettDavis — you're right, the auto-detected path made boto3 a real runtime dependency while the packaging still treated it as fully optional. Fixed all three, and rebased onto the latest 1. Install surface now carries boto3 for the wrap/proxy + Bedrock flow
2. Guide install command fixed pip install "headroom-ai[proxy,bedrock]"(plus a note that 3. Guard so
Bonus: made the runtime failure actionable — Verification (local, with boto3 present): |
JerrettDavis
left a comment
There was a problem hiding this comment.
The latest commit resolves the packaging/install blocker I raised. The Bedrock signing path no longer depends on an undocumented runtime dependency: [bedrock] declares boto3, [all] now includes bedrock, the Bedrock guide uses pip install "headroom-ai[proxy,bedrock]", and the new packaging tests guard against dropping the extra again.
Code review passes on the signing flow as well: Bedrock mode routes Claude Code through ANTHROPIC_BEDROCK_BASE_URL, starts the proxy with signing enabled, signs the post-compression outbound bytes, keeps explicit gateway URLs taking precedence, and has focused signer/handler/wrap coverage. CI still needs to run/finish on the final head, but the previous code blocker is resolved.
Description
headroom wrap claudespecial-cases Vertex (CLAUDE_CODE_USE_VERTEX) and Foundry (CLAUDE_CODE_USE_FOUNDRY), but has no Bedrock branch. WithCLAUDE_CODE_USE_BEDROCK=1,wrap claudefalls through to settingANTHROPIC_BASE_URL— which Claude Code ignores in Bedrock mode (it readsANTHROPIC_BEDROCK_BASE_URL). The result: Claude Code talks straight to AWS Bedrock and Headroom compresses nothing, silently. This is the same failure mode the Vertex guide warns about, but with no code path to prevent it.This PR makes
CLAUDE_CODE_USE_BEDROCK=1a turnkey, compressing flow — the Bedrock analogue of the existing native Vertex path.The wrinkle vs. Vertex: Claude Code signs each Bedrock
InvokeModelrequest with AWS SigV4, and the signature covers a hash of the request body. Compressing the body invalidates that signature, so simply repointing the endpoint would make AWS reject every request withInvalidSignatureException. So this PR also adds post-compression SigV4 re-signing to the Python proxy (boto3 is already a dependency), forwarding direct to the regional Bedrock endpoint with the user's own AWS credentials — no re-signing gateway required.Closes #
Type of Change
Changes Made
headroom/proxy/bedrock_signer.py(new) —BedrockSigner: re-signs the post-compression body with SigV4 via the standard boto3 credential chain (env, profile, SSO, IMDS, ECS/EKS). Drops staleAuthorization/X-Amz-*/host headers, preservescontent-typeandanthropic-*/x-amzn-bedrock-*passthrough headers, fails loud when no credentials resolve.headroom/proxy/handlers/bedrock.py— forward to the regional AWS endpoint and re-sign when in signing mode; sign over the exact outbound bytes (after compression). An explicit--bedrock-api-urlgateway still wins and is forwarded to verbatim (unchanged behavior).headroom/proxy/models.py— newbedrock_sign: boolconfig field.headroom/providers/proxy_routes.py— register the Bedrock routes when eitherbedrock_api_urlorbedrock_signis set.headroom/cli/proxy.py,headroom/proxy/server.py—--bedrock-signflag +HEADROOM_BEDROCK_SIGNenv, on both the click CLI and the argparse/native entry points.headroom/cli/wrap.py— detectCLAUDE_CODE_USE_BEDROCK, setANTHROPIC_BEDROCK_BASE_URL(notANTHROPIC_BASE_URL), start the proxy with--bedrock-sign, resolve region from--region→AWS_REGION→AWS_DEFAULT_REGION, and persist/restore the correctsettings.jsonkey for daemon-spawned workers ([BUG] headroom wrap claude: daemon child sessions don't inherit ANTHROPIC_BASE_URL, bypassing proxy after first conversation #951 parity).unwrap claudealso clears the Bedrock key.claude-code-bedrock.mdxguide (mirrors the Vertex guide); registered inmeta.json; README agent-matrix note.wrap claudebranch CLI tests.Testing
pytest)ruff check .)mypy headroom) — not run (see Not tested)Test Output
Real Behavior Proof
Environment: macOS (darwin 25.5.0), Python 3.11.11, headroom
_corebuilt viamaturin develop --release, a real AWS account withbedrock:InvokeModelinus-west-2, botocore SigV4, on branchfeat/wrap-claude-bedrock.Exact command / steps: Start the proxy in signing mode, then route the AWS CLI through it via
AWS_ENDPOINT_URL_BEDROCK_RUNTIMEso the CLI signs to the proxy, the proxy re-signs the post-compression body, and forwards direct to real AWS — which validates the signature. The exact commands:Observed result: 3/3 live invokes accepted by AWS (no
InvalidSignatureException), returningBEDROCK_SIGN_OK/LARGE_OK/COMPRESS_OKwithstop_reason: end_turn, including a 116KB body and a multi-block tool_use/tool_result conversation;GET /metricsreportedheadroom_requests_by_provider{provider="bedrock"} 3. This proves the re-signed signature is valid against AWS's real validator. Unit and integration coverage is green too: signer tests (the signature tracks the body, stale signing headers are dropped, passthrough headers are signed, missing credentials raiseBedrockSigningError); handler tests on a mocked-compressing pipeline (the compressed body is forwarded tohttps://bedrock-runtime.<region>.amazonaws.com/...carrying anAWS4-HMAC-SHA256Authorization, and--bedrock-api-urltakes precedence with no re-sign); CLI tests (CLAUDE_CODE_USE_BEDROCK=1setsANTHROPIC_BEDROCK_BASE_URL, notANTHROPIC_BASE_URL); and the full CLI + proxy suites at 477 passed, 3 skipped — no regressions.Not tested: Live compression and re-sign in the same request — the synthetic payloads used in the live run landed in the protected live-zone, so the live invokes re-signed the body unchanged; the compress-then-sign path is instead covered by a handler unit test with a mocked compressing pipeline, and this PR does not change compression logic.
mypywas not run in this environment. Live streaming (invoke-with-response-stream) re-sign is covered structurally via the same_sign_if_neededcall but not against a live stream.Review Readiness
Checklist