feat(bedrock): cross-region + Converse compression; bundle proxy binary in images#999
Conversation
PR governanceThis PR follows the template and is marked ready for human review. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
345955e to
476333d
Compare
Cross-region inference-profile detection, Converse-body compression, SSO credential fix, and headroom-proxy binary in published images. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cross-region inference-profile detection, Converse-body compression, SSO credential fix, and headroom-proxy binary in published images.
b2161c3 to
598c3a2
Compare
There was a problem hiding this comment.
Pull request overview
This PR hardens and extends the Rust headroom-proxy Bedrock-native path by improving model-id vendor detection (including cross-region inference profiles), broadening request-body compression to cover Converse-shaped bodies, enabling AWS SSO credential resolution, and shipping the headroom-proxy binary inside the published Docker images. It also updates Bedrock operator docs and the changelog to reflect the new capabilities.
Changes:
- Add canonical Bedrock vendor resolution (
bedrock::vendor) and use it to recognize geo-prefixed Anthropic inference profiles. - Adjust Bedrock Anthropic compression to proceed even when the body isn’t a parseable InvokeModel envelope (while gating envelope re-emit on successful parse).
- Enable
aws-configSSO support and bundle theheadroom-proxybinary intoruntimeandruntime-slimDocker images; update docs + changelog accordingly.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
docs/bedrock.md |
Adds “Running the proxy” instructions and documents cross-region inference-profile handling. |
Dockerfile |
Builds headroom-proxy in the builder stage and copies it into runtime images. |
crates/headroom-proxy/src/bedrock/vendor.rs |
New module to resolve canonical vendor from geo-prefixed model IDs + tests. |
crates/headroom-proxy/src/bedrock/mod.rs |
Exposes the new vendor module. |
crates/headroom-proxy/src/bedrock/invoke.rs |
Uses canonical vendor check and adjusts envelope-parse behavior for compression/re-emit. |
crates/headroom-proxy/src/bedrock/invoke_streaming.rs |
Uses canonical vendor check and adjusts envelope-parse behavior for streaming compression/re-emit. |
CHANGELOG.md |
Documents the new Bedrock features, Docker packaging change, and SSO fix. |
Cargo.toml |
Enables aws-config sso feature. |
Cargo.lock |
Locks new AWS SDK deps pulled in by SSO support. |
…bodies Two gaps in the native Bedrock compression path, plus an SSO build fix: 1. Cross-region inference-profile detection (addresses part of headroomlabs-ai#953). Anthropic models were detected by the bare `anthropic.` prefix only, so geo-prefixed inference-profile IDs (`eu.anthropic.`, `us.anthropic.`, `global.anthropic.`) silently skipped live-zone compression. Add `is_anthropic_model_id()` matching both the prefix and the `.anthropic.` segment, used by invoke + invoke_streaming. 2. Converse-body compression. `run_anthropic_compression` returned the body unchanged whenever it was not a parseable InvokeModel envelope, so Converse-shaped bodies were never compressed. Gate the envelope re-emit on a successful parse and fall through to generic Anthropic compression otherwise. 3. aws-config `sso` feature so SSO profiles resolve through the default credential chain for SigV4 signing. Tests: add anthropic_model_id_matches_cross_region_inference_profiles. Full headroom-proxy suite (225 lib + all integration tests) passes; rustfmt + clippy clean.
The published images already run "the proxy" and carry the `proxy` extra, but only shipped the Python `headroom` CLI — the native Rust `headroom-proxy` reverse proxy (SigV4 Bedrock + live-zone compression) was built in CI yet never included in any image or release artifact (issue headroomlabs-ai#976). Build the `headroom-proxy` binary in the builder stage and COPY it into both the debian (`runtime-slim-base`) and distroless (`runtime-slim`) runtime images at `/usr/local/bin/headroom-proxy`. Verified it is present and runs (`--help`) in both the `:code` (debian) and distroless `slim` variants. This lets operators front the Python proxy with the native Rust path from a single published image instead of building it from source.
…ed binary Keep the operator guide consistent with the code changes in this PR: - "Supported model IDs" now documents that geo-prefixed cross-region inference profiles (`eu.anthropic.`, `us.anthropic.`, `apac.anthropic.`, `global.anthropic.`) are matched as Anthropic-shape, alongside the bare `anthropic.` prefix — and that geo-prefixed non-Anthropic vendors still skip compression. - New "Running the proxy" section: the `headroom-proxy` binary now ships in the published images at `/usr/local/bin/headroom-proxy`, so operators can run the native Bedrock surface straight from a published tag (or front the Python proxy with it) instead of building from source.
Replace the per-handler `is_anthropic_model_id` (and its `.contains(".anthropic.")`
heuristic) with a shared `bedrock::vendor` module, following the design proposed in
issue headroomlabs-ai#953: strip a known cross-region inference-profile geo prefix (`eu.`, `us.`,
`apac.`, `global.`) then take the leading dot-segment as the canonical vendor.
This is more precise than a substring match (an unrelated `something.anthropic.x` id is
not mistaken for an Anthropic profile) and gives a single reusable vendor resolver for the
non-Anthropic vendor-parity follow-up in headroomlabs-ai#953. Tests move to `bedrock::vendor`; docs
updated to describe the strip-then-match rule.
Cross-region inference-profile detection, Converse-body compression, SSO credential fix, and headroom-proxy binary in published images.
…ment - Route /converse to the upstream Converse endpoint: handle_invoke now resolves the action from the inbound path via extract_invoke_action, mirroring invoke_streaming::extract_streaming_action, instead of the hard-coded "invoke". Adds unit tests for path resolution and URL build. - Dockerfile: point the cargo registry cache mount at CARGO_HOME (/usr/local/cargo/registry) so it is actually effective, and add --locked for reproducible builds. - docs/bedrock.md: mount AWS creds at /home/nonroot/.aws (the default nonroot image home) instead of /root/.aws; note the root alternative. - invoke.rs: correct the stale run_anthropic_compression comment that claimed parse-failure bodies are forwarded verbatim.
598c3a2 to
783516b
Compare
The live-zone dispatcher only recognized Anthropic content blocks
(`{"type":"text","text":…}`). Bedrock Converse blocks carry no
`type` discriminator — the variant is the key (`{"text":…}`), so
real Converse user-message text was passing through uncompressed even
after the parse-gate was relaxed.
Route a typeless block whose `text` field is a JSON string through the
same surgical text path. Anthropic blocks always carry `type`, so the
Anthropic path is byte-for-byte unchanged; non-text Converse blocks
(`{"image":…}`, `{"toolUse":…}`) stay unrecognized and no-op.
Adds unit tests asserting Converse `{text}` blocks dispatch identically
to Anthropic text blocks and that non-text blocks are not misrouted.
|
ok pushed the review fixes and rebased on latest main. ready for another look. whats changed since last time:
added tests for all of it (extract_invoke_action + converse url, the converse text-block routing, and the non-text blocks staying no-op). fmt, clippy -D warnings and cargo test --workspace all pass locally and CI's green (just the native docker e2e still finishing). |
🤖 I have created a release *beep* *boop* --- <details><summary>0.26.0</summary> ## [0.26.0](v0.25.0...v0.26.0) (2026-06-16) ### Features * add Copilot BYOK provider wrapper utilities and CLI support ([#1041](#1041)) ([e67ee2a](e67ee2a)) * add dashboard agent usage stats ([#814](#814)) ([6d3f39f](6d3f39f)) * Add support for Mistral Vibe CLI ([#935](#935)) ([0932b8b](0932b8b)) * attribute reread waste to over-compression via marker check ([#901](#901)) ([f928576](f928576)) * **bedrock:** cross-region + Converse compression; bundle proxy binary in images ([#999](#999)) ([0dc2e1c](0dc2e1c)) * **dashboard:** surface compression-vs-cache net impact in Prefix Cache panel ([#913](#913)) ([2a4d300](2a4d300)) * **evals:** adversarial-input robustness grid for compressors ([#918](#918)) ([5939004](5939004)) * **parser:** detect re-issued identical tool calls as reread waste ([#909](#909)) ([7d4ae86](7d4ae86)) * **policy:** batch deep edits through one cache-bust ([#856](#856) P3a) ([#1015](#1015)) ([c2e52fe](c2e52fe)) * **policy:** consume net-cost mutation gate in ContentRouter ([#856](#856) P2) ([#905](#905)) ([553ade4](553ade4)) * **proxy:** compress AWS Bedrock InvokeModel requests via configurable upstream ([#720](#720)) ([7edb27a](7edb27a)) ### Bug Fixes * **anthropic:** strip styled Claude model ids ([#651](#651)) ([0c5c89d](0c5c89d)) * **anyllm:** forward openai api_base/api_key to the any-llm backend ([#942](#942)) ([#954](#954)) ([a7ee8a6](a7ee8a6)) * **cache:** guard None exemplar embeddings in dynamic detector ([#950](#950)) ([1ec9320](1ec9320)) * **cache:** name the missing piece in semantic detector guard ([#1018](#1018)) ([3b0bcee](3b0bcee)) * **ci:** check out repo in PR Governance label job ([#1021](#1021)) ([4558bc2](4558bc2)) * **ci:** make PR governance advisory ([#1047](#1047)) ([74dff94](74dff94)) * **codex:** compute waste signals on the OpenAI Responses path ([#898](#898)) ([b9e2761](b9e2761)) * **codex:** poll /wham/usage for subscription limits (handshake no longer sends x-codex-* headers) ([#924](#924)) ([8c00f71](8c00f71)) * **codex:** PR health label check state ([#986](#986)) ([99c874d](99c874d)) * **codex:** retag thread providers so history menu stays whole across the proxy boundary ([#1034](#1034)) ([74ae781](74ae781)) * **codex:** write canonical hooks feature flag and migrate deprecated codex_hooks ([#743](#743)) ([dff6a19](dff6a19)) * **compression:** convert tree-sitter byte offsets to char offsets ([#892](#892)) ([b1f700f](b1f700f)) * **compression:** correct JSON array item counting and entropy gate ([#887](#887)) ([d6f0f0f](d6f0f0f)) * **compression:** keep container bodies compressible in code handler ([#890](#890)) ([16ed73b](16ed73b)) * **compression:** measure short-value threshold on payload, not token ([#889](#889)) ([65b0e8c](65b0e8c)) * **compression:** use thread-local tree-sitter parsers in code handler ([#893](#893)) ([6cdb846](6cdb846)) * **gemini:** surface functionResponse payloads to waste-signal detection ([#897](#897)) ([9b0c840](9b0c840)) * **learn:** decode directory names with spaces in Windows project paths ([#997](#997)) ([#1027](#1027)) ([2d3701b](2d3701b)) * **learn:** scan subagent and workflow transcripts ([#1045](#1045)) ([0ddd4ed](0ddd4ed)) * **openclaw:** declare headroom_retrieve tool contract ([#947](#947)) ([7c8c909](7c8c909)) * **policy:** correct warm-cache penalty in net_mutation_gain to (S + dT) ([#903](#903)) ([0632eba](0632eba)) * **proxy:** add native Bedrock converse-stream route ([#917](#917)) ([b08ec15](b08ec15)) * **proxy:** keep codex image-generation WS turns alive through the relay ([#1000](#1000)) ([7dbbb40](7dbbb40)) * **proxy:** make budget enforcement actually work ([#885](#885)) ([a14ab45](a14ab45)) * **proxy:** read RTK gain stats globally by default ([#957](#957)) ([b70fccb](b70fccb)) * route v1internal code assist requests to cloudcode-pa.googleapis… ([#821](#821)) ([e20f16b](e20f16b)) * **serena:** stop the Serena dashboard popup and make --no-serena actually disable Serena ([#1003](#1003)) ([919379a](919379a)) * support Copilot Business subscription auth ([#641](#641)) ([0b4a4bd](0b4a4bd)) * wire HEADROOM_EXCLUDE_TOOLS / HEADROOM_TOOL_PROFILES into Click proxy entrypoint ([#943](#943)) ([9b7b436](9b7b436)) * **wrap:** avoid duplicate top-level keys when injecting codex provider ([#884](#884)) ([dd22cfd](dd22cfd)) ### Code Refactoring * DRY cache logic, add thread safety, fix Bash exclusion ([#704](#704)) ([e36fccd](e36fccd)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…ry in images (headroomlabs-ai#999) ## Description The native Bedrock path (Phase D) compresses + signs Anthropic-on-Bedrock requests, but two real-world cases slipped through, and the native binary that powers it was never shipped. This PR closes those gaps as a focused set of give-backs. Aligns with the Rust migration plan (see below). ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - **Cross-region inference-profile detection** via a new `bedrock::vendor` module (`canonical_vendor()`), following the design proposed in headroomlabs-ai#953: strip a known geo prefix (`eu.`/`us.`/`apac.`/`global.`) then match the canonical vendor. Geo-prefixed Anthropic profiles (`eu.anthropic.…`) now get live-zone compression instead of being silently skipped; geo-prefixed non-Anthropic vendors stay correctly excluded. - **Converse-body compression (two parts)**: 1. `run_anthropic_compression` no longer bails to passthrough when the body lacks an InvokeModel `anthropic_version` envelope; envelope re-emit stays gated on successful parse. 2. The **live-zone dispatcher now recognizes Bedrock Converse content blocks**. Converse blocks carry no `type` discriminator (the variant is the key: `{"text": …}` vs Anthropic's `{"type":"text","text":…}`), so real Converse user-message text was still passing through uncompressed. A typeless block whose `text` is a JSON string now routes through the same surgical text path. Anthropic blocks always carry `type`, so the Anthropic path is byte-for-byte unchanged; non-text Converse blocks (`{"image":…}`, `{"toolUse":…}`) stay unrecognized and no-op. - **Correct `/converse` upstream routing**: the non-streaming handler resolved the upstream action from a hard-coded `"invoke"`, so `/converse` requests were forwarded to Bedrock's `/invoke` endpoint. It now resolves the action from the inbound path (`extract_invoke_action`), mirroring the streaming handler's `extract_streaming_action`. SigV4 signs the same URL it forwards, so the signature stays consistent. - **`aws-config` `sso` feature**: SSO profiles now resolve through the default credential chain for SigV4 — the credential chain in `docs/bedrock.md` already promised SSO; this makes the code match. - **Ship the `headroom-proxy` binary in published images** (`Dockerfile`): built in the builder stage (`--locked`, with the cargo registry cache mounted at `CARGO_HOME`) and copied into both the debian and distroless runtime images. - **Docs** (`docs/bedrock.md`): document cross-region inference profiles and a "Running the proxy" section. AWS credentials mount at `/home/nonroot/.aws` (the default nonroot image home) where the SDK looks for `~/.aws`, with a note on the root-image alternative. ## Related issues - Closes headroomlabs-ai#976 — ship the `headroom-proxy` binary in published images (this PR implements the exact fix proposed there). - Addresses the **cross-region inference-profile** half of headroomlabs-ai#953 via its proposed `canonical_vendor()` design. Non-Anthropic vendor compression parity (Nova/GLM/MiniMax/ Kimi) is the natural follow-up — `bedrock::vendor` is the shared resolver it can build on. - Extends the native Bedrock InvokeModel compression requested in headroomlabs-ai#734 (the Bedrock slice of headroomlabs-ai#510) to cross-region profiles and Converse bodies. - Partially enables headroomlabs-ai#181 (native, Python-free packaging): the native binary now ships in the images, though full Python-free distribution remains out of scope. ## Alignment with the Rust migration plan Per `docs/spec/022-rust-migration.md`, the migration is **proxy-first**: `headroom-proxy` is the deployable Rust artifact, native routes replace Python passthroughs one at a time (Stage 4 = provider expansion, Bedrock included), and the binary is meant to be "built, tested, and **released together with the Python package**." Two ways this PR advances that: - The binary-in-images change makes the codebase do what the spec already states (ship the artifact) — closing the gap that forced downstreams to build from source. - Hardening the native Bedrock route (cross-region, Converse routing + body compression) is exactly the Stage-4 provider-expansion work, keeping the native path at parity with real traffic so it can be the default rather than a passthrough. ## Testing - [x] Unit tests pass (`cargo test -p headroom-core -p headroom-proxy` — full suites, 0 failures) - [x] Linting passes (`cargo clippy -p headroom-core -p headroom-proxy --all-targets -- -D warnings`) - [x] Formatting passes (`cargo fmt -- --check`) - [x] New tests added — `bedrock::vendor` (foundation + inference-profile matching), `extract_invoke_action` + converse upstream URL, and live-zone Converse text-block routing (`block_has_string_text_field`, converse-vs-anthropic dispatch equivalence). - [x] Manual testing performed ### Test Output ```text $ cargo test -p headroom-core -p headroom-proxy # all suites: ok, 0 failed $ cargo clippy -p headroom-core -p headroom-proxy --all-targets -- -D warnings # Finished, no warnings $ cargo fmt -- --check # clean # image validation (local, proxy/code extras): $ docker build --target runtime ... # debian: /usr/local/bin/headroom-proxy, --help OK $ docker build --target runtime-slim ... # distroless: binary links + --help OK ``` ## Real Behavior Proof - Environment: native Bedrock proxy against `bedrock-runtime.eu-west-2`, SSO profile, model `eu.anthropic.claude-haiku-4-5-20251001-v1:0`. - Exact command / steps: POST a large multi-turn Converse body to `/model/eu.anthropic.claude-haiku-4-5-20251001-v1:0/converse`; separately build the `runtime` + `runtime-slim` targets and run `/usr/local/bin/headroom-proxy --help`. - Observed result: before — `bedrock_compression_skipped` (geo-prefixed id not recognized), forwarded uncompressed to the wrong `/invoke` upstream; after — geo-prefixed id recognized, `/converse` forwarded to the `/converse` upstream, live-zone dispatcher compresses the Converse user-message text, measurable token savings. Images contain a runnable `headroom-proxy` in both variants. - Not tested: non-Anthropic vendor compression parity (headroomlabs-ai#953 follow-up); Converse `toolResult` nested-text compression (follow-up — only top-level Converse text blocks compress today). ## 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 - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - An earlier revision flipped the EventStream `Accept` default (`*/*`/absent → passthrough); **dropped** — `*/*` is what most clients (incl. reqwest and the proxy's own metrics tests) send while expecting SSE, so forcing passthrough breaks the standard SSE path. - The binary build adds the native-proxy compile to the image build; happy to gate it behind a build arg if maintainers prefer it opt-in. - Addressed a Copilot review round: corrected the `/converse` upstream routing, the stale `run_anthropic_compression` comment, the Dockerfile cargo cache mount + `--locked`, and the nonroot AWS-credentials docs example.
…ry in images (headroomlabs-ai#999) ## Description The native Bedrock path (Phase D) compresses + signs Anthropic-on-Bedrock requests, but two real-world cases slipped through, and the native binary that powers it was never shipped. This PR closes those gaps as a focused set of give-backs. Aligns with the Rust migration plan (see below). ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - **Cross-region inference-profile detection** via a new `bedrock::vendor` module (`canonical_vendor()`), following the design proposed in headroomlabs-ai#953: strip a known geo prefix (`eu.`/`us.`/`apac.`/`global.`) then match the canonical vendor. Geo-prefixed Anthropic profiles (`eu.anthropic.…`) now get live-zone compression instead of being silently skipped; geo-prefixed non-Anthropic vendors stay correctly excluded. - **Converse-body compression (two parts)**: 1. `run_anthropic_compression` no longer bails to passthrough when the body lacks an InvokeModel `anthropic_version` envelope; envelope re-emit stays gated on successful parse. 2. The **live-zone dispatcher now recognizes Bedrock Converse content blocks**. Converse blocks carry no `type` discriminator (the variant is the key: `{"text": …}` vs Anthropic's `{"type":"text","text":…}`), so real Converse user-message text was still passing through uncompressed. A typeless block whose `text` is a JSON string now routes through the same surgical text path. Anthropic blocks always carry `type`, so the Anthropic path is byte-for-byte unchanged; non-text Converse blocks (`{"image":…}`, `{"toolUse":…}`) stay unrecognized and no-op. - **Correct `/converse` upstream routing**: the non-streaming handler resolved the upstream action from a hard-coded `"invoke"`, so `/converse` requests were forwarded to Bedrock's `/invoke` endpoint. It now resolves the action from the inbound path (`extract_invoke_action`), mirroring the streaming handler's `extract_streaming_action`. SigV4 signs the same URL it forwards, so the signature stays consistent. - **`aws-config` `sso` feature**: SSO profiles now resolve through the default credential chain for SigV4 — the credential chain in `docs/bedrock.md` already promised SSO; this makes the code match. - **Ship the `headroom-proxy` binary in published images** (`Dockerfile`): built in the builder stage (`--locked`, with the cargo registry cache mounted at `CARGO_HOME`) and copied into both the debian and distroless runtime images. - **Docs** (`docs/bedrock.md`): document cross-region inference profiles and a "Running the proxy" section. AWS credentials mount at `/home/nonroot/.aws` (the default nonroot image home) where the SDK looks for `~/.aws`, with a note on the root-image alternative. ## Related issues - Closes headroomlabs-ai#976 — ship the `headroom-proxy` binary in published images (this PR implements the exact fix proposed there). - Addresses the **cross-region inference-profile** half of headroomlabs-ai#953 via its proposed `canonical_vendor()` design. Non-Anthropic vendor compression parity (Nova/GLM/MiniMax/ Kimi) is the natural follow-up — `bedrock::vendor` is the shared resolver it can build on. - Extends the native Bedrock InvokeModel compression requested in headroomlabs-ai#734 (the Bedrock slice of headroomlabs-ai#510) to cross-region profiles and Converse bodies. - Partially enables headroomlabs-ai#181 (native, Python-free packaging): the native binary now ships in the images, though full Python-free distribution remains out of scope. ## Alignment with the Rust migration plan Per `docs/spec/022-rust-migration.md`, the migration is **proxy-first**: `headroom-proxy` is the deployable Rust artifact, native routes replace Python passthroughs one at a time (Stage 4 = provider expansion, Bedrock included), and the binary is meant to be "built, tested, and **released together with the Python package**." Two ways this PR advances that: - The binary-in-images change makes the codebase do what the spec already states (ship the artifact) — closing the gap that forced downstreams to build from source. - Hardening the native Bedrock route (cross-region, Converse routing + body compression) is exactly the Stage-4 provider-expansion work, keeping the native path at parity with real traffic so it can be the default rather than a passthrough. ## Testing - [x] Unit tests pass (`cargo test -p headroom-core -p headroom-proxy` — full suites, 0 failures) - [x] Linting passes (`cargo clippy -p headroom-core -p headroom-proxy --all-targets -- -D warnings`) - [x] Formatting passes (`cargo fmt -- --check`) - [x] New tests added — `bedrock::vendor` (foundation + inference-profile matching), `extract_invoke_action` + converse upstream URL, and live-zone Converse text-block routing (`block_has_string_text_field`, converse-vs-anthropic dispatch equivalence). - [x] Manual testing performed ### Test Output ```text $ cargo test -p headroom-core -p headroom-proxy # all suites: ok, 0 failed $ cargo clippy -p headroom-core -p headroom-proxy --all-targets -- -D warnings # Finished, no warnings $ cargo fmt -- --check # clean # image validation (local, proxy/code extras): $ docker build --target runtime ... # debian: /usr/local/bin/headroom-proxy, --help OK $ docker build --target runtime-slim ... # distroless: binary links + --help OK ``` ## Real Behavior Proof - Environment: native Bedrock proxy against `bedrock-runtime.eu-west-2`, SSO profile, model `eu.anthropic.claude-haiku-4-5-20251001-v1:0`. - Exact command / steps: POST a large multi-turn Converse body to `/model/eu.anthropic.claude-haiku-4-5-20251001-v1:0/converse`; separately build the `runtime` + `runtime-slim` targets and run `/usr/local/bin/headroom-proxy --help`. - Observed result: before — `bedrock_compression_skipped` (geo-prefixed id not recognized), forwarded uncompressed to the wrong `/invoke` upstream; after — geo-prefixed id recognized, `/converse` forwarded to the `/converse` upstream, live-zone dispatcher compresses the Converse user-message text, measurable token savings. Images contain a runnable `headroom-proxy` in both variants. - Not tested: non-Anthropic vendor compression parity (headroomlabs-ai#953 follow-up); Converse `toolResult` nested-text compression (follow-up — only top-level Converse text blocks compress today). ## 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 - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - An earlier revision flipped the EventStream `Accept` default (`*/*`/absent → passthrough); **dropped** — `*/*` is what most clients (incl. reqwest and the proxy's own metrics tests) send while expecting SSE, so forcing passthrough breaks the standard SSE path. - The binary build adds the native-proxy compile to the image build; happy to gate it behind a build arg if maintainers prefer it opt-in. - Addressed a Copilot review round: corrected the `/converse` upstream routing, the stale `run_anthropic_compression` comment, the Dockerfile cargo cache mount + `--locked`, and the nonroot AWS-credentials docs example.
🤖 I have created a release *beep* *boop* --- <details><summary>0.26.0</summary> ## [0.26.0](headroomlabs-ai/headroom@v0.25.0...v0.26.0) (2026-06-16) ### Features * add Copilot BYOK provider wrapper utilities and CLI support ([headroomlabs-ai#1041](headroomlabs-ai#1041)) ([e67ee2a](headroomlabs-ai@e67ee2a)) * add dashboard agent usage stats ([headroomlabs-ai#814](headroomlabs-ai#814)) ([6d3f39f](headroomlabs-ai@6d3f39f)) * Add support for Mistral Vibe CLI ([headroomlabs-ai#935](headroomlabs-ai#935)) ([0932b8b](headroomlabs-ai@0932b8b)) * attribute reread waste to over-compression via marker check ([headroomlabs-ai#901](headroomlabs-ai#901)) ([f928576](headroomlabs-ai@f928576)) * **bedrock:** cross-region + Converse compression; bundle proxy binary in images ([headroomlabs-ai#999](headroomlabs-ai#999)) ([0dc2e1c](headroomlabs-ai@0dc2e1c)) * **dashboard:** surface compression-vs-cache net impact in Prefix Cache panel ([headroomlabs-ai#913](headroomlabs-ai#913)) ([2a4d300](headroomlabs-ai@2a4d300)) * **evals:** adversarial-input robustness grid for compressors ([headroomlabs-ai#918](headroomlabs-ai#918)) ([5939004](headroomlabs-ai@5939004)) * **parser:** detect re-issued identical tool calls as reread waste ([headroomlabs-ai#909](headroomlabs-ai#909)) ([7d4ae86](headroomlabs-ai@7d4ae86)) * **policy:** batch deep edits through one cache-bust ([headroomlabs-ai#856](headroomlabs-ai#856) P3a) ([headroomlabs-ai#1015](headroomlabs-ai#1015)) ([c2e52fe](headroomlabs-ai@c2e52fe)) * **policy:** consume net-cost mutation gate in ContentRouter ([headroomlabs-ai#856](headroomlabs-ai#856) P2) ([headroomlabs-ai#905](headroomlabs-ai#905)) ([553ade4](headroomlabs-ai@553ade4)) * **proxy:** compress AWS Bedrock InvokeModel requests via configurable upstream ([headroomlabs-ai#720](headroomlabs-ai#720)) ([7edb27a](headroomlabs-ai@7edb27a)) ### Bug Fixes * **anthropic:** strip styled Claude model ids ([headroomlabs-ai#651](headroomlabs-ai#651)) ([0c5c89d](headroomlabs-ai@0c5c89d)) * **anyllm:** forward openai api_base/api_key to the any-llm backend ([headroomlabs-ai#942](headroomlabs-ai#942)) ([headroomlabs-ai#954](headroomlabs-ai#954)) ([a7ee8a6](headroomlabs-ai@a7ee8a6)) * **cache:** guard None exemplar embeddings in dynamic detector ([headroomlabs-ai#950](headroomlabs-ai#950)) ([1ec9320](headroomlabs-ai@1ec9320)) * **cache:** name the missing piece in semantic detector guard ([headroomlabs-ai#1018](headroomlabs-ai#1018)) ([3b0bcee](headroomlabs-ai@3b0bcee)) * **ci:** check out repo in PR Governance label job ([headroomlabs-ai#1021](headroomlabs-ai#1021)) ([4558bc2](headroomlabs-ai@4558bc2)) * **ci:** make PR governance advisory ([headroomlabs-ai#1047](headroomlabs-ai#1047)) ([74dff94](headroomlabs-ai@74dff94)) * **codex:** compute waste signals on the OpenAI Responses path ([headroomlabs-ai#898](headroomlabs-ai#898)) ([b9e2761](headroomlabs-ai@b9e2761)) * **codex:** poll /wham/usage for subscription limits (handshake no longer sends x-codex-* headers) ([headroomlabs-ai#924](headroomlabs-ai#924)) ([8c00f71](headroomlabs-ai@8c00f71)) * **codex:** PR health label check state ([headroomlabs-ai#986](headroomlabs-ai#986)) ([99c874d](headroomlabs-ai@99c874d)) * **codex:** retag thread providers so history menu stays whole across the proxy boundary ([headroomlabs-ai#1034](headroomlabs-ai#1034)) ([74ae781](headroomlabs-ai@74ae781)) * **codex:** write canonical hooks feature flag and migrate deprecated codex_hooks ([headroomlabs-ai#743](headroomlabs-ai#743)) ([dff6a19](headroomlabs-ai@dff6a19)) * **compression:** convert tree-sitter byte offsets to char offsets ([headroomlabs-ai#892](headroomlabs-ai#892)) ([b1f700f](headroomlabs-ai@b1f700f)) * **compression:** correct JSON array item counting and entropy gate ([headroomlabs-ai#887](headroomlabs-ai#887)) ([d6f0f0f](headroomlabs-ai@d6f0f0f)) * **compression:** keep container bodies compressible in code handler ([headroomlabs-ai#890](headroomlabs-ai#890)) ([16ed73b](headroomlabs-ai@16ed73b)) * **compression:** measure short-value threshold on payload, not token ([headroomlabs-ai#889](headroomlabs-ai#889)) ([65b0e8c](headroomlabs-ai@65b0e8c)) * **compression:** use thread-local tree-sitter parsers in code handler ([headroomlabs-ai#893](headroomlabs-ai#893)) ([6cdb846](headroomlabs-ai@6cdb846)) * **gemini:** surface functionResponse payloads to waste-signal detection ([headroomlabs-ai#897](headroomlabs-ai#897)) ([9b0c840](headroomlabs-ai@9b0c840)) * **learn:** decode directory names with spaces in Windows project paths ([headroomlabs-ai#997](headroomlabs-ai#997)) ([headroomlabs-ai#1027](headroomlabs-ai#1027)) ([2d3701b](headroomlabs-ai@2d3701b)) * **learn:** scan subagent and workflow transcripts ([headroomlabs-ai#1045](headroomlabs-ai#1045)) ([0ddd4ed](headroomlabs-ai@0ddd4ed)) * **openclaw:** declare headroom_retrieve tool contract ([headroomlabs-ai#947](headroomlabs-ai#947)) ([7c8c909](headroomlabs-ai@7c8c909)) * **policy:** correct warm-cache penalty in net_mutation_gain to (S + dT) ([headroomlabs-ai#903](headroomlabs-ai#903)) ([0632eba](headroomlabs-ai@0632eba)) * **proxy:** add native Bedrock converse-stream route ([headroomlabs-ai#917](headroomlabs-ai#917)) ([b08ec15](headroomlabs-ai@b08ec15)) * **proxy:** keep codex image-generation WS turns alive through the relay ([headroomlabs-ai#1000](headroomlabs-ai#1000)) ([7dbbb40](headroomlabs-ai@7dbbb40)) * **proxy:** make budget enforcement actually work ([headroomlabs-ai#885](headroomlabs-ai#885)) ([a14ab45](headroomlabs-ai@a14ab45)) * **proxy:** read RTK gain stats globally by default ([headroomlabs-ai#957](headroomlabs-ai#957)) ([b70fccb](headroomlabs-ai@b70fccb)) * route v1internal code assist requests to cloudcode-pa.googleapis… ([headroomlabs-ai#821](headroomlabs-ai#821)) ([e20f16b](headroomlabs-ai@e20f16b)) * **serena:** stop the Serena dashboard popup and make --no-serena actually disable Serena ([headroomlabs-ai#1003](headroomlabs-ai#1003)) ([919379a](headroomlabs-ai@919379a)) * support Copilot Business subscription auth ([headroomlabs-ai#641](headroomlabs-ai#641)) ([0b4a4bd](headroomlabs-ai@0b4a4bd)) * wire HEADROOM_EXCLUDE_TOOLS / HEADROOM_TOOL_PROFILES into Click proxy entrypoint ([headroomlabs-ai#943](headroomlabs-ai#943)) ([9b7b436](headroomlabs-ai@9b7b436)) * **wrap:** avoid duplicate top-level keys when injecting codex provider ([headroomlabs-ai#884](headroomlabs-ai#884)) ([dd22cfd](headroomlabs-ai@dd22cfd)) ### Code Refactoring * DRY cache logic, add thread safety, fix Bash exclusion ([headroomlabs-ai#704](headroomlabs-ai#704)) ([e36fccd](headroomlabs-ai@e36fccd)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Description
The native Bedrock path (Phase D) compresses + signs Anthropic-on-Bedrock requests, but
two real-world cases slipped through, and the native binary that powers it was never
shipped. This PR closes those gaps as a focused set of give-backs.
Aligns with the Rust migration plan (see below).
Type of Change
Changes Made
bedrock::vendormodule(
canonical_vendor()), following the design proposed in [FEATURE] Bedrock-native compression parity for non-Anthropic vendors (Amazon Nova, ZAI/GLM, MiniMax, Kimi) + cross-region inference profile fix #953: strip a known geo prefix(
eu./us./apac./global.) then match the canonical vendor. Geo-prefixed Anthropicprofiles (
eu.anthropic.…) now get live-zone compression instead of being silentlyskipped; geo-prefixed non-Anthropic vendors stay correctly excluded.
run_anthropic_compressionno longer bails to passthrough when the body lacks anInvokeModel
anthropic_versionenvelope; envelope re-emit stays gated on successful parse.blocks carry no
typediscriminator (the variant is the key:{"text": …}vsAnthropic's
{"type":"text","text":…}), so real Converse user-message text was stillpassing through uncompressed. A typeless block whose
textis a JSON string now routesthrough the same surgical text path. Anthropic blocks always carry
type, so theAnthropic path is byte-for-byte unchanged; non-text Converse blocks (
{"image":…},{"toolUse":…}) stay unrecognized and no-op./converseupstream routing: the non-streaming handler resolved the upstreamaction from a hard-coded
"invoke", so/converserequests were forwarded to Bedrock's/invokeendpoint. It now resolves the action from the inbound path (extract_invoke_action),mirroring the streaming handler's
extract_streaming_action. SigV4 signs the same URL itforwards, so the signature stays consistent.
aws-configssofeature: SSO profiles now resolve through the default credentialchain for SigV4 — the credential chain in
docs/bedrock.mdalready promised SSO; thismakes the code match.
headroom-proxybinary in published images (Dockerfile): built in thebuilder stage (
--locked, with the cargo registry cache mounted atCARGO_HOME) andcopied into both the debian and distroless runtime images.
docs/bedrock.md): document cross-region inference profiles and a "Running theproxy" section. AWS credentials mount at
/home/nonroot/.aws(the default nonroot imagehome) where the SDK looks for
~/.aws, with a note on the root-image alternative.Related issues
headroom-proxybinary in published images (this PR implementsthe exact fix proposed there).
canonical_vendor()design. Non-Anthropic vendor compression parity (Nova/GLM/MiniMax/Kimi) is the natural follow-up —
bedrock::vendoris the shared resolver it can build on.Feature: provider-agnostic proxy mode (Bedrock, OpenAI, Vertex) #510) to cross-region profiles and Converse bodies.
images, though full Python-free distribution remains out of scope.
Alignment with the Rust migration plan
Per
docs/spec/022-rust-migration.md, the migration is proxy-first:headroom-proxyisthe deployable Rust artifact, native routes replace Python passthroughs one at a time
(Stage 4 = provider expansion, Bedrock included), and the binary is meant to be "built,
tested, and released together with the Python package." Two ways this PR advances that:
artifact) — closing the gap that forced downstreams to build from source.
exactly the Stage-4 provider-expansion work, keeping the native path at parity with real
traffic so it can be the default rather than a passthrough.
Testing
cargo test -p headroom-core -p headroom-proxy— full suites, 0 failures)cargo clippy -p headroom-core -p headroom-proxy --all-targets -- -D warnings)cargo fmt -- --check)bedrock::vendor(foundation + inference-profile matching),extract_invoke_action+ converse upstream URL, and live-zone Converse text-block routing(
block_has_string_text_field, converse-vs-anthropic dispatch equivalence).Test Output
Real Behavior Proof
bedrock-runtime.eu-west-2, SSO profile, modeleu.anthropic.claude-haiku-4-5-20251001-v1:0./model/eu.anthropic.claude-haiku-4-5-20251001-v1:0/converse; separately build theruntime+runtime-slimtargets and run/usr/local/bin/headroom-proxy --help.bedrock_compression_skipped(geo-prefixed id not recognized),forwarded uncompressed to the wrong
/invokeupstream; after — geo-prefixed id recognized,/converseforwarded to the/converseupstream, live-zone dispatcher compresses theConverse user-message text, measurable token savings. Images contain a runnable
headroom-proxyin both variants.toolResultnested-text compression (follow-up — only top-level Converse text blockscompress today).
Review Readiness
Checklist
Additional Notes
Acceptdefault (*/*/absent → passthrough);dropped —
*/*is what most clients (incl. reqwest and the proxy's own metrics tests)send while expecting SSE, so forcing passthrough breaks the standard SSE path.
a build arg if maintainers prefer it opt-in.
/converseupstream routing, the stalerun_anthropic_compressioncomment, the Dockerfile cargo cache mount +--locked, and thenonroot AWS-credentials docs example.