Skip to content

feat(provider-bedrock): wire Anthropic-on-Bedrock chat via aws-sdk-bedrockruntime (D7.2.a) - #320

Merged
moonming merged 2 commits into
mainfrom
feat/bedrock-wire
May 17, 2026
Merged

feat(provider-bedrock): wire Anthropic-on-Bedrock chat via aws-sdk-bedrockruntime (D7.2.a)#320
moonming merged 2 commits into
mainfrom
feat/bedrock-wire

Conversation

@moonming

@moonming moonming commented May 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Replaces the skeleton's BridgeError::Config(\"not yet implemented\") stubs in aisix-provider-bedrock with real Bedrock dispatch for the anthropic.* publisher (Claude on Bedrock). The AWS SDK handles SigV4 signing, retries, and (in the streaming follow-up D7.2.b) the binary event-stream framing.

Scope: deliberately minimalchat() for anthropic.* only. Other Bedrock publishers (meta.*, mistral.*, amazon.*, cohere.*, ai21.*) surface a clear publisher-named "not yet implemented — D7.3+" error. chat_stream() returns "streaming not yet implemented — D7.2.b" for all publishers. This mirrors the D6 PR #319 pattern: focused, mergeable, follow-ups carry the rest.

Wire shape (Anthropic on Bedrock)

Per https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html:

  • URL: POST /model/<id>/invoke
  • Body: Anthropic Messages JSON with Bedrock-specific shape rules:
    1. anthropic_version: \"bedrock-2023-05-31\" MUST be present
    2. model MUST be absent (Bedrock dispatches via URL path)
    3. stream MUST be absent for InvokeModel
  • Auth: SigV4 — Authorization: AWS4-HMAC-SHA256 ... + x-amz-date headers signed by aws-sdk-bedrockruntime

Credentials convention

ProviderKey.secret is a JSON-encoded {access_key_id, secret_access_key, session_token?, region} blob. The bridge parses it per request (cheap — strings only); credential rotation lands as soon as the PK snapshot refreshes. The cp-api side delivers credentials decrypted via the mTLS-only etcd channel (same trust boundary as Guardrail credentials).

ProviderKey.api_base (if set) is forwarded as the SDK's endpoint_url so operators can point at a private deployment / VPC endpoint. Tests use the same path via a #[cfg(test)] endpoint_url_override seam to drive bridge.chat() end-to-end against wiremock.

Anthropic wire reuse

Same playbook as PR #319 for aisix-provider-openai::wire: promotes the needed types/fns in aisix-provider-anthropic::wire from pub(crate) to pub so the Bedrock crate can build Anthropic Messages bodies without re-implementing them. AnthropicBridge itself is unchanged. The Anthropic crate's wire module gets a doc note explaining the visibility (sibling crate reuse, workspace-internal, not external-SDK stability promise).

References

Test plan

35 unit tests, all passing:

Publisher resolution (12 tests — preserved from skeleton)

All publisher tags, cross-region prefixes (us./eu./apac./global./us-gov.), guard against treating a publisher segment as a region, catch-all to BedrockPublisher::Other for not-yet-wired publishers (DeepSeek, Writer, Stability AI, etc.).

BedrockSecret parsing (5 tests)

Full form, with session_token, empty rejected, non-JSON rejected with generic shape error, missing region rejected, error message does NOT echo raw secret bytes (M1-style leak guard pinned by bedrock_secret_error_does_not_leak_secret_content).

Pre-dispatch validation (6 tests)

Unknown publisher, non-Anthropic publisher named in error, invalid secret, empty secret, missing model_name, chat_ignores_req_model_and_uses_ctx_model_name (D6 audit HIGH-1 regression carried over).

chat_stream not-implemented error (1 test)

Returns clear D7.2.b not-implemented error.

Bridge dispatch via bridge.chat() end-to-end against wiremock (8 tests, highest-confidence pins)

  • URL path includes deployment id with : URL-encoded (path_regex tolerant)
  • Body carries anthropic_version=bedrock-2023-05-31, no model, no stream
  • SigV4 Authorization: AWS4-HMAC-SHA256 header reaches the wire along with x-amz-date
  • tool_use response blocks translate to OpenAI tool_calls shape (via reused Anthropic crate's converter)
  • 4xx upstream error body redacted to canned phrase (does NOT echo operator's account number / IAM role ARN — Audit M1 lesson applied proactively)
  • 429 mapped to canned "rate limited" message + status preserved
  • Cross-region inference profile (us.anthropic.claude-*) dispatches with the full prefixed model id in the URL
  • system role messages translated to top-level Anthropic system field

Test plan TODO

  • cargo test -p aisix-provider-bedrock → 35/35 pass
  • cargo test -p aisix-provider-anthropic → 63/63 pass (no regression from pub promotions)
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --check clean
  • CI: cargo test --workspace
  • CI: cargo clippy
  • CI: cargo fmt --check

Summary by CodeRabbit

  • New Features

    • AWS Bedrock Runtime integration for Anthropic models: credential parsing, request translation, dispatch, and error handling.
  • API Changes

    • Anthropic provider request/response types and helper utilities are now publicly accessible to other crates.
  • Documentation

    • Updated Bedrock integration docs and status, clarifying supported/unsupported areas (notably streaming).
  • Chores

    • Updated crate dependencies and test tooling for Bedrock provider.

Review Change Stack

…drockruntime (D7.2.a, #302 Phase G)

Replaces the skeleton's `BridgeError::Config("not yet implemented")`
stubs in `aisix-provider-bedrock` with real Bedrock dispatch for the
`anthropic.*` publisher (Claude on Bedrock). The AWS SDK handles
SigV4 signing, retries, and (in the streaming follow-up D7.2.b) the
binary event-stream framing.

Other Bedrock publishers (`meta.*`, `mistral.*`, `amazon.*`,
`cohere.*`, `ai21.*`) return a clear publisher-named "not yet
implemented — D7.3+" error. `chat_stream()` returns "streaming not
yet implemented — D7.2.b" for all publishers.

## Wire shape pinned (Anthropic on Bedrock)

Per the AWS docs URL cited inline:

- URL: `POST /model/<id>/invoke` (model id encoded into the path —
  the `:` in `anthropic.claude-3-5-sonnet-20241022-v2:0` becomes
  `%3A`; tests use a regex matcher to stay SDK-version-tolerant)
- Body: Anthropic Messages JSON, with three Bedrock-specific shape
  rules pinned by `chat_anthropic_body_contains_bedrock_anthropic_version_and_no_model_field`:
    1. `anthropic_version: "bedrock-2023-05-31"` MUST be present
    2. `model` MUST be absent (Bedrock dispatches via URL path)
    3. `stream` MUST be absent for InvokeModel (non-streaming)
- Auth: SigV4 — pinned by
  `chat_anthropic_uses_sigv4_authorization_header` (asserts
  `Authorization: AWS4-HMAC-SHA256 ...` + `x-amz-date` reach the
  wire)

## Credentials convention

`ProviderKey.secret` is a JSON-encoded
`{access_key_id, secret_access_key, session_token?, region}` blob.
The bridge parses it per request (cheap — strings only); credential
rotation lands as soon as the PK snapshot refreshes, no client cache
to invalidate. The cp-api side delivers credentials decrypted (mTLS-
only etcd channel; same trust boundary as Guardrail credentials).

`ProviderKey.api_base` (if set) is forwarded as the SDK's
`endpoint_url` so operators can point at a private deployment / VPC
endpoint. Tests use the same path via a `#[cfg(test)]`
`endpoint_url_override` seam to drive `bridge.chat()` end-to-end
against wiremock.

## Anthropic wire reuse

The Anthropic-on-Bedrock body shape is the Anthropic Messages API
minus the `model` field plus `anthropic_version`. To avoid
re-implementing the wire types, promotes the needed items in
`aisix-provider-anthropic::wire` from `pub(crate)` to `pub`:

- `AnthropicRequest`, `AnthropicMessage`
- `AnthropicResponse`, `AnthropicResponseBlock`, `AnthropicUsage`
- `AnthropicStreamEvent` + substructs (for D7.2.b streaming follow-up)
- `StreamState`
- `build_request`, `messages_from`, `split_system`,
  `response_into_chat_response`
- `translate_openai_tools_to_anthropic`,
  `translate_openai_tool_choice_to_anthropic`
- `DEFAULT_MAX_TOKENS`

Same pattern as PR #319 for `aisix-provider-openai::wire` — wire
types are JSON-shape contracts, public surface for sibling provider
crates (workspace-internal, not a stability promise to external SDK
consumers). `AnthropicBridge` itself is unchanged.

## Test coverage

35 unit tests, all passing:

- Publisher resolution (12 tests, preserved from skeleton): every
  publisher tag, cross-region prefixes (`us.`/`eu.`/`apac.`/`global.`/
  `us-gov.`), guard against treating a publisher segment as region,
  catch-all to `BedrockPublisher::Other` for not-yet-wired publishers
- `BedrockSecret` parsing (5 tests): full form, with session_token,
  empty rejected, non-JSON rejected with generic shape error,
  missing region rejected, **error message does NOT echo raw secret
  bytes** (M1-style leak guard)
- Pre-dispatch validation (6 tests): unknown publisher, non-Anthropic
  publisher named in error, invalid secret, empty secret, missing
  model_name, `chat_ignores_req_model_and_uses_ctx_model_name` (D6
  audit HIGH-1 regression carried over)
- `chat_stream` returns clear D7.2.b not-implemented error
- Bridge dispatch via `bridge.chat()` end-to-end against wiremock (7
  tests, the highest-confidence pins):
  - URL path includes deployment id with `:` URL-encoded
  - body carries `anthropic_version=bedrock-2023-05-31`, no `model`,
    no `stream`
  - SigV4 `Authorization: AWS4-HMAC-SHA256` header reaches the wire
    along with `x-amz-date`
  - `tool_use` response blocks translate to OpenAI `tool_calls` shape
    (via reused Anthropic crate's converter)
  - 4xx upstream error body redacted to canned phrase (does NOT echo
    operator's account number / IAM role ARN — Audit M1)
  - 429 mapped to canned "rate limited" message + status preserved
  - Cross-region inference profile (`us.anthropic.claude-*`) dispatches
    with the full prefixed model id in the URL
  - `system` role messages translated to top-level Anthropic `system`
    field (not left in `messages[]`)
Copilot AI review requested due to automatic review settings May 17, 2026 12:58
@coderabbitai

coderabbitai Bot commented May 17, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 111569c0-4407-4bea-85f0-cabcdb582e69

📥 Commits

Reviewing files that changed from the base of the PR and between d330894 and 48b11c8.

📒 Files selected for processing (2)
  • crates/aisix-provider-bedrock/Cargo.toml
  • crates/aisix-provider-bedrock/src/bridge.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/aisix-provider-bedrock/Cargo.toml

📝 Walkthrough

Walkthrough

This PR adds AWS Bedrock as a deployment target for Anthropic models: it exposes Anthropic wire types/functions, adds Bedrock SDK dependencies, implements model-id publisher resolution, builds per-request SigV4 credential handling and SDK clients, implements Anthropic-on-Bedrock chat dispatch, and adds comprehensive tests.

Changes

Bedrock Anthropic Integration

Layer / File(s) Summary
Expose Anthropic wire types and functions
crates/aisix-provider-anthropic/src/lib.rs, crates/aisix-provider-anthropic/src/wire.rs
Module and type visibility changed from crate-private to public. Exposes AnthropicRequest, AnthropicMessage, AnthropicResponse, AnthropicResponseBlock, AnthropicUsage, streaming event/shape types, DEFAULT_MAX_TOKENS, and translation helpers (split_system, build_request, tool translation, response_into_chat_response).
Bedrock dependencies, docs, and test seam
crates/aisix-provider-bedrock/Cargo.toml, crates/aisix-provider-bedrock/src/lib.rs, crates/aisix-provider-bedrock/src/bridge.rs
Adds aisix-provider-anthropic and workspace-managed AWS Bedrock/runtime dependencies, updates crate-level documentation/status, and adds a test-only endpoint override seam for wiremock.
Publisher resolution from model ID
crates/aisix-provider-bedrock/src/bridge.rs
Implements BedrockPublisher::from_model_id() with known publisher tags and conditional cross-region prefix stripping (us., eu., apac., global., us-gov.).
Credential handling and SDK client setup
crates/aisix-provider-bedrock/src/bridge.rs
Introduces BedrockSecret JSON parsing and validation, per-request AWS SDK Bedrock client construction with SigV4 credentials and optional endpoint override, model-id character validation, and mapping of SDK/service errors into canonical BridgeError with sensitive-info redaction.
Chat control flow and Anthropic dispatch
crates/aisix-provider-bedrock/src/bridge.rs
Implements Bridge::chat() to resolve upstream model id and dispatch Anthropic to chat_anthropic(); chat_anthropic() reshapes gateway chat into Anthropic-on-Bedrock request (strip model/stream, inject anthropic_version), calls invoke_model, decodes AnthropicResponse, and converts to gateway ChatResponse. chat_stream() returns not-implemented errors with Anthropic/non-Anthropic distinctions.
Bridge implementation and wiremock tests
crates/aisix-provider-bedrock/src/bridge.rs
Adds extensive tests for publisher resolution, BedrockSecret parsing and redaction, pre-dispatch validation, dispatch selection, end-to-end wiremock invoke-model assertions (URL/path shape, body pinning rules, SigV4 headers), tool-use translation, upstream error mapping without leaking account/role details, cross-region dispatch behavior, model-id validation, and system-message translation.

🎯 3 (Moderate) | ⏱️ ~20 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…5/M6)

PR #320 audit surfaced 4 HIGH + 6 MEDIUM + 3 LOW; this commit
addresses the HIGH items and the MEDIUM items that have concrete
test/code-level fixes. (M1 — anthropic-crate constructor visibility
asymmetry — and M3 — explicit SdkError variant match — deferred as
out-of-scope; they're stylistic improvements that don't gate merge.)

## HIGH (all fixed)

H1 — Retry-After header propagation. Bedrock returns `Retry-After`
  on 429 throttle responses; the previous code collapsed it to
  `None`, silently degrading the cooldown layer's multi-region /
  burst behavior. `map_service_error` now converts the smithy
  HeaderMap → http::HeaderMap so it can call the gateway-level
  `parse_retry_after` helper. Pinned by new
  `chat_maps_upstream_429_with_retry_after_and_canned_rate_limited`
  test (mock returns `Retry-After: 42`, asserts
  `BridgeError::UpstreamStatus { retry_after: Some(42s), .. }`).

H2 — `BedrockPublisher::Debug` taxonomy leak in customer error.
  The not-implemented-publisher error used `{publisher:?}` which
  formatted as `Other` / `Anthropic` etc. — internal labels that
  don't help the operator open the right follow-up tracking task.
  Replaced with the operator's actual model id + the
  publisher.name() catalog identifier. Pinned by new
  `chat_publisher_not_implemented_error_includes_model_id_and_publisher_name`
  (asserts `meta.llama3-3-70b-instruct-v1:0` and `publisher=meta`
  reach the error; `Other` and `<unspecified>` do NOT).

H3 — `SdkError::TimeoutError` reported `elapsed_ms: 0` which formats
  as "timed out after 0ms" in customer logs. Now plumbs (started,
  deadline) into `map_sdk_error` so the actual elapsed budget is
  reported. Falls back to the configured deadline if elapsed
  rounds to 0 (clock skew defense).

H4 — Tool-call test missing OpenAI-spec assertion. The previous
  `chat_anthropic_handles_tool_use_response_blocks` test asserted
  `function.name` but not the `arguments` shape. Per OpenAI's
  Chat Completions spec, `arguments` MUST be a JSON-encoded STRING
  (not a parsed object) so SDK consumers can do
  `JSON.parse(toolCall.function.arguments)`. A future refactor
  that passed the parsed object would silently break every
  OpenAI-SDK caller against an Anthropic upstream. Test now
  pins the string shape AND round-trip-parses it back to verify
  the original `{"city": "SF"}` arguments.

## MEDIUM (addressed)

M2 — `validate_model_id_chars` defense-in-depth check. The AWS
  SDK URL-encodes reserved chars but the gateway layer must
  reject upfront: the model id propagates into metrics labels,
  so an embedded `\t` / whitespace / `?` / `#` would corrupt
  dashboards. Allowed set: `[A-Za-z0-9._:/-]`. Pinned by new
  `chat_rejects_model_id_with_path_injection_chars`.

M4 — `chat_stream` distinguishes "anthropic streaming not wired"
  (D7.2.b — same publisher as chat, just streaming) from
  "publisher X not wired at all" (D7.3+). The previous code
  returned the same generic "streaming not yet implemented"
  error for both, which mis-routed operators to the wrong
  tracking task. Two new tests pin the split:
  `chat_stream_anthropic_returns_d7_2_b_specific_error` and
  `chat_stream_non_anthropic_publisher_returns_d7_3_specific_error`.

M5 — 4xx redaction test was a weak negative assertion (does NOT
  contain leaky strings). Strengthened with exact-match positive
  assertion on the canned phrase (`assert_eq!(message,
  "upstream returned 400")`). A future refactor that re-rendered
  SDK metadata into the message would pass the absence check
  but fail the exact-match.

M6 — Cross-region dispatch parity. The only e2e test covered
  `us.`; the historically-broken case (`us-gov.` with hyphen)
  and `global.` (exactly 6 chars, accidentally working under the
  old matcher) lacked dispatch-path coverage. Added
  `chat_with_us_gov_cross_region_prefix_dispatches_with_full_model_id`
  and `chat_with_global_cross_region_prefix_dispatches_with_full_model_id`.

## Verification

cargo test -p aisix-provider-bedrock → 41 passed (was 35; +6 audit
regression tests)
cargo clippy --workspace --all-targets -- -D warnings → clean
cargo fmt --check → clean

## Deferred

M1 — `AnthropicRequest` field visibility asymmetry. Workspace-internal
  surface decision; doesn't gate merge. Tracked for follow-up.
M3 — Explicit `SdkError` variant arms. `SdkError` is `non_exhaustive`;
  future SDK upgrades adding new variants would silently fall into
  the `_ =>` catch-all. Tracked for follow-up — needs a CI lint to
  catch on SDK bump, not just code-level enumeration.
L1/L3 — `Other` Debug variant + metrics-label concern. Deferred to
  the D7.3+ PRs that wire `Other` publisher dispatch (the variant
  goes away then).
@moonming

Copy link
Copy Markdown
Collaborator Author

Audit follow-up pushed (48b11c8): Independent audit surfaced 4 HIGH + 6 MEDIUM + 3 LOW findings. This commit addresses all HIGH + the MEDIUM items with concrete test/code-level fixes.

HIGH (all fixed)

  • H1Retry-After header on 429 now propagated. map_service_error converts the smithy HeaderMap → http::HeaderMap and reuses aisix_gateway::parse_retry_after. Pinned by chat_maps_upstream_429_with_retry_after_and_canned_rate_limited (mock returns Retry-After: 42, asserts retry_after: Some(42s)).
  • H2BedrockPublisher::Debug taxonomy (Other / <unspecified>) replaced with operator's actual model id + publisher.name() catalog identifier in not-implemented errors. Pinned by chat_publisher_not_implemented_error_includes_model_id_and_publisher_name.
  • H3SdkError::TimeoutError no longer reports elapsed_ms: 0. map_sdk_error now takes (started, deadline) and reports the actual elapsed budget.
  • H4 — Tool-call test now asserts arguments is a JSON-encoded STRING per OpenAI spec (not a parsed object) and round-trip-parses to verify {"city": "SF"}.

MEDIUM (addressed)

  • M2validate_model_id_chars defense-in-depth check ([A-Za-z0-9._:/-] allowed). Pinned by chat_rejects_model_id_with_path_injection_chars.
  • M4chat_stream distinguishes D7.2.b (anthropic streaming) from D7.3+ (publisher not wired). Two new tests pin the split.
  • M5 — 4xx redaction test strengthened with exact-match positive assertion (assert_eq!(message, "upstream returned 400")), not just absence-of-leak.
  • M6 — Added chat_with_us_gov_cross_region_prefix_dispatches_with_full_model_id and chat_with_global_cross_region_prefix_dispatches_with_full_model_id for dispatch-path parity with us..

Deferred (justified)

  • M1AnthropicRequest field visibility asymmetry. Workspace-internal surface decision; doesn't gate merge. Will revisit when the anthropic crate gets a public-API stabilization pass.
  • M3 — Explicit SdkError variant arms. SdkError is non_exhaustive; the _ => catch-all is intentional. Tracking a future CI lint to catch silent absorption on SDK bumps separately.
  • L1/L3Other Debug variant + metrics-label cosmetic. The variant goes away as D7.3+ wires real publisher dispatch; addressing now would be churn.

Verification

cargo test -p aisix-provider-bedrock → 41 passed (was 35; +6 audit regression tests)
cargo clippy --workspace --all-targets -- -D warnings → clean
cargo fmt --check → clean

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants