feat(provider-bedrock): wire Anthropic-on-Bedrock chat via aws-sdk-bedrockruntime (D7.2.a) - #320
Conversation
…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[]`)
ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis 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. ChangesBedrock Anthropic Integration
🎯 3 (Moderate) | ⏱️ ~20 minutes Note 🎁 Summarized by CodeRabbit FreeYour 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 |
…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).
|
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)
MEDIUM (addressed)
Deferred (justified)
Verification |
Summary
Replaces the skeleton's
BridgeError::Config(\"not yet implemented\")stubs inaisix-provider-bedrockwith real Bedrock dispatch for theanthropic.*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 minimal —
chat()foranthropic.*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:
POST /model/<id>/invokeanthropic_version: \"bedrock-2023-05-31\"MUST be presentmodelMUST be absent (Bedrock dispatches via URL path)streamMUST be absent for InvokeModelAuthorization: AWS4-HMAC-SHA256 ...+x-amz-dateheaders signed byaws-sdk-bedrockruntimeCredentials convention
ProviderKey.secretis 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'sendpoint_urlso operators can point at a private deployment / VPC endpoint. Tests use the same path via a#[cfg(test)]endpoint_url_overrideseam to drivebridge.chat()end-to-end against wiremock.Anthropic wire reuse
Same playbook as PR #319 for
aisix-provider-openai::wire: promotes the needed types/fns inaisix-provider-anthropic::wirefrompub(crate)topubso the Bedrock crate can build Anthropic Messages bodies without re-implementing them.AnthropicBridgeitself is unchanged. The Anthropic crate'swiremodule 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 toBedrockPublisher::Otherfor not-yet-wired publishers (DeepSeek, Writer, Stability AI, etc.).BedrockSecretparsing (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_streamnot-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-encoded (path_regex tolerant)anthropic_version=bedrock-2023-05-31, nomodel, nostreamAuthorization: AWS4-HMAC-SHA256header reaches the wire along withx-amz-datetool_useresponse blocks translate to OpenAItool_callsshape (via reused Anthropic crate's converter)us.anthropic.claude-*) dispatches with the full prefixed model id in the URLsystemrole messages translated to top-level AnthropicsystemfieldTest plan TODO
cargo test -p aisix-provider-bedrock→ 35/35 passcargo test -p aisix-provider-anthropic→ 63/63 pass (no regression from pub promotions)cargo clippy --workspace --all-targets -- -D warningscleancargo fmt --checkcleancargo test --workspacecargo clippycargo fmt --checkSummary by CodeRabbit
New Features
API Changes
Documentation
Chores