feat(bedrock): wire Converse API for all publishers + Anthropic streaming (#302 Phase G Step 3 / D7.2.b/D7.3/D7.4) - #389

Merged
moonming merged 2 commits into
mainfrom
feat/bedrock-converse
May 25, 2026
Merged

feat(bedrock): wire Converse API for all publishers + Anthropic streaming (#302 Phase G Step 3 / D7.2.b/D7.3/D7.4)#389
moonming merged 2 commits into
mainfrom
feat/bedrock-converse

Conversation

@moonming

@moonmingmoonming commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Replaces per-publisher `/invoke` dispatch with the unified Converse API path for everything except Anthropic non-stream (which keeps its existing `/invoke` path for backward compat with operator deployments + e2e test fixtures pinned in #320). All other dispatch — Meta / Mistral / Cohere / Amazon Titan / Amazon Nova / AI21 — and all `chat_stream()` requests (including Anthropic) — now flow through the SDK's `.converse()` / `.converse_stream()`.

Closes D7.2.b (Anthropic streaming), D7.3 (Meta dispatch), and D7.4 (Mistral / Amazon Titan/Nova / Cohere / AI21 dispatch) per #302 Phase G's audit-corrected roadmap.

Why Converse vs. extending per-publisher `/invoke`

AWS introduced the Converse API exactly to solve the per-publisher body-shaping problem: one JSON envelope (`messages[].content[].text` + `system[]` + `inferenceConfig`), one URL pattern, one response shape — works for every Bedrock publisher. The legacy `/invoke` path requires a per-publisher body translator on the gateway side (Anthropic Messages, Meta Llama prompt template, Cohere command shape, etc.) — that multiplies maintenance burden as AWS adds publishers.

Eventstream framing

`/converse-stream` emits `application/vnd.amazon.eventstream` binary frames. The aws-sdk-bedrockruntime SDK owns the frame decoder — we don't need our own (compare with the mock-bedrock fixture in #480 which hand-rolled the encoder for the test mock). The bridge layer just walks the typed `ConverseStreamOutput` event sequence and maps each event to zero, one, or two `ChatChunk`s:

EventEmits
`MessageStart`role chunk (first emission only)
`ContentBlockDelta` (text)content chunk
`MessageStop`finish_reason chunk
`Metadata`usage chunk
`ContentBlockStart` / `ContentBlockStop` / tool-use deltasno chunk

Dispatch shape

```rust
// chat() — Anthropic legacy /invoke for backward compat; everyone else Converse
match publisher {
BedrockPublisher::Anthropic => self.chat_anthropic(req, ctx, upstream_id).await,
_ => self.chat_converse(req, ctx, upstream_id).await,
}

// chat_stream() — all publishers through Converse (legacy /invoke had no stream variant)
self.chat_converse_stream(req, ctx, upstream_id).await
```

Test changes

Deleted (5 tests, obsolete after Converse wiring):

  • chat_rejects_non_anthropic_publishers_with_publisher_named
  • chat_stream_returns_clear_not_implemented_error
  • chat_stream_anthropic_returns_d7_2_b_specific_error
  • chat_stream_non_anthropic_publisher_returns_d7_3_specific_error
  • chat_publisher_not_implemented_error_includes_model_id_and_publisher_name

Updated (1 test):

  • chat_ignores_req_model_and_uses_ctx_model_name — re-anchored to publisher-unknown error path (only publisher-resolution failures still produce Config errors before dispatch)

Added (5 tests):

  • `chat_meta_publisher_dispatches_via_converse_url` — `/model/meta.*/converse` regex + response decode
  • `chat_amazon_nova_publisher_dispatches_via_converse_url` — same contract, different publisher prefix (regression guard against per-publisher hard-coding)
  • `chat_stream_for_anthropic_dispatches_via_converse_stream_url` — `/converse-stream` pattern for Anthropic streaming
  • `chat_stream_for_meta_dispatches_via_converse_stream_url` — same for non-Anthropic streaming
  • `chat_converse_request_body_uses_text_content_block_shape` — pins outbound body: system → top-level array, user → typed text blocks (NOT Anthropic-style flat string), no top-level `model`

Test plan

  • `cargo test -p aisix-provider-bedrock` → 41/41 PASS
  • `cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings` clean
  • `cargo fmt --all` applied
  • CI
  • Independent audit per CLAUDE.md §8

References (CLAUDE.md §7)

Out of scope (deferred follow-ups)

  • D7.2.a Anthropic /invoke → Converse migration. Keeping legacy path until cp-api / dashboard test fixtures are updated to assert the Converse outbound body shape instead of the Anthropic Messages shape. Customer-visible ChatResponse is identical on both paths.
  • Tool-use / image / document `ContentBlock` variants. Chat scenarios only carry `Text` blocks today; the `Role::Tool` arm in `build_converse_inputs` is a deliberate no-op pending a structured tool-use surface on `ChatFormat`.

Unblocks

AC.10 in api7/AISIX-Cloud#302: "Bedrock 上 Claude / Llama 都能 stream". Both halves now done — Claude streaming via Converse, Llama via Converse (chat + stream). Live e2e against mock-bedrock (#480) is the next sub-step, separate PR.

Summary by CodeRabbit

  • New Features

    • Unified routing: non-Anthropic providers now use Bedrock's Converse endpoints for both chat and streaming; Anthropic non-streaming retains legacy path.
  • Improvements

    • Stronger model-id validation, improved error handling including retry propagation and sensitive-info redaction.
  • Documentation

    • Status/docs updated to reflect Phase G routing and provider behavior.
  • Tests

    • Tests updated to remove old error expectations and verify Converse routing and request shape.

Review Change Stack

…ming (#302 Phase G Step 3 / D7.2.b / D7.3 / D7.4)
Replaces per-publisher /invoke dispatch with the unified Converse
API path for everything except Anthropic non-stream (which keeps
its existing /invoke path for backward compat with operator
deployments + the e2e test fixtures pinned in #320). All other
dispatch — Meta / Mistral / Cohere / Amazon Titan / Amazon Nova /
AI21 — and ALL chat_stream() requests — including Anthropic —
now flow through the SDK's `.converse()` / `.converse_stream()`.
This closes D7.2.b (Anthropic streaming), D7.3 (Meta dispatch),
and D7.4 (Mistral / Amazon / Cohere / AI21 dispatch) per #302
Phase G's audit-corrected roadmap.
## Why Converse (vs. extending the per-publisher /invoke path)
AWS introduced the Converse API exactly to solve the per-publisher
body-shaping problem: one JSON envelope (`messages[].content[].text`
+ `system[]` + `inferenceConfig`), one URL pattern
(`/model/<id>/converse[-stream]`), one response shape, works for
every Bedrock publisher. The legacy /invoke path requires a
per-publisher body translator on the gateway side (Anthropic's
Messages, Meta's Llama prompt template, Cohere's command shape,
etc.) — multiplying maintenance burden as AWS adds publishers.
## Eventstream framing
`/converse-stream` emits `application/vnd.amazon.eventstream`
binary frames. The aws-sdk-bedrockruntime SDK owns the frame
decoder — we don't need our own (compare with the mock-bedrock
fixture I wrote in api7/AISIX-Cloud#480 which hand-rolled the
encoder for the test mock). The bridge layer just walks the
typed `ConverseStreamOutput` event sequence and maps each event
to zero, one, or two `ChatChunk`s:
- `MessageStart` → role chunk (first emission only)
- `ContentBlockDelta` (text) → content chunk
- `MessageStop` → finish_reason chunk
- `Metadata` → usage chunk
- `ContentBlockStart` / `ContentBlockStop` / tool-use deltas → no chunk
## Error classification
The new `map_aws_sdk_error_generic` helper preserves the existing
4xx-vs-5xx classification: ServiceError → `UpstreamStatus` carrying
the HTTP code; TimeoutError or deadline-elapsed → `Timeout`;
everything else → `Transport`. Mirrors the pattern in the legacy
`map_sdk_error` + the audit-corrected Vertex/Azure token-mint
classifications (#387, #388).
## Test changes
Tests deleted (obsolete after Converse wiring):
- chat_rejects_non_anthropic_publishers_with_publisher_named
- chat_stream_returns_clear_not_implemented_error
- chat_stream_anthropic_returns_d7_2_b_specific_error
- chat_stream_non_anthropic_publisher_returns_d7_3_specific_error
- chat_publisher_not_implemented_error_includes_model_id_and_publisher_name
Tests updated:
- chat_ignores_req_model_and_uses_ctx_model_name — re-anchored to
the publisher-unknown error path (only publisher-resolution
failures still produce Config errors before dispatch)
Tests added (5 new for the Converse path):
- chat_meta_publisher_dispatches_via_converse_url —
`/model/meta.*/converse` path regex + Bedrock-shape response decode
- chat_amazon_nova_publisher_dispatches_via_converse_url —
same dispatch contract, different publisher prefix (regression
guard against a future change that hard-codes per-publisher
routing inside chat_converse)
- chat_stream_for_anthropic_dispatches_via_converse_stream_url —
`/converse-stream` URL pattern pin for Anthropic streaming
(legacy /invoke had no stream variant)
- chat_stream_for_meta_dispatches_via_converse_stream_url —
same pattern for non-Anthropic streaming
- chat_converse_request_body_uses_text_content_block_shape —
pins outbound body shape: system messages lifted to top-level
`system[]`, user messages emit typed `content: [{text: "..."}]`
blocks (NOT Anthropic-style flat string), no top-level `model`
field
`cargo test -p aisix-provider-bedrock` → 41/41 PASS (was 36 with
5 deletions + 1 update + 5 additions).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings`
clean. `cargo fmt --all` applied.
## References (CLAUDE.md §7)
- Converse API spec:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html
- Converse stream events:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html
- aws-sdk-bedrockruntime 1.130.0 operation modules:
https://docs.rs/aws-sdk-bedrockruntime/1.130.0/aws_sdk_bedrockruntime/operation/
- mock-bedrock fixture (api7/AISIX-Cloud#480) uses the same
`vnd.amazon.eventstream` frame format for its encoder; the bridge
here consumes the SDK-decoded events rather than re-implementing
the decoder
## Out of scope (deferred follow-ups)
- D7.2.a Anthropic /invoke → Converse migration. Keeping legacy
path until cp-api / dashboard test fixtures are updated to assert
the Converse outbound body shape instead of the Anthropic
Messages shape. The customer-visible ChatResponse is identical
on both paths.
- Tool-use / image / document `ContentBlock` variants — chat scenarios
only carry `Text` blocks today; the `Role::Tool` arm in
`build_converse_inputs` is a deliberate no-op pending a structured
tool-use surface on `ChatFormat`.
## Unblocks
AC.10 in api7/AISIX-Cloud#302: "Bedrock 上 Claude / Llama 都能 stream".
Both halves are now done — Claude streaming via Converse, Llama
via Converse (chat + stream). Live e2e against mock-bedrock
(#480) is the next sub-step, separate PR.
CopilotAI review requested due to automatic review settings May 25, 2026 00:07
@coderabbitai

coderabbitaiBot commented May 25, 2026

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

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 0a0e3bea-4faf-4241-be55-b99b16ae84ca

📥 Commits

Reviewing files that changed from the base of the PR and between f082564 and b97829c.

📒 Files selected for processing (1)
  • crates/aisix-provider-bedrock/src/bridge.rs

📝 Walkthrough

Walkthrough

This PR completes Bedrock Phase G by unifying chat and streaming dispatch through the Bedrock Converse API for all non-Anthropic publishers, while preserving Anthropic non-streaming on the legacy invoke path. New translation, streaming, and error-mapping helpers support the Converse integration, with test coverage validating the correct routing and request body shapes.

Changes

Bedrock Phase G Converse Unification

Layer / File(s)Summary
Phase G Documentation and Dependencies
crates/aisix-provider-bedrock/src/lib.rs, crates/aisix-provider-bedrock/Cargo.toml
Crate documentation updates mark Phase G completion with checked items for unified Converse dispatch and legacy Anthropic invoke path. aisix-provider-anthropic dependency is reintroduced in Cargo.toml alongside routing comments.
Chat Dispatch Routing and Setup
crates/aisix-provider-bedrock/src/bridge.rs
Updated imports include Converse SDK types. chat() method routes Anthropic to chat_anthropic and all others to chat_converse; chat_stream() always uses unified chat_converse_stream. Doc comments clarify legacy /invoke path usage and a small helper is allowed dead code.
Non-streaming & Streaming Converse Implementation
crates/aisix-provider-bedrock/src/bridge.rs
Adds build_client_from_ctx, implements chat_converse and chat_converse_stream, translates request/response shapes (build_converse_inputs, converse_output_into_chat_response, map_stop_reason, emit_converse_chunk), and centralizes AWS SDK error mapping with Retry-After preservation and deadline-aware classification.
Test Updates for Phase G Converse Routing
crates/aisix-provider-bedrock/src/bridge.rs
Removed tests asserting old "not yet implemented" publisher/streaming errors. Added Phase G tests verify /converse and /converse-stream URL routing and Converse request body shape: typed content-block arrays, top-level system field, and absent top-level model field; updated publisher-resolution regression tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 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.

CopilotAI 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.

Pull request overview

This PR switches aisix-provider-bedrock from per-publisher /invoke dispatch to the unified Bedrock Converse API for all non-Anthropic chat() calls and for allchat_stream() calls (including Anthropic), while keeping Anthropic non-streaming on the legacy /invoke path for backwards compatibility.

Changes:

  • Route non-Anthropic chat() requests via POST /model/<id>/converse and implement response translation to ChatResponse.
  • Implement chat_stream() for all publishers via POST /model/<id>/converse-stream, mapping typed SDK events into ChatChunks.
  • Update unit tests and add wiremock-based dispatch/body-shape guards; add async-stream dependency.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.

FileDescription
crates/aisix-provider-bedrock/src/lib.rsUpdates crate-level docs to reflect Converse-based dispatch and streaming support.
crates/aisix-provider-bedrock/src/bridge.rsImplements Converse + ConverseStream dispatch paths, request/response translation, and associated tests.
crates/aisix-provider-bedrock/Cargo.tomlAdds async-stream and updates comments to match the new dispatch split.
Cargo.lockRecords the new async-stream dependency in the lockfile.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +785 to +792
// Tool-result messages are part of Anthropic's tool-use
// protocol; Bedrock Converse supports them via
// `ContentBlock::ToolResult` but the gateway's ChatMessage
// surface doesn't carry the structured tool_use_id +
// content shape needed to round-trip cleanly. Skip
// silently for now; a tool-use-aware follow-up PR can
// wire this when the upstream ChatFormat extends to
// carry the structured payload.
Comment on lines +936 to +945
/// Map the SDK's `ConverseError` SdkError variant to BridgeError.
/// Same classification rules as `map_sdk_error` (UpstreamStatus for
/// retryable upstream failures, Timeout on dispatch timeout).
fn map_converse_sdk_error(
e: SdkError<ConverseError, aws_smithy_runtime_api::http::Response>,
started: Instant,
deadline: Option<Duration>,
) -> BridgeError {
map_aws_sdk_error_generic(e.to_string(), &e, started, deadline)
}
Comment on lines +979 to +988
match err {
SdkError::ServiceError(svc) => {
let status = svc.raw().status().as_u16();
BridgeError::upstream_status(status, msg)
}
SdkError::TimeoutError(_) => BridgeError::Timeout {
elapsed_ms: started.elapsed().as_millis() as u64,
},
_ => BridgeError::Transport(msg),
}
// unset by default so the upstream model's own defaults apply.
// A follow-up override-pipeline PR can wire temperature /
// max_tokens from RequestOverrides.
let _ = InferenceConfiguration::builder();
Comment on lines +802 to +820
fn converse_output_into_chat_response(
resp: aws_sdk_bedrockruntime::operation::converse::ConverseOutput,
upstream_id: &str,
) -> ChatResponse {
let (text, finish) = match resp.output() {
Some(aws_sdk_bedrockruntime::types::ConverseOutput::Message(msg)) => {
let text: String = msg
.content()
.iter()
.filter_map(|cb| match cb {
ContentBlock::Text(t) => Some(t.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("");
(text, map_stop_reason(resp.stop_reason()))
}
_ => (String::new(), FinishReason::Stop),
};
…audit HIGH/MEDIUM on #389)
Independent audit-aigw-389-bedrock-converse flagged 2 HIGH + 2
MEDIUM bugs in the original Converse wiring. All fixed in this
commit; 45/45 tests pass.
## HIGH-1 + HIGH-2 — `map_aws_sdk_error_generic` silently
regressed the legacy /invoke path's error envelope hardening
The original helper collapsed all ServiceError responses to
`BridgeError::upstream_status(status, msg)`, a convenience
constructor that sets `wire: Unknown` + `parsed: None` +
`retry_after: None`. The legacy `chat_anthropic` /invoke path
uses `map_service_error` which builds a fully-shaped
`BridgeError::UpstreamStatus` with:
- wire: Bedrock — required by error_translate to render Bedrock-
shape errors back to OpenAI/Anthropic-shape clients
- retry_after: parsed from upstream Retry-After header — required
by the cooldown layer to honour AWS throttle hints
- parsed.kind: extracted via .meta().code() — distinguishes a
ThrottlingException 429 from a different throttle source
Without this fix every non-Anthropic publisher + all streaming
silently shipped degraded errors vs. the legacy path — broke the
PR #323 audit (MEDIUM-2) hardening it had just restored.
Fix: introduce `bedrock_service_error_to_upstream_status` (generic
over `E: ProvideErrorMetadata`) that mirrors `map_service_error`'s
field-by-field construction. Both Converse + ConverseStream errors
now flow through it.
## MEDIUM-1 — `e.to_string()` produced opaque "service error"
strings as the customer-visible message
Same root cause; resolved by the HIGH fix above. The customer-
visible `message` now uses the legacy path's canned status-keyed
phrase ("upstream rate limited", "upstream authentication failed",
etc.) — preserving the operator-ARN redaction the legacy path
also enforces.
## MEDIUM-2 — `chat_converse` silently dropped ChatFormat's
temperature/max_tokens/top_p
The original `chat_converse` constructed an
`InferenceConfiguration::builder()` and immediately discarded it
with `let _ =`. Every non-Anthropic Bedrock customer using these
knobs would see them vanish — real behavioural regression vs. the
legacy chat_anthropic path which forwards them via build_request →
AnthropicRequest.
Fix: new `build_inference_config(req)` helper extracts
temperature / max_tokens / top_p when set, returns None when all
absent (so we don't emit an empty `inferenceConfig: {}` which 400s
on some Bedrock publishers). Wired into both chat_converse and
chat_converse_stream.
## Tests added (5 new for the audit fixes)
- chat_converse_maps_upstream_429_with_retry_after_and_bedrock_wire
pins the HIGH-1+2 fix: 429 + Retry-After: 42 → UpstreamStatus
with status=429, message="upstream rate limited",
wire=Bedrock, retry_after=Some(42s), parsed.kind="ThrottlingException"
- chat_converse_maps_upstream_4xx_to_canned_message_with_bedrock_wire
pins HIGH-1 for the non-throttle path: 403 with leaky ARN body
→ canned "upstream authentication failed" (NO ARN leak),
wire=Bedrock, parsed.kind="AccessDeniedException"
- chat_converse_wires_temperature_max_tokens_top_p_into_inference_config
pins MEDIUM-2: ChatFormat knobs reach Bedrock's body as
camelCase {temperature, maxTokens, topP}
- chat_converse_omits_inference_config_when_no_knobs_set
companion: empty knobs → no inferenceConfig field on the wire
`cargo test -p aisix-provider-bedrock` → 45/45 PASS (was 41; +4
new audit-regression tests).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings`
clean.
## LOWs (non-blocking, deferred per CLAUDE.md §8)
- LOW-1: Role::Tool messages silently dropped on Converse path —
deliberate no-op pending a structured tool-use surface on
ChatFormat. Documented in `build_converse_inputs`.
- LOW-2: Per-publisher dispatch test matrix coverage (Mistral /
Cohere / AmazonTitan / AI21 not individually pinned). The match
arm in chat() is structurally uniform (`_ => chat_converse`) so
routing is correct by construction; the two existing
Meta + Amazon Nova tests already cover the non-Anthropic
dispatch contract.
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit response — all HIGH/MEDIUM addressed in commit `b97829c`

Independent audit-aigw-389-bedrock-converse returned 2 HIGH + 2 MEDIUM + 2 LOW. All HIGH/MEDIUM fixed in this commit; LOWs explicitly deferred per CLAUDE.md §8.

HIGH-1 + HIGH-2 ✅ fixed

The original `map_aws_sdk_error_generic` collapsed all ServiceError responses to `BridgeError::upstream_status(status, msg)` — a convenience constructor that sets `wire: Unknown`, `parsed: None`, `retry_after: None`. The legacy `/invoke` path's `map_service_error` (hardened by PR #323's MEDIUM-2 audit) builds full UpstreamStatus with `wire: Bedrock`, parsed AWS error code, and parsed Retry-After. The Converse path silently regressed this hardening for every non-Anthropic publisher + all streaming.

Fix: new `bedrock_service_error_to_upstream_status` helper (generic over `E: ProvideErrorMetadata`) mirrors `map_service_error` field-by-field. Both Converse + ConverseStream errors flow through it.

MEDIUM-1 ✅ fixed

Same root cause as HIGH-1; resolved by the same change. Customer-visible messages now use the canned status-keyed phrases ("upstream rate limited", "upstream authentication failed") — preserving the operator-ARN redaction the legacy path enforces.

MEDIUM-2 ✅ fixed

`chat_converse` silently dropped `ChatFormat.temperature` / `.max_tokens` / `.top_p` — real behavioural regression for non-Anthropic Bedrock customers. New `build_inference_config` helper extracts the knobs when set (returns None when all absent to avoid emitting empty `inferenceConfig: {}` which 400s on some publishers). Wired into both chat_converse and chat_converse_stream.

Test coverage added (4 new tests pinning the audit fixes)

  • `chat_converse_maps_upstream_429_with_retry_after_and_bedrock_wire` — pins HIGH-1+2: 429 + Retry-After: 42 → UpstreamStatus with status=429, message="upstream rate limited", wire=Bedrock, retry_after=Some(42s), parsed.kind="ThrottlingException"
  • `chat_converse_maps_upstream_4xx_to_canned_message_with_bedrock_wire` — pins HIGH-1 for non-throttle: 403 with leaky ARN body → canned "upstream authentication failed" (NO ARN leak), wire=Bedrock, parsed.kind="AccessDeniedException"
  • `chat_converse_wires_temperature_max_tokens_top_p_into_inference_config` — pins MEDIUM-2: ChatFormat knobs → camelCase `{temperature, maxTokens, topP}` on the wire
  • `chat_converse_omits_inference_config_when_no_knobs_set` — companion: empty knobs → no inferenceConfig field

LOWs (deferred)

  • LOW-1 (Role::Tool silently dropped): deliberate no-op pending structured tool-use surface on ChatFormat; documented in `build_converse_inputs`.
  • LOW-2 (per-publisher dispatch matrix coverage): match arm in chat() is `_ => chat_converse` so routing is correct by construction; existing Meta + Amazon Nova tests already cover the non-Anthropic dispatch contract.

Result

`cargo test -p aisix-provider-bedrock` → 45/45 PASS (was 41; +4 new audit-regression tests).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings` clean.

All HIGH/MEDIUM findings addressed. Awaiting fresh CI green.

@moonming
moonming merged commit 9ed3fb9 into mainMay 25, 2026
8 checks passed
@moonming
moonming deleted the feat/bedrock-converse branch May 25, 2026 00:25
Sign up for freeto 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

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(bedrock): wire Converse API for all publishers + Anthropic streaming (#302 Phase G Step 3 / D7.2.b/D7.3/D7.4) - #389

Merged
moonming merged 2 commits into
mainfrom
feat/bedrock-converse
May 25, 2026
Merged

feat(bedrock): wire Converse API for all publishers + Anthropic streaming (#302 Phase G Step 3 / D7.2.b/D7.3/D7.4)#389
moonming merged 2 commits into
mainfrom
feat/bedrock-converse

Conversation

@moonming

@moonmingmoonming commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Replaces per-publisher `/invoke` dispatch with the unified Converse API path for everything except Anthropic non-stream (which keeps its existing `/invoke` path for backward compat with operator deployments + e2e test fixtures pinned in #320). All other dispatch — Meta / Mistral / Cohere / Amazon Titan / Amazon Nova / AI21 — and all `chat_stream()` requests (including Anthropic) — now flow through the SDK's `.converse()` / `.converse_stream()`.

Closes D7.2.b (Anthropic streaming), D7.3 (Meta dispatch), and D7.4 (Mistral / Amazon Titan/Nova / Cohere / AI21 dispatch) per #302 Phase G's audit-corrected roadmap.

Why Converse vs. extending per-publisher `/invoke`

AWS introduced the Converse API exactly to solve the per-publisher body-shaping problem: one JSON envelope (`messages[].content[].text` + `system[]` + `inferenceConfig`), one URL pattern, one response shape — works for every Bedrock publisher. The legacy `/invoke` path requires a per-publisher body translator on the gateway side (Anthropic Messages, Meta Llama prompt template, Cohere command shape, etc.) — that multiplies maintenance burden as AWS adds publishers.

Eventstream framing

`/converse-stream` emits `application/vnd.amazon.eventstream` binary frames. The aws-sdk-bedrockruntime SDK owns the frame decoder — we don't need our own (compare with the mock-bedrock fixture in #480 which hand-rolled the encoder for the test mock). The bridge layer just walks the typed `ConverseStreamOutput` event sequence and maps each event to zero, one, or two `ChatChunk`s:

EventEmits
`MessageStart`role chunk (first emission only)
`ContentBlockDelta` (text)content chunk
`MessageStop`finish_reason chunk
`Metadata`usage chunk
`ContentBlockStart` / `ContentBlockStop` / tool-use deltasno chunk

Dispatch shape

```rust
// chat() — Anthropic legacy /invoke for backward compat; everyone else Converse
match publisher {
BedrockPublisher::Anthropic => self.chat_anthropic(req, ctx, upstream_id).await,
_ => self.chat_converse(req, ctx, upstream_id).await,
}

// chat_stream() — all publishers through Converse (legacy /invoke had no stream variant)
self.chat_converse_stream(req, ctx, upstream_id).await
```

Test changes

Deleted (5 tests, obsolete after Converse wiring):

  • chat_rejects_non_anthropic_publishers_with_publisher_named
  • chat_stream_returns_clear_not_implemented_error
  • chat_stream_anthropic_returns_d7_2_b_specific_error
  • chat_stream_non_anthropic_publisher_returns_d7_3_specific_error
  • chat_publisher_not_implemented_error_includes_model_id_and_publisher_name

Updated (1 test):

  • chat_ignores_req_model_and_uses_ctx_model_name — re-anchored to publisher-unknown error path (only publisher-resolution failures still produce Config errors before dispatch)

Added (5 tests):

  • `chat_meta_publisher_dispatches_via_converse_url` — `/model/meta.*/converse` regex + response decode
  • `chat_amazon_nova_publisher_dispatches_via_converse_url` — same contract, different publisher prefix (regression guard against per-publisher hard-coding)
  • `chat_stream_for_anthropic_dispatches_via_converse_stream_url` — `/converse-stream` pattern for Anthropic streaming
  • `chat_stream_for_meta_dispatches_via_converse_stream_url` — same for non-Anthropic streaming
  • `chat_converse_request_body_uses_text_content_block_shape` — pins outbound body: system → top-level array, user → typed text blocks (NOT Anthropic-style flat string), no top-level `model`

Test plan

  • `cargo test -p aisix-provider-bedrock` → 41/41 PASS
  • `cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings` clean
  • `cargo fmt --all` applied
  • CI
  • Independent audit per CLAUDE.md §8

References (CLAUDE.md §7)

Out of scope (deferred follow-ups)

  • D7.2.a Anthropic /invoke → Converse migration. Keeping legacy path until cp-api / dashboard test fixtures are updated to assert the Converse outbound body shape instead of the Anthropic Messages shape. Customer-visible ChatResponse is identical on both paths.
  • Tool-use / image / document `ContentBlock` variants. Chat scenarios only carry `Text` blocks today; the `Role::Tool` arm in `build_converse_inputs` is a deliberate no-op pending a structured tool-use surface on `ChatFormat`.

Unblocks

AC.10 in api7/AISIX-Cloud#302: "Bedrock 上 Claude / Llama 都能 stream". Both halves now done — Claude streaming via Converse, Llama via Converse (chat + stream). Live e2e against mock-bedrock (#480) is the next sub-step, separate PR.

Summary by CodeRabbit

  • New Features

    • Unified routing: non-Anthropic providers now use Bedrock's Converse endpoints for both chat and streaming; Anthropic non-streaming retains legacy path.
  • Improvements

    • Stronger model-id validation, improved error handling including retry propagation and sensitive-info redaction.
  • Documentation

    • Status/docs updated to reflect Phase G routing and provider behavior.
  • Tests

    • Tests updated to remove old error expectations and verify Converse routing and request shape.

Review Change Stack

…ming (#302 Phase G Step 3 / D7.2.b / D7.3 / D7.4)
Replaces per-publisher /invoke dispatch with the unified Converse
API path for everything except Anthropic non-stream (which keeps
its existing /invoke path for backward compat with operator
deployments + the e2e test fixtures pinned in #320). All other
dispatch — Meta / Mistral / Cohere / Amazon Titan / Amazon Nova /
AI21 — and ALL chat_stream() requests — including Anthropic —
now flow through the SDK's `.converse()` / `.converse_stream()`.
This closes D7.2.b (Anthropic streaming), D7.3 (Meta dispatch),
and D7.4 (Mistral / Amazon / Cohere / AI21 dispatch) per #302
Phase G's audit-corrected roadmap.
## Why Converse (vs. extending the per-publisher /invoke path)
AWS introduced the Converse API exactly to solve the per-publisher
body-shaping problem: one JSON envelope (`messages[].content[].text`
+ `system[]` + `inferenceConfig`), one URL pattern
(`/model/<id>/converse[-stream]`), one response shape, works for
every Bedrock publisher. The legacy /invoke path requires a
per-publisher body translator on the gateway side (Anthropic's
Messages, Meta's Llama prompt template, Cohere's command shape,
etc.) — multiplying maintenance burden as AWS adds publishers.
## Eventstream framing
`/converse-stream` emits `application/vnd.amazon.eventstream`
binary frames. The aws-sdk-bedrockruntime SDK owns the frame
decoder — we don't need our own (compare with the mock-bedrock
fixture I wrote in api7/AISIX-Cloud#480 which hand-rolled the
encoder for the test mock). The bridge layer just walks the
typed `ConverseStreamOutput` event sequence and maps each event
to zero, one, or two `ChatChunk`s:
- `MessageStart` → role chunk (first emission only)
- `ContentBlockDelta` (text) → content chunk
- `MessageStop` → finish_reason chunk
- `Metadata` → usage chunk
- `ContentBlockStart` / `ContentBlockStop` / tool-use deltas → no chunk
## Error classification
The new `map_aws_sdk_error_generic` helper preserves the existing
4xx-vs-5xx classification: ServiceError → `UpstreamStatus` carrying
the HTTP code; TimeoutError or deadline-elapsed → `Timeout`;
everything else → `Transport`. Mirrors the pattern in the legacy
`map_sdk_error` + the audit-corrected Vertex/Azure token-mint
classifications (#387, #388).
## Test changes
Tests deleted (obsolete after Converse wiring):
- chat_rejects_non_anthropic_publishers_with_publisher_named
- chat_stream_returns_clear_not_implemented_error
- chat_stream_anthropic_returns_d7_2_b_specific_error
- chat_stream_non_anthropic_publisher_returns_d7_3_specific_error
- chat_publisher_not_implemented_error_includes_model_id_and_publisher_name
Tests updated:
- chat_ignores_req_model_and_uses_ctx_model_name — re-anchored to
the publisher-unknown error path (only publisher-resolution
failures still produce Config errors before dispatch)
Tests added (5 new for the Converse path):
- chat_meta_publisher_dispatches_via_converse_url —
`/model/meta.*/converse` path regex + Bedrock-shape response decode
- chat_amazon_nova_publisher_dispatches_via_converse_url —
same dispatch contract, different publisher prefix (regression
guard against a future change that hard-codes per-publisher
routing inside chat_converse)
- chat_stream_for_anthropic_dispatches_via_converse_stream_url —
`/converse-stream` URL pattern pin for Anthropic streaming
(legacy /invoke had no stream variant)
- chat_stream_for_meta_dispatches_via_converse_stream_url —
same pattern for non-Anthropic streaming
- chat_converse_request_body_uses_text_content_block_shape —
pins outbound body shape: system messages lifted to top-level
`system[]`, user messages emit typed `content: [{text: "..."}]`
blocks (NOT Anthropic-style flat string), no top-level `model`
field
`cargo test -p aisix-provider-bedrock` → 41/41 PASS (was 36 with
5 deletions + 1 update + 5 additions).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings`
clean. `cargo fmt --all` applied.
## References (CLAUDE.md §7)
- Converse API spec:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html
- Converse stream events:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html
- aws-sdk-bedrockruntime 1.130.0 operation modules:
https://docs.rs/aws-sdk-bedrockruntime/1.130.0/aws_sdk_bedrockruntime/operation/
- mock-bedrock fixture (api7/AISIX-Cloud#480) uses the same
`vnd.amazon.eventstream` frame format for its encoder; the bridge
here consumes the SDK-decoded events rather than re-implementing
the decoder
## Out of scope (deferred follow-ups)
- D7.2.a Anthropic /invoke → Converse migration. Keeping legacy
path until cp-api / dashboard test fixtures are updated to assert
the Converse outbound body shape instead of the Anthropic
Messages shape. The customer-visible ChatResponse is identical
on both paths.
- Tool-use / image / document `ContentBlock` variants — chat scenarios
only carry `Text` blocks today; the `Role::Tool` arm in
`build_converse_inputs` is a deliberate no-op pending a structured
tool-use surface on `ChatFormat`.
## Unblocks
AC.10 in api7/AISIX-Cloud#302: "Bedrock 上 Claude / Llama 都能 stream".
Both halves are now done — Claude streaming via Converse, Llama
via Converse (chat + stream). Live e2e against mock-bedrock
(#480) is the next sub-step, separate PR.
CopilotAI review requested due to automatic review settings May 25, 2026 00:07
@coderabbitai

coderabbitaiBot commented May 25, 2026

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

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 0a0e3bea-4faf-4241-be55-b99b16ae84ca

📥 Commits

Reviewing files that changed from the base of the PR and between f082564 and b97829c.

📒 Files selected for processing (1)
  • crates/aisix-provider-bedrock/src/bridge.rs

📝 Walkthrough

Walkthrough

This PR completes Bedrock Phase G by unifying chat and streaming dispatch through the Bedrock Converse API for all non-Anthropic publishers, while preserving Anthropic non-streaming on the legacy invoke path. New translation, streaming, and error-mapping helpers support the Converse integration, with test coverage validating the correct routing and request body shapes.

Changes

Bedrock Phase G Converse Unification

Layer / File(s)Summary
Phase G Documentation and Dependencies
crates/aisix-provider-bedrock/src/lib.rs, crates/aisix-provider-bedrock/Cargo.toml
Crate documentation updates mark Phase G completion with checked items for unified Converse dispatch and legacy Anthropic invoke path. aisix-provider-anthropic dependency is reintroduced in Cargo.toml alongside routing comments.
Chat Dispatch Routing and Setup
crates/aisix-provider-bedrock/src/bridge.rs
Updated imports include Converse SDK types. chat() method routes Anthropic to chat_anthropic and all others to chat_converse; chat_stream() always uses unified chat_converse_stream. Doc comments clarify legacy /invoke path usage and a small helper is allowed dead code.
Non-streaming & Streaming Converse Implementation
crates/aisix-provider-bedrock/src/bridge.rs
Adds build_client_from_ctx, implements chat_converse and chat_converse_stream, translates request/response shapes (build_converse_inputs, converse_output_into_chat_response, map_stop_reason, emit_converse_chunk), and centralizes AWS SDK error mapping with Retry-After preservation and deadline-aware classification.
Test Updates for Phase G Converse Routing
crates/aisix-provider-bedrock/src/bridge.rs
Removed tests asserting old "not yet implemented" publisher/streaming errors. Added Phase G tests verify /converse and /converse-stream URL routing and Converse request body shape: typed content-block arrays, top-level system field, and absent top-level model field; updated publisher-resolution regression tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 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.

CopilotAI 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.

Pull request overview

This PR switches aisix-provider-bedrock from per-publisher /invoke dispatch to the unified Bedrock Converse API for all non-Anthropic chat() calls and for allchat_stream() calls (including Anthropic), while keeping Anthropic non-streaming on the legacy /invoke path for backwards compatibility.

Changes:

  • Route non-Anthropic chat() requests via POST /model/<id>/converse and implement response translation to ChatResponse.
  • Implement chat_stream() for all publishers via POST /model/<id>/converse-stream, mapping typed SDK events into ChatChunks.
  • Update unit tests and add wiremock-based dispatch/body-shape guards; add async-stream dependency.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.

FileDescription
crates/aisix-provider-bedrock/src/lib.rsUpdates crate-level docs to reflect Converse-based dispatch and streaming support.
crates/aisix-provider-bedrock/src/bridge.rsImplements Converse + ConverseStream dispatch paths, request/response translation, and associated tests.
crates/aisix-provider-bedrock/Cargo.tomlAdds async-stream and updates comments to match the new dispatch split.
Cargo.lockRecords the new async-stream dependency in the lockfile.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +785 to +792
// Tool-result messages are part of Anthropic's tool-use
// protocol; Bedrock Converse supports them via
// `ContentBlock::ToolResult` but the gateway's ChatMessage
// surface doesn't carry the structured tool_use_id +
// content shape needed to round-trip cleanly. Skip
// silently for now; a tool-use-aware follow-up PR can
// wire this when the upstream ChatFormat extends to
// carry the structured payload.
Comment on lines +936 to +945
/// Map the SDK's `ConverseError` SdkError variant to BridgeError.
/// Same classification rules as `map_sdk_error` (UpstreamStatus for
/// retryable upstream failures, Timeout on dispatch timeout).
fn map_converse_sdk_error(
e: SdkError<ConverseError, aws_smithy_runtime_api::http::Response>,
started: Instant,
deadline: Option<Duration>,
) -> BridgeError {
map_aws_sdk_error_generic(e.to_string(), &e, started, deadline)
}
Comment on lines +979 to +988
match err {
SdkError::ServiceError(svc) => {
let status = svc.raw().status().as_u16();
BridgeError::upstream_status(status, msg)
}
SdkError::TimeoutError(_) => BridgeError::Timeout {
elapsed_ms: started.elapsed().as_millis() as u64,
},
_ => BridgeError::Transport(msg),
}
// unset by default so the upstream model's own defaults apply.
// A follow-up override-pipeline PR can wire temperature /
// max_tokens from RequestOverrides.
let _ = InferenceConfiguration::builder();
Comment on lines +802 to +820
fn converse_output_into_chat_response(
resp: aws_sdk_bedrockruntime::operation::converse::ConverseOutput,
upstream_id: &str,
) -> ChatResponse {
let (text, finish) = match resp.output() {
Some(aws_sdk_bedrockruntime::types::ConverseOutput::Message(msg)) => {
let text: String = msg
.content()
.iter()
.filter_map(|cb| match cb {
ContentBlock::Text(t) => Some(t.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("");
(text, map_stop_reason(resp.stop_reason()))
}
_ => (String::new(), FinishReason::Stop),
};
…audit HIGH/MEDIUM on #389)
Independent audit-aigw-389-bedrock-converse flagged 2 HIGH + 2
MEDIUM bugs in the original Converse wiring. All fixed in this
commit; 45/45 tests pass.
## HIGH-1 + HIGH-2 — `map_aws_sdk_error_generic` silently
regressed the legacy /invoke path's error envelope hardening
The original helper collapsed all ServiceError responses to
`BridgeError::upstream_status(status, msg)`, a convenience
constructor that sets `wire: Unknown` + `parsed: None` +
`retry_after: None`. The legacy `chat_anthropic` /invoke path
uses `map_service_error` which builds a fully-shaped
`BridgeError::UpstreamStatus` with:
- wire: Bedrock — required by error_translate to render Bedrock-
shape errors back to OpenAI/Anthropic-shape clients
- retry_after: parsed from upstream Retry-After header — required
by the cooldown layer to honour AWS throttle hints
- parsed.kind: extracted via .meta().code() — distinguishes a
ThrottlingException 429 from a different throttle source
Without this fix every non-Anthropic publisher + all streaming
silently shipped degraded errors vs. the legacy path — broke the
PR #323 audit (MEDIUM-2) hardening it had just restored.
Fix: introduce `bedrock_service_error_to_upstream_status` (generic
over `E: ProvideErrorMetadata`) that mirrors `map_service_error`'s
field-by-field construction. Both Converse + ConverseStream errors
now flow through it.
## MEDIUM-1 — `e.to_string()` produced opaque "service error"
strings as the customer-visible message
Same root cause; resolved by the HIGH fix above. The customer-
visible `message` now uses the legacy path's canned status-keyed
phrase ("upstream rate limited", "upstream authentication failed",
etc.) — preserving the operator-ARN redaction the legacy path
also enforces.
## MEDIUM-2 — `chat_converse` silently dropped ChatFormat's
temperature/max_tokens/top_p
The original `chat_converse` constructed an
`InferenceConfiguration::builder()` and immediately discarded it
with `let _ =`. Every non-Anthropic Bedrock customer using these
knobs would see them vanish — real behavioural regression vs. the
legacy chat_anthropic path which forwards them via build_request →
AnthropicRequest.
Fix: new `build_inference_config(req)` helper extracts
temperature / max_tokens / top_p when set, returns None when all
absent (so we don't emit an empty `inferenceConfig: {}` which 400s
on some Bedrock publishers). Wired into both chat_converse and
chat_converse_stream.
## Tests added (5 new for the audit fixes)
- chat_converse_maps_upstream_429_with_retry_after_and_bedrock_wire
pins the HIGH-1+2 fix: 429 + Retry-After: 42 → UpstreamStatus
with status=429, message="upstream rate limited",
wire=Bedrock, retry_after=Some(42s), parsed.kind="ThrottlingException"
- chat_converse_maps_upstream_4xx_to_canned_message_with_bedrock_wire
pins HIGH-1 for the non-throttle path: 403 with leaky ARN body
→ canned "upstream authentication failed" (NO ARN leak),
wire=Bedrock, parsed.kind="AccessDeniedException"
- chat_converse_wires_temperature_max_tokens_top_p_into_inference_config
pins MEDIUM-2: ChatFormat knobs reach Bedrock's body as
camelCase {temperature, maxTokens, topP}
- chat_converse_omits_inference_config_when_no_knobs_set
companion: empty knobs → no inferenceConfig field on the wire
`cargo test -p aisix-provider-bedrock` → 45/45 PASS (was 41; +4
new audit-regression tests).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings`
clean.
## LOWs (non-blocking, deferred per CLAUDE.md §8)
- LOW-1: Role::Tool messages silently dropped on Converse path —
deliberate no-op pending a structured tool-use surface on
ChatFormat. Documented in `build_converse_inputs`.
- LOW-2: Per-publisher dispatch test matrix coverage (Mistral /
Cohere / AmazonTitan / AI21 not individually pinned). The match
arm in chat() is structurally uniform (`_ => chat_converse`) so
routing is correct by construction; the two existing
Meta + Amazon Nova tests already cover the non-Anthropic
dispatch contract.
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit response — all HIGH/MEDIUM addressed in commit `b97829c`

Independent audit-aigw-389-bedrock-converse returned 2 HIGH + 2 MEDIUM + 2 LOW. All HIGH/MEDIUM fixed in this commit; LOWs explicitly deferred per CLAUDE.md §8.

HIGH-1 + HIGH-2 ✅ fixed

The original `map_aws_sdk_error_generic` collapsed all ServiceError responses to `BridgeError::upstream_status(status, msg)` — a convenience constructor that sets `wire: Unknown`, `parsed: None`, `retry_after: None`. The legacy `/invoke` path's `map_service_error` (hardened by PR #323's MEDIUM-2 audit) builds full UpstreamStatus with `wire: Bedrock`, parsed AWS error code, and parsed Retry-After. The Converse path silently regressed this hardening for every non-Anthropic publisher + all streaming.

Fix: new `bedrock_service_error_to_upstream_status` helper (generic over `E: ProvideErrorMetadata`) mirrors `map_service_error` field-by-field. Both Converse + ConverseStream errors flow through it.

MEDIUM-1 ✅ fixed

Same root cause as HIGH-1; resolved by the same change. Customer-visible messages now use the canned status-keyed phrases ("upstream rate limited", "upstream authentication failed") — preserving the operator-ARN redaction the legacy path enforces.

MEDIUM-2 ✅ fixed

`chat_converse` silently dropped `ChatFormat.temperature` / `.max_tokens` / `.top_p` — real behavioural regression for non-Anthropic Bedrock customers. New `build_inference_config` helper extracts the knobs when set (returns None when all absent to avoid emitting empty `inferenceConfig: {}` which 400s on some publishers). Wired into both chat_converse and chat_converse_stream.

Test coverage added (4 new tests pinning the audit fixes)

  • `chat_converse_maps_upstream_429_with_retry_after_and_bedrock_wire` — pins HIGH-1+2: 429 + Retry-After: 42 → UpstreamStatus with status=429, message="upstream rate limited", wire=Bedrock, retry_after=Some(42s), parsed.kind="ThrottlingException"
  • `chat_converse_maps_upstream_4xx_to_canned_message_with_bedrock_wire` — pins HIGH-1 for non-throttle: 403 with leaky ARN body → canned "upstream authentication failed" (NO ARN leak), wire=Bedrock, parsed.kind="AccessDeniedException"
  • `chat_converse_wires_temperature_max_tokens_top_p_into_inference_config` — pins MEDIUM-2: ChatFormat knobs → camelCase `{temperature, maxTokens, topP}` on the wire
  • `chat_converse_omits_inference_config_when_no_knobs_set` — companion: empty knobs → no inferenceConfig field

LOWs (deferred)

  • LOW-1 (Role::Tool silently dropped): deliberate no-op pending structured tool-use surface on ChatFormat; documented in `build_converse_inputs`.
  • LOW-2 (per-publisher dispatch matrix coverage): match arm in chat() is `_ => chat_converse` so routing is correct by construction; existing Meta + Amazon Nova tests already cover the non-Anthropic dispatch contract.

Result

`cargo test -p aisix-provider-bedrock` → 45/45 PASS (was 41; +4 new audit-regression tests).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings` clean.

All HIGH/MEDIUM findings addressed. Awaiting fresh CI green.

@moonming
moonming merged commit 9ed3fb9 into mainMay 25, 2026
8 checks passed
@moonming
moonming deleted the feat/bedrock-converse branch May 25, 2026 00:25
Sign up for freeto 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

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(bedrock): wire Converse API for all publishers + Anthropic streaming (#302 Phase G Step 3 / D7.2.b/D7.3/D7.4) - #389

Merged
moonming merged 2 commits into
mainfrom
feat/bedrock-converse
May 25, 2026
Merged

feat(bedrock): wire Converse API for all publishers + Anthropic streaming (#302 Phase G Step 3 / D7.2.b/D7.3/D7.4)#389
moonming merged 2 commits into
mainfrom
feat/bedrock-converse

Conversation

@moonming

@moonmingmoonming commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Replaces per-publisher `/invoke` dispatch with the unified Converse API path for everything except Anthropic non-stream (which keeps its existing `/invoke` path for backward compat with operator deployments + e2e test fixtures pinned in #320). All other dispatch — Meta / Mistral / Cohere / Amazon Titan / Amazon Nova / AI21 — and all `chat_stream()` requests (including Anthropic) — now flow through the SDK's `.converse()` / `.converse_stream()`.

Closes D7.2.b (Anthropic streaming), D7.3 (Meta dispatch), and D7.4 (Mistral / Amazon Titan/Nova / Cohere / AI21 dispatch) per #302 Phase G's audit-corrected roadmap.

Why Converse vs. extending per-publisher `/invoke`

AWS introduced the Converse API exactly to solve the per-publisher body-shaping problem: one JSON envelope (`messages[].content[].text` + `system[]` + `inferenceConfig`), one URL pattern, one response shape — works for every Bedrock publisher. The legacy `/invoke` path requires a per-publisher body translator on the gateway side (Anthropic Messages, Meta Llama prompt template, Cohere command shape, etc.) — that multiplies maintenance burden as AWS adds publishers.

Eventstream framing

`/converse-stream` emits `application/vnd.amazon.eventstream` binary frames. The aws-sdk-bedrockruntime SDK owns the frame decoder — we don't need our own (compare with the mock-bedrock fixture in #480 which hand-rolled the encoder for the test mock). The bridge layer just walks the typed `ConverseStreamOutput` event sequence and maps each event to zero, one, or two `ChatChunk`s:

EventEmits
`MessageStart`role chunk (first emission only)
`ContentBlockDelta` (text)content chunk
`MessageStop`finish_reason chunk
`Metadata`usage chunk
`ContentBlockStart` / `ContentBlockStop` / tool-use deltasno chunk

Dispatch shape

```rust
// chat() — Anthropic legacy /invoke for backward compat; everyone else Converse
match publisher {
BedrockPublisher::Anthropic => self.chat_anthropic(req, ctx, upstream_id).await,
_ => self.chat_converse(req, ctx, upstream_id).await,
}

// chat_stream() — all publishers through Converse (legacy /invoke had no stream variant)
self.chat_converse_stream(req, ctx, upstream_id).await
```

Test changes

Deleted (5 tests, obsolete after Converse wiring):

  • chat_rejects_non_anthropic_publishers_with_publisher_named
  • chat_stream_returns_clear_not_implemented_error
  • chat_stream_anthropic_returns_d7_2_b_specific_error
  • chat_stream_non_anthropic_publisher_returns_d7_3_specific_error
  • chat_publisher_not_implemented_error_includes_model_id_and_publisher_name

Updated (1 test):

  • chat_ignores_req_model_and_uses_ctx_model_name — re-anchored to publisher-unknown error path (only publisher-resolution failures still produce Config errors before dispatch)

Added (5 tests):

  • `chat_meta_publisher_dispatches_via_converse_url` — `/model/meta.*/converse` regex + response decode
  • `chat_amazon_nova_publisher_dispatches_via_converse_url` — same contract, different publisher prefix (regression guard against per-publisher hard-coding)
  • `chat_stream_for_anthropic_dispatches_via_converse_stream_url` — `/converse-stream` pattern for Anthropic streaming
  • `chat_stream_for_meta_dispatches_via_converse_stream_url` — same for non-Anthropic streaming
  • `chat_converse_request_body_uses_text_content_block_shape` — pins outbound body: system → top-level array, user → typed text blocks (NOT Anthropic-style flat string), no top-level `model`

Test plan

  • `cargo test -p aisix-provider-bedrock` → 41/41 PASS
  • `cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings` clean
  • `cargo fmt --all` applied
  • CI
  • Independent audit per CLAUDE.md §8

References (CLAUDE.md §7)

Out of scope (deferred follow-ups)

  • D7.2.a Anthropic /invoke → Converse migration. Keeping legacy path until cp-api / dashboard test fixtures are updated to assert the Converse outbound body shape instead of the Anthropic Messages shape. Customer-visible ChatResponse is identical on both paths.
  • Tool-use / image / document `ContentBlock` variants. Chat scenarios only carry `Text` blocks today; the `Role::Tool` arm in `build_converse_inputs` is a deliberate no-op pending a structured tool-use surface on `ChatFormat`.

Unblocks

AC.10 in api7/AISIX-Cloud#302: "Bedrock 上 Claude / Llama 都能 stream". Both halves now done — Claude streaming via Converse, Llama via Converse (chat + stream). Live e2e against mock-bedrock (#480) is the next sub-step, separate PR.

Summary by CodeRabbit

  • New Features

    • Unified routing: non-Anthropic providers now use Bedrock's Converse endpoints for both chat and streaming; Anthropic non-streaming retains legacy path.
  • Improvements

    • Stronger model-id validation, improved error handling including retry propagation and sensitive-info redaction.
  • Documentation

    • Status/docs updated to reflect Phase G routing and provider behavior.
  • Tests

    • Tests updated to remove old error expectations and verify Converse routing and request shape.

Review Change Stack

…ming (#302 Phase G Step 3 / D7.2.b / D7.3 / D7.4)
Replaces per-publisher /invoke dispatch with the unified Converse
API path for everything except Anthropic non-stream (which keeps
its existing /invoke path for backward compat with operator
deployments + the e2e test fixtures pinned in #320). All other
dispatch — Meta / Mistral / Cohere / Amazon Titan / Amazon Nova /
AI21 — and ALL chat_stream() requests — including Anthropic —
now flow through the SDK's `.converse()` / `.converse_stream()`.
This closes D7.2.b (Anthropic streaming), D7.3 (Meta dispatch),
and D7.4 (Mistral / Amazon / Cohere / AI21 dispatch) per #302
Phase G's audit-corrected roadmap.
## Why Converse (vs. extending the per-publisher /invoke path)
AWS introduced the Converse API exactly to solve the per-publisher
body-shaping problem: one JSON envelope (`messages[].content[].text`
+ `system[]` + `inferenceConfig`), one URL pattern
(`/model/<id>/converse[-stream]`), one response shape, works for
every Bedrock publisher. The legacy /invoke path requires a
per-publisher body translator on the gateway side (Anthropic's
Messages, Meta's Llama prompt template, Cohere's command shape,
etc.) — multiplying maintenance burden as AWS adds publishers.
## Eventstream framing
`/converse-stream` emits `application/vnd.amazon.eventstream`
binary frames. The aws-sdk-bedrockruntime SDK owns the frame
decoder — we don't need our own (compare with the mock-bedrock
fixture I wrote in api7/AISIX-Cloud#480 which hand-rolled the
encoder for the test mock). The bridge layer just walks the
typed `ConverseStreamOutput` event sequence and maps each event
to zero, one, or two `ChatChunk`s:
- `MessageStart` → role chunk (first emission only)
- `ContentBlockDelta` (text) → content chunk
- `MessageStop` → finish_reason chunk
- `Metadata` → usage chunk
- `ContentBlockStart` / `ContentBlockStop` / tool-use deltas → no chunk
## Error classification
The new `map_aws_sdk_error_generic` helper preserves the existing
4xx-vs-5xx classification: ServiceError → `UpstreamStatus` carrying
the HTTP code; TimeoutError or deadline-elapsed → `Timeout`;
everything else → `Transport`. Mirrors the pattern in the legacy
`map_sdk_error` + the audit-corrected Vertex/Azure token-mint
classifications (#387, #388).
## Test changes
Tests deleted (obsolete after Converse wiring):
- chat_rejects_non_anthropic_publishers_with_publisher_named
- chat_stream_returns_clear_not_implemented_error
- chat_stream_anthropic_returns_d7_2_b_specific_error
- chat_stream_non_anthropic_publisher_returns_d7_3_specific_error
- chat_publisher_not_implemented_error_includes_model_id_and_publisher_name
Tests updated:
- chat_ignores_req_model_and_uses_ctx_model_name — re-anchored to
the publisher-unknown error path (only publisher-resolution
failures still produce Config errors before dispatch)
Tests added (5 new for the Converse path):
- chat_meta_publisher_dispatches_via_converse_url —
`/model/meta.*/converse` path regex + Bedrock-shape response decode
- chat_amazon_nova_publisher_dispatches_via_converse_url —
same dispatch contract, different publisher prefix (regression
guard against a future change that hard-codes per-publisher
routing inside chat_converse)
- chat_stream_for_anthropic_dispatches_via_converse_stream_url —
`/converse-stream` URL pattern pin for Anthropic streaming
(legacy /invoke had no stream variant)
- chat_stream_for_meta_dispatches_via_converse_stream_url —
same pattern for non-Anthropic streaming
- chat_converse_request_body_uses_text_content_block_shape —
pins outbound body shape: system messages lifted to top-level
`system[]`, user messages emit typed `content: [{text: "..."}]`
blocks (NOT Anthropic-style flat string), no top-level `model`
field
`cargo test -p aisix-provider-bedrock` → 41/41 PASS (was 36 with
5 deletions + 1 update + 5 additions).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings`
clean. `cargo fmt --all` applied.
## References (CLAUDE.md §7)
- Converse API spec:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html
- Converse stream events:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html
- aws-sdk-bedrockruntime 1.130.0 operation modules:
https://docs.rs/aws-sdk-bedrockruntime/1.130.0/aws_sdk_bedrockruntime/operation/
- mock-bedrock fixture (api7/AISIX-Cloud#480) uses the same
`vnd.amazon.eventstream` frame format for its encoder; the bridge
here consumes the SDK-decoded events rather than re-implementing
the decoder
## Out of scope (deferred follow-ups)
- D7.2.a Anthropic /invoke → Converse migration. Keeping legacy
path until cp-api / dashboard test fixtures are updated to assert
the Converse outbound body shape instead of the Anthropic
Messages shape. The customer-visible ChatResponse is identical
on both paths.
- Tool-use / image / document `ContentBlock` variants — chat scenarios
only carry `Text` blocks today; the `Role::Tool` arm in
`build_converse_inputs` is a deliberate no-op pending a structured
tool-use surface on `ChatFormat`.
## Unblocks
AC.10 in api7/AISIX-Cloud#302: "Bedrock 上 Claude / Llama 都能 stream".
Both halves are now done — Claude streaming via Converse, Llama
via Converse (chat + stream). Live e2e against mock-bedrock
(#480) is the next sub-step, separate PR.
CopilotAI review requested due to automatic review settings May 25, 2026 00:07
@coderabbitai

coderabbitaiBot commented May 25, 2026

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

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 0a0e3bea-4faf-4241-be55-b99b16ae84ca

📥 Commits

Reviewing files that changed from the base of the PR and between f082564 and b97829c.

📒 Files selected for processing (1)
  • crates/aisix-provider-bedrock/src/bridge.rs

📝 Walkthrough

Walkthrough

This PR completes Bedrock Phase G by unifying chat and streaming dispatch through the Bedrock Converse API for all non-Anthropic publishers, while preserving Anthropic non-streaming on the legacy invoke path. New translation, streaming, and error-mapping helpers support the Converse integration, with test coverage validating the correct routing and request body shapes.

Changes

Bedrock Phase G Converse Unification

Layer / File(s)Summary
Phase G Documentation and Dependencies
crates/aisix-provider-bedrock/src/lib.rs, crates/aisix-provider-bedrock/Cargo.toml
Crate documentation updates mark Phase G completion with checked items for unified Converse dispatch and legacy Anthropic invoke path. aisix-provider-anthropic dependency is reintroduced in Cargo.toml alongside routing comments.
Chat Dispatch Routing and Setup
crates/aisix-provider-bedrock/src/bridge.rs
Updated imports include Converse SDK types. chat() method routes Anthropic to chat_anthropic and all others to chat_converse; chat_stream() always uses unified chat_converse_stream. Doc comments clarify legacy /invoke path usage and a small helper is allowed dead code.
Non-streaming & Streaming Converse Implementation
crates/aisix-provider-bedrock/src/bridge.rs
Adds build_client_from_ctx, implements chat_converse and chat_converse_stream, translates request/response shapes (build_converse_inputs, converse_output_into_chat_response, map_stop_reason, emit_converse_chunk), and centralizes AWS SDK error mapping with Retry-After preservation and deadline-aware classification.
Test Updates for Phase G Converse Routing
crates/aisix-provider-bedrock/src/bridge.rs
Removed tests asserting old "not yet implemented" publisher/streaming errors. Added Phase G tests verify /converse and /converse-stream URL routing and Converse request body shape: typed content-block arrays, top-level system field, and absent top-level model field; updated publisher-resolution regression tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 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.

CopilotAI 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.

Pull request overview

This PR switches aisix-provider-bedrock from per-publisher /invoke dispatch to the unified Bedrock Converse API for all non-Anthropic chat() calls and for allchat_stream() calls (including Anthropic), while keeping Anthropic non-streaming on the legacy /invoke path for backwards compatibility.

Changes:

  • Route non-Anthropic chat() requests via POST /model/<id>/converse and implement response translation to ChatResponse.
  • Implement chat_stream() for all publishers via POST /model/<id>/converse-stream, mapping typed SDK events into ChatChunks.
  • Update unit tests and add wiremock-based dispatch/body-shape guards; add async-stream dependency.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.

FileDescription
crates/aisix-provider-bedrock/src/lib.rsUpdates crate-level docs to reflect Converse-based dispatch and streaming support.
crates/aisix-provider-bedrock/src/bridge.rsImplements Converse + ConverseStream dispatch paths, request/response translation, and associated tests.
crates/aisix-provider-bedrock/Cargo.tomlAdds async-stream and updates comments to match the new dispatch split.
Cargo.lockRecords the new async-stream dependency in the lockfile.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +785 to +792
// Tool-result messages are part of Anthropic's tool-use
// protocol; Bedrock Converse supports them via
// `ContentBlock::ToolResult` but the gateway's ChatMessage
// surface doesn't carry the structured tool_use_id +
// content shape needed to round-trip cleanly. Skip
// silently for now; a tool-use-aware follow-up PR can
// wire this when the upstream ChatFormat extends to
// carry the structured payload.
Comment on lines +936 to +945
/// Map the SDK's `ConverseError` SdkError variant to BridgeError.
/// Same classification rules as `map_sdk_error` (UpstreamStatus for
/// retryable upstream failures, Timeout on dispatch timeout).
fn map_converse_sdk_error(
e: SdkError<ConverseError, aws_smithy_runtime_api::http::Response>,
started: Instant,
deadline: Option<Duration>,
) -> BridgeError {
map_aws_sdk_error_generic(e.to_string(), &e, started, deadline)
}
Comment on lines +979 to +988
match err {
SdkError::ServiceError(svc) => {
let status = svc.raw().status().as_u16();
BridgeError::upstream_status(status, msg)
}
SdkError::TimeoutError(_) => BridgeError::Timeout {
elapsed_ms: started.elapsed().as_millis() as u64,
},
_ => BridgeError::Transport(msg),
}
// unset by default so the upstream model's own defaults apply.
// A follow-up override-pipeline PR can wire temperature /
// max_tokens from RequestOverrides.
let _ = InferenceConfiguration::builder();
Comment on lines +802 to +820
fn converse_output_into_chat_response(
resp: aws_sdk_bedrockruntime::operation::converse::ConverseOutput,
upstream_id: &str,
) -> ChatResponse {
let (text, finish) = match resp.output() {
Some(aws_sdk_bedrockruntime::types::ConverseOutput::Message(msg)) => {
let text: String = msg
.content()
.iter()
.filter_map(|cb| match cb {
ContentBlock::Text(t) => Some(t.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("");
(text, map_stop_reason(resp.stop_reason()))
}
_ => (String::new(), FinishReason::Stop),
};
…audit HIGH/MEDIUM on #389)
Independent audit-aigw-389-bedrock-converse flagged 2 HIGH + 2
MEDIUM bugs in the original Converse wiring. All fixed in this
commit; 45/45 tests pass.
## HIGH-1 + HIGH-2 — `map_aws_sdk_error_generic` silently
regressed the legacy /invoke path's error envelope hardening
The original helper collapsed all ServiceError responses to
`BridgeError::upstream_status(status, msg)`, a convenience
constructor that sets `wire: Unknown` + `parsed: None` +
`retry_after: None`. The legacy `chat_anthropic` /invoke path
uses `map_service_error` which builds a fully-shaped
`BridgeError::UpstreamStatus` with:
- wire: Bedrock — required by error_translate to render Bedrock-
shape errors back to OpenAI/Anthropic-shape clients
- retry_after: parsed from upstream Retry-After header — required
by the cooldown layer to honour AWS throttle hints
- parsed.kind: extracted via .meta().code() — distinguishes a
ThrottlingException 429 from a different throttle source
Without this fix every non-Anthropic publisher + all streaming
silently shipped degraded errors vs. the legacy path — broke the
PR #323 audit (MEDIUM-2) hardening it had just restored.
Fix: introduce `bedrock_service_error_to_upstream_status` (generic
over `E: ProvideErrorMetadata`) that mirrors `map_service_error`'s
field-by-field construction. Both Converse + ConverseStream errors
now flow through it.
## MEDIUM-1 — `e.to_string()` produced opaque "service error"
strings as the customer-visible message
Same root cause; resolved by the HIGH fix above. The customer-
visible `message` now uses the legacy path's canned status-keyed
phrase ("upstream rate limited", "upstream authentication failed",
etc.) — preserving the operator-ARN redaction the legacy path
also enforces.
## MEDIUM-2 — `chat_converse` silently dropped ChatFormat's
temperature/max_tokens/top_p
The original `chat_converse` constructed an
`InferenceConfiguration::builder()` and immediately discarded it
with `let _ =`. Every non-Anthropic Bedrock customer using these
knobs would see them vanish — real behavioural regression vs. the
legacy chat_anthropic path which forwards them via build_request →
AnthropicRequest.
Fix: new `build_inference_config(req)` helper extracts
temperature / max_tokens / top_p when set, returns None when all
absent (so we don't emit an empty `inferenceConfig: {}` which 400s
on some Bedrock publishers). Wired into both chat_converse and
chat_converse_stream.
## Tests added (5 new for the audit fixes)
- chat_converse_maps_upstream_429_with_retry_after_and_bedrock_wire
pins the HIGH-1+2 fix: 429 + Retry-After: 42 → UpstreamStatus
with status=429, message="upstream rate limited",
wire=Bedrock, retry_after=Some(42s), parsed.kind="ThrottlingException"
- chat_converse_maps_upstream_4xx_to_canned_message_with_bedrock_wire
pins HIGH-1 for the non-throttle path: 403 with leaky ARN body
→ canned "upstream authentication failed" (NO ARN leak),
wire=Bedrock, parsed.kind="AccessDeniedException"
- chat_converse_wires_temperature_max_tokens_top_p_into_inference_config
pins MEDIUM-2: ChatFormat knobs reach Bedrock's body as
camelCase {temperature, maxTokens, topP}
- chat_converse_omits_inference_config_when_no_knobs_set
companion: empty knobs → no inferenceConfig field on the wire
`cargo test -p aisix-provider-bedrock` → 45/45 PASS (was 41; +4
new audit-regression tests).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings`
clean.
## LOWs (non-blocking, deferred per CLAUDE.md §8)
- LOW-1: Role::Tool messages silently dropped on Converse path —
deliberate no-op pending a structured tool-use surface on
ChatFormat. Documented in `build_converse_inputs`.
- LOW-2: Per-publisher dispatch test matrix coverage (Mistral /
Cohere / AmazonTitan / AI21 not individually pinned). The match
arm in chat() is structurally uniform (`_ => chat_converse`) so
routing is correct by construction; the two existing
Meta + Amazon Nova tests already cover the non-Anthropic
dispatch contract.
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit response — all HIGH/MEDIUM addressed in commit `b97829c`

Independent audit-aigw-389-bedrock-converse returned 2 HIGH + 2 MEDIUM + 2 LOW. All HIGH/MEDIUM fixed in this commit; LOWs explicitly deferred per CLAUDE.md §8.

HIGH-1 + HIGH-2 ✅ fixed

The original `map_aws_sdk_error_generic` collapsed all ServiceError responses to `BridgeError::upstream_status(status, msg)` — a convenience constructor that sets `wire: Unknown`, `parsed: None`, `retry_after: None`. The legacy `/invoke` path's `map_service_error` (hardened by PR #323's MEDIUM-2 audit) builds full UpstreamStatus with `wire: Bedrock`, parsed AWS error code, and parsed Retry-After. The Converse path silently regressed this hardening for every non-Anthropic publisher + all streaming.

Fix: new `bedrock_service_error_to_upstream_status` helper (generic over `E: ProvideErrorMetadata`) mirrors `map_service_error` field-by-field. Both Converse + ConverseStream errors flow through it.

MEDIUM-1 ✅ fixed

Same root cause as HIGH-1; resolved by the same change. Customer-visible messages now use the canned status-keyed phrases ("upstream rate limited", "upstream authentication failed") — preserving the operator-ARN redaction the legacy path enforces.

MEDIUM-2 ✅ fixed

`chat_converse` silently dropped `ChatFormat.temperature` / `.max_tokens` / `.top_p` — real behavioural regression for non-Anthropic Bedrock customers. New `build_inference_config` helper extracts the knobs when set (returns None when all absent to avoid emitting empty `inferenceConfig: {}` which 400s on some publishers). Wired into both chat_converse and chat_converse_stream.

Test coverage added (4 new tests pinning the audit fixes)

  • `chat_converse_maps_upstream_429_with_retry_after_and_bedrock_wire` — pins HIGH-1+2: 429 + Retry-After: 42 → UpstreamStatus with status=429, message="upstream rate limited", wire=Bedrock, retry_after=Some(42s), parsed.kind="ThrottlingException"
  • `chat_converse_maps_upstream_4xx_to_canned_message_with_bedrock_wire` — pins HIGH-1 for non-throttle: 403 with leaky ARN body → canned "upstream authentication failed" (NO ARN leak), wire=Bedrock, parsed.kind="AccessDeniedException"
  • `chat_converse_wires_temperature_max_tokens_top_p_into_inference_config` — pins MEDIUM-2: ChatFormat knobs → camelCase `{temperature, maxTokens, topP}` on the wire
  • `chat_converse_omits_inference_config_when_no_knobs_set` — companion: empty knobs → no inferenceConfig field

LOWs (deferred)

  • LOW-1 (Role::Tool silently dropped): deliberate no-op pending structured tool-use surface on ChatFormat; documented in `build_converse_inputs`.
  • LOW-2 (per-publisher dispatch matrix coverage): match arm in chat() is `_ => chat_converse` so routing is correct by construction; existing Meta + Amazon Nova tests already cover the non-Anthropic dispatch contract.

Result

`cargo test -p aisix-provider-bedrock` → 45/45 PASS (was 41; +4 new audit-regression tests).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings` clean.

All HIGH/MEDIUM findings addressed. Awaiting fresh CI green.

@moonming
moonming merged commit 9ed3fb9 into mainMay 25, 2026
8 checks passed
@moonming
moonming deleted the feat/bedrock-converse branch May 25, 2026 00:25
Sign up for freeto 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

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(bedrock): wire Converse API for all publishers + Anthropic streaming (#302 Phase G Step 3 / D7.2.b/D7.3/D7.4) - #389

Merged
moonming merged 2 commits into
mainfrom
feat/bedrock-converse
May 25, 2026
Merged

feat(bedrock): wire Converse API for all publishers + Anthropic streaming (#302 Phase G Step 3 / D7.2.b/D7.3/D7.4)#389
moonming merged 2 commits into
mainfrom
feat/bedrock-converse

Conversation

@moonming

@moonmingmoonming commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Replaces per-publisher `/invoke` dispatch with the unified Converse API path for everything except Anthropic non-stream (which keeps its existing `/invoke` path for backward compat with operator deployments + e2e test fixtures pinned in #320). All other dispatch — Meta / Mistral / Cohere / Amazon Titan / Amazon Nova / AI21 — and all `chat_stream()` requests (including Anthropic) — now flow through the SDK's `.converse()` / `.converse_stream()`.

Closes D7.2.b (Anthropic streaming), D7.3 (Meta dispatch), and D7.4 (Mistral / Amazon Titan/Nova / Cohere / AI21 dispatch) per #302 Phase G's audit-corrected roadmap.

Why Converse vs. extending per-publisher `/invoke`

AWS introduced the Converse API exactly to solve the per-publisher body-shaping problem: one JSON envelope (`messages[].content[].text` + `system[]` + `inferenceConfig`), one URL pattern, one response shape — works for every Bedrock publisher. The legacy `/invoke` path requires a per-publisher body translator on the gateway side (Anthropic Messages, Meta Llama prompt template, Cohere command shape, etc.) — that multiplies maintenance burden as AWS adds publishers.

Eventstream framing

`/converse-stream` emits `application/vnd.amazon.eventstream` binary frames. The aws-sdk-bedrockruntime SDK owns the frame decoder — we don't need our own (compare with the mock-bedrock fixture in #480 which hand-rolled the encoder for the test mock). The bridge layer just walks the typed `ConverseStreamOutput` event sequence and maps each event to zero, one, or two `ChatChunk`s:

EventEmits
`MessageStart`role chunk (first emission only)
`ContentBlockDelta` (text)content chunk
`MessageStop`finish_reason chunk
`Metadata`usage chunk
`ContentBlockStart` / `ContentBlockStop` / tool-use deltasno chunk

Dispatch shape

```rust
// chat() — Anthropic legacy /invoke for backward compat; everyone else Converse
match publisher {
BedrockPublisher::Anthropic => self.chat_anthropic(req, ctx, upstream_id).await,
_ => self.chat_converse(req, ctx, upstream_id).await,
}

// chat_stream() — all publishers through Converse (legacy /invoke had no stream variant)
self.chat_converse_stream(req, ctx, upstream_id).await
```

Test changes

Deleted (5 tests, obsolete after Converse wiring):

  • chat_rejects_non_anthropic_publishers_with_publisher_named
  • chat_stream_returns_clear_not_implemented_error
  • chat_stream_anthropic_returns_d7_2_b_specific_error
  • chat_stream_non_anthropic_publisher_returns_d7_3_specific_error
  • chat_publisher_not_implemented_error_includes_model_id_and_publisher_name

Updated (1 test):

  • chat_ignores_req_model_and_uses_ctx_model_name — re-anchored to publisher-unknown error path (only publisher-resolution failures still produce Config errors before dispatch)

Added (5 tests):

  • `chat_meta_publisher_dispatches_via_converse_url` — `/model/meta.*/converse` regex + response decode
  • `chat_amazon_nova_publisher_dispatches_via_converse_url` — same contract, different publisher prefix (regression guard against per-publisher hard-coding)
  • `chat_stream_for_anthropic_dispatches_via_converse_stream_url` — `/converse-stream` pattern for Anthropic streaming
  • `chat_stream_for_meta_dispatches_via_converse_stream_url` — same for non-Anthropic streaming
  • `chat_converse_request_body_uses_text_content_block_shape` — pins outbound body: system → top-level array, user → typed text blocks (NOT Anthropic-style flat string), no top-level `model`

Test plan

  • `cargo test -p aisix-provider-bedrock` → 41/41 PASS
  • `cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings` clean
  • `cargo fmt --all` applied
  • CI
  • Independent audit per CLAUDE.md §8

References (CLAUDE.md §7)

Out of scope (deferred follow-ups)

  • D7.2.a Anthropic /invoke → Converse migration. Keeping legacy path until cp-api / dashboard test fixtures are updated to assert the Converse outbound body shape instead of the Anthropic Messages shape. Customer-visible ChatResponse is identical on both paths.
  • Tool-use / image / document `ContentBlock` variants. Chat scenarios only carry `Text` blocks today; the `Role::Tool` arm in `build_converse_inputs` is a deliberate no-op pending a structured tool-use surface on `ChatFormat`.

Unblocks

AC.10 in api7/AISIX-Cloud#302: "Bedrock 上 Claude / Llama 都能 stream". Both halves now done — Claude streaming via Converse, Llama via Converse (chat + stream). Live e2e against mock-bedrock (#480) is the next sub-step, separate PR.

Summary by CodeRabbit

  • New Features

    • Unified routing: non-Anthropic providers now use Bedrock's Converse endpoints for both chat and streaming; Anthropic non-streaming retains legacy path.
  • Improvements

    • Stronger model-id validation, improved error handling including retry propagation and sensitive-info redaction.
  • Documentation

    • Status/docs updated to reflect Phase G routing and provider behavior.
  • Tests

    • Tests updated to remove old error expectations and verify Converse routing and request shape.

Review Change Stack

…ming (#302 Phase G Step 3 / D7.2.b / D7.3 / D7.4)
Replaces per-publisher /invoke dispatch with the unified Converse
API path for everything except Anthropic non-stream (which keeps
its existing /invoke path for backward compat with operator
deployments + the e2e test fixtures pinned in #320). All other
dispatch — Meta / Mistral / Cohere / Amazon Titan / Amazon Nova /
AI21 — and ALL chat_stream() requests — including Anthropic —
now flow through the SDK's `.converse()` / `.converse_stream()`.
This closes D7.2.b (Anthropic streaming), D7.3 (Meta dispatch),
and D7.4 (Mistral / Amazon / Cohere / AI21 dispatch) per #302
Phase G's audit-corrected roadmap.
## Why Converse (vs. extending the per-publisher /invoke path)
AWS introduced the Converse API exactly to solve the per-publisher
body-shaping problem: one JSON envelope (`messages[].content[].text`
+ `system[]` + `inferenceConfig`), one URL pattern
(`/model/<id>/converse[-stream]`), one response shape, works for
every Bedrock publisher. The legacy /invoke path requires a
per-publisher body translator on the gateway side (Anthropic's
Messages, Meta's Llama prompt template, Cohere's command shape,
etc.) — multiplying maintenance burden as AWS adds publishers.
## Eventstream framing
`/converse-stream` emits `application/vnd.amazon.eventstream`
binary frames. The aws-sdk-bedrockruntime SDK owns the frame
decoder — we don't need our own (compare with the mock-bedrock
fixture I wrote in api7/AISIX-Cloud#480 which hand-rolled the
encoder for the test mock). The bridge layer just walks the
typed `ConverseStreamOutput` event sequence and maps each event
to zero, one, or two `ChatChunk`s:
- `MessageStart` → role chunk (first emission only)
- `ContentBlockDelta` (text) → content chunk
- `MessageStop` → finish_reason chunk
- `Metadata` → usage chunk
- `ContentBlockStart` / `ContentBlockStop` / tool-use deltas → no chunk
## Error classification
The new `map_aws_sdk_error_generic` helper preserves the existing
4xx-vs-5xx classification: ServiceError → `UpstreamStatus` carrying
the HTTP code; TimeoutError or deadline-elapsed → `Timeout`;
everything else → `Transport`. Mirrors the pattern in the legacy
`map_sdk_error` + the audit-corrected Vertex/Azure token-mint
classifications (#387, #388).
## Test changes
Tests deleted (obsolete after Converse wiring):
- chat_rejects_non_anthropic_publishers_with_publisher_named
- chat_stream_returns_clear_not_implemented_error
- chat_stream_anthropic_returns_d7_2_b_specific_error
- chat_stream_non_anthropic_publisher_returns_d7_3_specific_error
- chat_publisher_not_implemented_error_includes_model_id_and_publisher_name
Tests updated:
- chat_ignores_req_model_and_uses_ctx_model_name — re-anchored to
the publisher-unknown error path (only publisher-resolution
failures still produce Config errors before dispatch)
Tests added (5 new for the Converse path):
- chat_meta_publisher_dispatches_via_converse_url —
`/model/meta.*/converse` path regex + Bedrock-shape response decode
- chat_amazon_nova_publisher_dispatches_via_converse_url —
same dispatch contract, different publisher prefix (regression
guard against a future change that hard-codes per-publisher
routing inside chat_converse)
- chat_stream_for_anthropic_dispatches_via_converse_stream_url —
`/converse-stream` URL pattern pin for Anthropic streaming
(legacy /invoke had no stream variant)
- chat_stream_for_meta_dispatches_via_converse_stream_url —
same pattern for non-Anthropic streaming
- chat_converse_request_body_uses_text_content_block_shape —
pins outbound body shape: system messages lifted to top-level
`system[]`, user messages emit typed `content: [{text: "..."}]`
blocks (NOT Anthropic-style flat string), no top-level `model`
field
`cargo test -p aisix-provider-bedrock` → 41/41 PASS (was 36 with
5 deletions + 1 update + 5 additions).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings`
clean. `cargo fmt --all` applied.
## References (CLAUDE.md §7)
- Converse API spec:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html
- Converse stream events:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html
- aws-sdk-bedrockruntime 1.130.0 operation modules:
https://docs.rs/aws-sdk-bedrockruntime/1.130.0/aws_sdk_bedrockruntime/operation/
- mock-bedrock fixture (api7/AISIX-Cloud#480) uses the same
`vnd.amazon.eventstream` frame format for its encoder; the bridge
here consumes the SDK-decoded events rather than re-implementing
the decoder
## Out of scope (deferred follow-ups)
- D7.2.a Anthropic /invoke → Converse migration. Keeping legacy
path until cp-api / dashboard test fixtures are updated to assert
the Converse outbound body shape instead of the Anthropic
Messages shape. The customer-visible ChatResponse is identical
on both paths.
- Tool-use / image / document `ContentBlock` variants — chat scenarios
only carry `Text` blocks today; the `Role::Tool` arm in
`build_converse_inputs` is a deliberate no-op pending a structured
tool-use surface on `ChatFormat`.
## Unblocks
AC.10 in api7/AISIX-Cloud#302: "Bedrock 上 Claude / Llama 都能 stream".
Both halves are now done — Claude streaming via Converse, Llama
via Converse (chat + stream). Live e2e against mock-bedrock
(#480) is the next sub-step, separate PR.
CopilotAI review requested due to automatic review settings May 25, 2026 00:07
@coderabbitai

coderabbitaiBot commented May 25, 2026

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

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 0a0e3bea-4faf-4241-be55-b99b16ae84ca

📥 Commits

Reviewing files that changed from the base of the PR and between f082564 and b97829c.

📒 Files selected for processing (1)
  • crates/aisix-provider-bedrock/src/bridge.rs

📝 Walkthrough

Walkthrough

This PR completes Bedrock Phase G by unifying chat and streaming dispatch through the Bedrock Converse API for all non-Anthropic publishers, while preserving Anthropic non-streaming on the legacy invoke path. New translation, streaming, and error-mapping helpers support the Converse integration, with test coverage validating the correct routing and request body shapes.

Changes

Bedrock Phase G Converse Unification

Layer / File(s)Summary
Phase G Documentation and Dependencies
crates/aisix-provider-bedrock/src/lib.rs, crates/aisix-provider-bedrock/Cargo.toml
Crate documentation updates mark Phase G completion with checked items for unified Converse dispatch and legacy Anthropic invoke path. aisix-provider-anthropic dependency is reintroduced in Cargo.toml alongside routing comments.
Chat Dispatch Routing and Setup
crates/aisix-provider-bedrock/src/bridge.rs
Updated imports include Converse SDK types. chat() method routes Anthropic to chat_anthropic and all others to chat_converse; chat_stream() always uses unified chat_converse_stream. Doc comments clarify legacy /invoke path usage and a small helper is allowed dead code.
Non-streaming & Streaming Converse Implementation
crates/aisix-provider-bedrock/src/bridge.rs
Adds build_client_from_ctx, implements chat_converse and chat_converse_stream, translates request/response shapes (build_converse_inputs, converse_output_into_chat_response, map_stop_reason, emit_converse_chunk), and centralizes AWS SDK error mapping with Retry-After preservation and deadline-aware classification.
Test Updates for Phase G Converse Routing
crates/aisix-provider-bedrock/src/bridge.rs
Removed tests asserting old "not yet implemented" publisher/streaming errors. Added Phase G tests verify /converse and /converse-stream URL routing and Converse request body shape: typed content-block arrays, top-level system field, and absent top-level model field; updated publisher-resolution regression tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 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.

CopilotAI 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.

Pull request overview

This PR switches aisix-provider-bedrock from per-publisher /invoke dispatch to the unified Bedrock Converse API for all non-Anthropic chat() calls and for allchat_stream() calls (including Anthropic), while keeping Anthropic non-streaming on the legacy /invoke path for backwards compatibility.

Changes:

  • Route non-Anthropic chat() requests via POST /model/<id>/converse and implement response translation to ChatResponse.
  • Implement chat_stream() for all publishers via POST /model/<id>/converse-stream, mapping typed SDK events into ChatChunks.
  • Update unit tests and add wiremock-based dispatch/body-shape guards; add async-stream dependency.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.

FileDescription
crates/aisix-provider-bedrock/src/lib.rsUpdates crate-level docs to reflect Converse-based dispatch and streaming support.
crates/aisix-provider-bedrock/src/bridge.rsImplements Converse + ConverseStream dispatch paths, request/response translation, and associated tests.
crates/aisix-provider-bedrock/Cargo.tomlAdds async-stream and updates comments to match the new dispatch split.
Cargo.lockRecords the new async-stream dependency in the lockfile.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +785 to +792
// Tool-result messages are part of Anthropic's tool-use
// protocol; Bedrock Converse supports them via
// `ContentBlock::ToolResult` but the gateway's ChatMessage
// surface doesn't carry the structured tool_use_id +
// content shape needed to round-trip cleanly. Skip
// silently for now; a tool-use-aware follow-up PR can
// wire this when the upstream ChatFormat extends to
// carry the structured payload.
Comment on lines +936 to +945
/// Map the SDK's `ConverseError` SdkError variant to BridgeError.
/// Same classification rules as `map_sdk_error` (UpstreamStatus for
/// retryable upstream failures, Timeout on dispatch timeout).
fn map_converse_sdk_error(
e: SdkError<ConverseError, aws_smithy_runtime_api::http::Response>,
started: Instant,
deadline: Option<Duration>,
) -> BridgeError {
map_aws_sdk_error_generic(e.to_string(), &e, started, deadline)
}
Comment on lines +979 to +988
match err {
SdkError::ServiceError(svc) => {
let status = svc.raw().status().as_u16();
BridgeError::upstream_status(status, msg)
}
SdkError::TimeoutError(_) => BridgeError::Timeout {
elapsed_ms: started.elapsed().as_millis() as u64,
},
_ => BridgeError::Transport(msg),
}
// unset by default so the upstream model's own defaults apply.
// A follow-up override-pipeline PR can wire temperature /
// max_tokens from RequestOverrides.
let _ = InferenceConfiguration::builder();
Comment on lines +802 to +820
fn converse_output_into_chat_response(
resp: aws_sdk_bedrockruntime::operation::converse::ConverseOutput,
upstream_id: &str,
) -> ChatResponse {
let (text, finish) = match resp.output() {
Some(aws_sdk_bedrockruntime::types::ConverseOutput::Message(msg)) => {
let text: String = msg
.content()
.iter()
.filter_map(|cb| match cb {
ContentBlock::Text(t) => Some(t.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("");
(text, map_stop_reason(resp.stop_reason()))
}
_ => (String::new(), FinishReason::Stop),
};
…audit HIGH/MEDIUM on #389)
Independent audit-aigw-389-bedrock-converse flagged 2 HIGH + 2
MEDIUM bugs in the original Converse wiring. All fixed in this
commit; 45/45 tests pass.
## HIGH-1 + HIGH-2 — `map_aws_sdk_error_generic` silently
regressed the legacy /invoke path's error envelope hardening
The original helper collapsed all ServiceError responses to
`BridgeError::upstream_status(status, msg)`, a convenience
constructor that sets `wire: Unknown` + `parsed: None` +
`retry_after: None`. The legacy `chat_anthropic` /invoke path
uses `map_service_error` which builds a fully-shaped
`BridgeError::UpstreamStatus` with:
- wire: Bedrock — required by error_translate to render Bedrock-
shape errors back to OpenAI/Anthropic-shape clients
- retry_after: parsed from upstream Retry-After header — required
by the cooldown layer to honour AWS throttle hints
- parsed.kind: extracted via .meta().code() — distinguishes a
ThrottlingException 429 from a different throttle source
Without this fix every non-Anthropic publisher + all streaming
silently shipped degraded errors vs. the legacy path — broke the
PR #323 audit (MEDIUM-2) hardening it had just restored.
Fix: introduce `bedrock_service_error_to_upstream_status` (generic
over `E: ProvideErrorMetadata`) that mirrors `map_service_error`'s
field-by-field construction. Both Converse + ConverseStream errors
now flow through it.
## MEDIUM-1 — `e.to_string()` produced opaque "service error"
strings as the customer-visible message
Same root cause; resolved by the HIGH fix above. The customer-
visible `message` now uses the legacy path's canned status-keyed
phrase ("upstream rate limited", "upstream authentication failed",
etc.) — preserving the operator-ARN redaction the legacy path
also enforces.
## MEDIUM-2 — `chat_converse` silently dropped ChatFormat's
temperature/max_tokens/top_p
The original `chat_converse` constructed an
`InferenceConfiguration::builder()` and immediately discarded it
with `let _ =`. Every non-Anthropic Bedrock customer using these
knobs would see them vanish — real behavioural regression vs. the
legacy chat_anthropic path which forwards them via build_request →
AnthropicRequest.
Fix: new `build_inference_config(req)` helper extracts
temperature / max_tokens / top_p when set, returns None when all
absent (so we don't emit an empty `inferenceConfig: {}` which 400s
on some Bedrock publishers). Wired into both chat_converse and
chat_converse_stream.
## Tests added (5 new for the audit fixes)
- chat_converse_maps_upstream_429_with_retry_after_and_bedrock_wire
pins the HIGH-1+2 fix: 429 + Retry-After: 42 → UpstreamStatus
with status=429, message="upstream rate limited",
wire=Bedrock, retry_after=Some(42s), parsed.kind="ThrottlingException"
- chat_converse_maps_upstream_4xx_to_canned_message_with_bedrock_wire
pins HIGH-1 for the non-throttle path: 403 with leaky ARN body
→ canned "upstream authentication failed" (NO ARN leak),
wire=Bedrock, parsed.kind="AccessDeniedException"
- chat_converse_wires_temperature_max_tokens_top_p_into_inference_config
pins MEDIUM-2: ChatFormat knobs reach Bedrock's body as
camelCase {temperature, maxTokens, topP}
- chat_converse_omits_inference_config_when_no_knobs_set
companion: empty knobs → no inferenceConfig field on the wire
`cargo test -p aisix-provider-bedrock` → 45/45 PASS (was 41; +4
new audit-regression tests).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings`
clean.
## LOWs (non-blocking, deferred per CLAUDE.md §8)
- LOW-1: Role::Tool messages silently dropped on Converse path —
deliberate no-op pending a structured tool-use surface on
ChatFormat. Documented in `build_converse_inputs`.
- LOW-2: Per-publisher dispatch test matrix coverage (Mistral /
Cohere / AmazonTitan / AI21 not individually pinned). The match
arm in chat() is structurally uniform (`_ => chat_converse`) so
routing is correct by construction; the two existing
Meta + Amazon Nova tests already cover the non-Anthropic
dispatch contract.
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit response — all HIGH/MEDIUM addressed in commit `b97829c`

Independent audit-aigw-389-bedrock-converse returned 2 HIGH + 2 MEDIUM + 2 LOW. All HIGH/MEDIUM fixed in this commit; LOWs explicitly deferred per CLAUDE.md §8.

HIGH-1 + HIGH-2 ✅ fixed

The original `map_aws_sdk_error_generic` collapsed all ServiceError responses to `BridgeError::upstream_status(status, msg)` — a convenience constructor that sets `wire: Unknown`, `parsed: None`, `retry_after: None`. The legacy `/invoke` path's `map_service_error` (hardened by PR #323's MEDIUM-2 audit) builds full UpstreamStatus with `wire: Bedrock`, parsed AWS error code, and parsed Retry-After. The Converse path silently regressed this hardening for every non-Anthropic publisher + all streaming.

Fix: new `bedrock_service_error_to_upstream_status` helper (generic over `E: ProvideErrorMetadata`) mirrors `map_service_error` field-by-field. Both Converse + ConverseStream errors flow through it.

MEDIUM-1 ✅ fixed

Same root cause as HIGH-1; resolved by the same change. Customer-visible messages now use the canned status-keyed phrases ("upstream rate limited", "upstream authentication failed") — preserving the operator-ARN redaction the legacy path enforces.

MEDIUM-2 ✅ fixed

`chat_converse` silently dropped `ChatFormat.temperature` / `.max_tokens` / `.top_p` — real behavioural regression for non-Anthropic Bedrock customers. New `build_inference_config` helper extracts the knobs when set (returns None when all absent to avoid emitting empty `inferenceConfig: {}` which 400s on some publishers). Wired into both chat_converse and chat_converse_stream.

Test coverage added (4 new tests pinning the audit fixes)

  • `chat_converse_maps_upstream_429_with_retry_after_and_bedrock_wire` — pins HIGH-1+2: 429 + Retry-After: 42 → UpstreamStatus with status=429, message="upstream rate limited", wire=Bedrock, retry_after=Some(42s), parsed.kind="ThrottlingException"
  • `chat_converse_maps_upstream_4xx_to_canned_message_with_bedrock_wire` — pins HIGH-1 for non-throttle: 403 with leaky ARN body → canned "upstream authentication failed" (NO ARN leak), wire=Bedrock, parsed.kind="AccessDeniedException"
  • `chat_converse_wires_temperature_max_tokens_top_p_into_inference_config` — pins MEDIUM-2: ChatFormat knobs → camelCase `{temperature, maxTokens, topP}` on the wire
  • `chat_converse_omits_inference_config_when_no_knobs_set` — companion: empty knobs → no inferenceConfig field

LOWs (deferred)

  • LOW-1 (Role::Tool silently dropped): deliberate no-op pending structured tool-use surface on ChatFormat; documented in `build_converse_inputs`.
  • LOW-2 (per-publisher dispatch matrix coverage): match arm in chat() is `_ => chat_converse` so routing is correct by construction; existing Meta + Amazon Nova tests already cover the non-Anthropic dispatch contract.

Result

`cargo test -p aisix-provider-bedrock` → 45/45 PASS (was 41; +4 new audit-regression tests).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings` clean.

All HIGH/MEDIUM findings addressed. Awaiting fresh CI green.

@moonming
moonming merged commit 9ed3fb9 into mainMay 25, 2026
8 checks passed
@moonming
moonming deleted the feat/bedrock-converse branch May 25, 2026 00:25
Sign up for freeto 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

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(bedrock): wire Converse API for all publishers + Anthropic streaming (#302 Phase G Step 3 / D7.2.b/D7.3/D7.4) - #389

Merged
moonming merged 2 commits into
mainfrom
feat/bedrock-converse
May 25, 2026
Merged

feat(bedrock): wire Converse API for all publishers + Anthropic streaming (#302 Phase G Step 3 / D7.2.b/D7.3/D7.4)#389
moonming merged 2 commits into
mainfrom
feat/bedrock-converse

Conversation

@moonming

@moonmingmoonming commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Replaces per-publisher `/invoke` dispatch with the unified Converse API path for everything except Anthropic non-stream (which keeps its existing `/invoke` path for backward compat with operator deployments + e2e test fixtures pinned in #320). All other dispatch — Meta / Mistral / Cohere / Amazon Titan / Amazon Nova / AI21 — and all `chat_stream()` requests (including Anthropic) — now flow through the SDK's `.converse()` / `.converse_stream()`.

Closes D7.2.b (Anthropic streaming), D7.3 (Meta dispatch), and D7.4 (Mistral / Amazon Titan/Nova / Cohere / AI21 dispatch) per #302 Phase G's audit-corrected roadmap.

Why Converse vs. extending per-publisher `/invoke`

AWS introduced the Converse API exactly to solve the per-publisher body-shaping problem: one JSON envelope (`messages[].content[].text` + `system[]` + `inferenceConfig`), one URL pattern, one response shape — works for every Bedrock publisher. The legacy `/invoke` path requires a per-publisher body translator on the gateway side (Anthropic Messages, Meta Llama prompt template, Cohere command shape, etc.) — that multiplies maintenance burden as AWS adds publishers.

Eventstream framing

`/converse-stream` emits `application/vnd.amazon.eventstream` binary frames. The aws-sdk-bedrockruntime SDK owns the frame decoder — we don't need our own (compare with the mock-bedrock fixture in #480 which hand-rolled the encoder for the test mock). The bridge layer just walks the typed `ConverseStreamOutput` event sequence and maps each event to zero, one, or two `ChatChunk`s:

EventEmits
`MessageStart`role chunk (first emission only)
`ContentBlockDelta` (text)content chunk
`MessageStop`finish_reason chunk
`Metadata`usage chunk
`ContentBlockStart` / `ContentBlockStop` / tool-use deltasno chunk

Dispatch shape

```rust
// chat() — Anthropic legacy /invoke for backward compat; everyone else Converse
match publisher {
BedrockPublisher::Anthropic => self.chat_anthropic(req, ctx, upstream_id).await,
_ => self.chat_converse(req, ctx, upstream_id).await,
}

// chat_stream() — all publishers through Converse (legacy /invoke had no stream variant)
self.chat_converse_stream(req, ctx, upstream_id).await
```

Test changes

Deleted (5 tests, obsolete after Converse wiring):

  • chat_rejects_non_anthropic_publishers_with_publisher_named
  • chat_stream_returns_clear_not_implemented_error
  • chat_stream_anthropic_returns_d7_2_b_specific_error
  • chat_stream_non_anthropic_publisher_returns_d7_3_specific_error
  • chat_publisher_not_implemented_error_includes_model_id_and_publisher_name

Updated (1 test):

  • chat_ignores_req_model_and_uses_ctx_model_name — re-anchored to publisher-unknown error path (only publisher-resolution failures still produce Config errors before dispatch)

Added (5 tests):

  • `chat_meta_publisher_dispatches_via_converse_url` — `/model/meta.*/converse` regex + response decode
  • `chat_amazon_nova_publisher_dispatches_via_converse_url` — same contract, different publisher prefix (regression guard against per-publisher hard-coding)
  • `chat_stream_for_anthropic_dispatches_via_converse_stream_url` — `/converse-stream` pattern for Anthropic streaming
  • `chat_stream_for_meta_dispatches_via_converse_stream_url` — same for non-Anthropic streaming
  • `chat_converse_request_body_uses_text_content_block_shape` — pins outbound body: system → top-level array, user → typed text blocks (NOT Anthropic-style flat string), no top-level `model`

Test plan

  • `cargo test -p aisix-provider-bedrock` → 41/41 PASS
  • `cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings` clean
  • `cargo fmt --all` applied
  • CI
  • Independent audit per CLAUDE.md §8

References (CLAUDE.md §7)

Out of scope (deferred follow-ups)

  • D7.2.a Anthropic /invoke → Converse migration. Keeping legacy path until cp-api / dashboard test fixtures are updated to assert the Converse outbound body shape instead of the Anthropic Messages shape. Customer-visible ChatResponse is identical on both paths.
  • Tool-use / image / document `ContentBlock` variants. Chat scenarios only carry `Text` blocks today; the `Role::Tool` arm in `build_converse_inputs` is a deliberate no-op pending a structured tool-use surface on `ChatFormat`.

Unblocks

AC.10 in api7/AISIX-Cloud#302: "Bedrock 上 Claude / Llama 都能 stream". Both halves now done — Claude streaming via Converse, Llama via Converse (chat + stream). Live e2e against mock-bedrock (#480) is the next sub-step, separate PR.

Summary by CodeRabbit

  • New Features

    • Unified routing: non-Anthropic providers now use Bedrock's Converse endpoints for both chat and streaming; Anthropic non-streaming retains legacy path.
  • Improvements

    • Stronger model-id validation, improved error handling including retry propagation and sensitive-info redaction.
  • Documentation

    • Status/docs updated to reflect Phase G routing and provider behavior.
  • Tests

    • Tests updated to remove old error expectations and verify Converse routing and request shape.

Review Change Stack

…ming (#302 Phase G Step 3 / D7.2.b / D7.3 / D7.4)
Replaces per-publisher /invoke dispatch with the unified Converse
API path for everything except Anthropic non-stream (which keeps
its existing /invoke path for backward compat with operator
deployments + the e2e test fixtures pinned in #320). All other
dispatch — Meta / Mistral / Cohere / Amazon Titan / Amazon Nova /
AI21 — and ALL chat_stream() requests — including Anthropic —
now flow through the SDK's `.converse()` / `.converse_stream()`.
This closes D7.2.b (Anthropic streaming), D7.3 (Meta dispatch),
and D7.4 (Mistral / Amazon / Cohere / AI21 dispatch) per #302
Phase G's audit-corrected roadmap.
## Why Converse (vs. extending the per-publisher /invoke path)
AWS introduced the Converse API exactly to solve the per-publisher
body-shaping problem: one JSON envelope (`messages[].content[].text`
+ `system[]` + `inferenceConfig`), one URL pattern
(`/model/<id>/converse[-stream]`), one response shape, works for
every Bedrock publisher. The legacy /invoke path requires a
per-publisher body translator on the gateway side (Anthropic's
Messages, Meta's Llama prompt template, Cohere's command shape,
etc.) — multiplying maintenance burden as AWS adds publishers.
## Eventstream framing
`/converse-stream` emits `application/vnd.amazon.eventstream`
binary frames. The aws-sdk-bedrockruntime SDK owns the frame
decoder — we don't need our own (compare with the mock-bedrock
fixture I wrote in api7/AISIX-Cloud#480 which hand-rolled the
encoder for the test mock). The bridge layer just walks the
typed `ConverseStreamOutput` event sequence and maps each event
to zero, one, or two `ChatChunk`s:
- `MessageStart` → role chunk (first emission only)
- `ContentBlockDelta` (text) → content chunk
- `MessageStop` → finish_reason chunk
- `Metadata` → usage chunk
- `ContentBlockStart` / `ContentBlockStop` / tool-use deltas → no chunk
## Error classification
The new `map_aws_sdk_error_generic` helper preserves the existing
4xx-vs-5xx classification: ServiceError → `UpstreamStatus` carrying
the HTTP code; TimeoutError or deadline-elapsed → `Timeout`;
everything else → `Transport`. Mirrors the pattern in the legacy
`map_sdk_error` + the audit-corrected Vertex/Azure token-mint
classifications (#387, #388).
## Test changes
Tests deleted (obsolete after Converse wiring):
- chat_rejects_non_anthropic_publishers_with_publisher_named
- chat_stream_returns_clear_not_implemented_error
- chat_stream_anthropic_returns_d7_2_b_specific_error
- chat_stream_non_anthropic_publisher_returns_d7_3_specific_error
- chat_publisher_not_implemented_error_includes_model_id_and_publisher_name
Tests updated:
- chat_ignores_req_model_and_uses_ctx_model_name — re-anchored to
the publisher-unknown error path (only publisher-resolution
failures still produce Config errors before dispatch)
Tests added (5 new for the Converse path):
- chat_meta_publisher_dispatches_via_converse_url —
`/model/meta.*/converse` path regex + Bedrock-shape response decode
- chat_amazon_nova_publisher_dispatches_via_converse_url —
same dispatch contract, different publisher prefix (regression
guard against a future change that hard-codes per-publisher
routing inside chat_converse)
- chat_stream_for_anthropic_dispatches_via_converse_stream_url —
`/converse-stream` URL pattern pin for Anthropic streaming
(legacy /invoke had no stream variant)
- chat_stream_for_meta_dispatches_via_converse_stream_url —
same pattern for non-Anthropic streaming
- chat_converse_request_body_uses_text_content_block_shape —
pins outbound body shape: system messages lifted to top-level
`system[]`, user messages emit typed `content: [{text: "..."}]`
blocks (NOT Anthropic-style flat string), no top-level `model`
field
`cargo test -p aisix-provider-bedrock` → 41/41 PASS (was 36 with
5 deletions + 1 update + 5 additions).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings`
clean. `cargo fmt --all` applied.
## References (CLAUDE.md §7)
- Converse API spec:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html
- Converse stream events:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html
- aws-sdk-bedrockruntime 1.130.0 operation modules:
https://docs.rs/aws-sdk-bedrockruntime/1.130.0/aws_sdk_bedrockruntime/operation/
- mock-bedrock fixture (api7/AISIX-Cloud#480) uses the same
`vnd.amazon.eventstream` frame format for its encoder; the bridge
here consumes the SDK-decoded events rather than re-implementing
the decoder
## Out of scope (deferred follow-ups)
- D7.2.a Anthropic /invoke → Converse migration. Keeping legacy
path until cp-api / dashboard test fixtures are updated to assert
the Converse outbound body shape instead of the Anthropic
Messages shape. The customer-visible ChatResponse is identical
on both paths.
- Tool-use / image / document `ContentBlock` variants — chat scenarios
only carry `Text` blocks today; the `Role::Tool` arm in
`build_converse_inputs` is a deliberate no-op pending a structured
tool-use surface on `ChatFormat`.
## Unblocks
AC.10 in api7/AISIX-Cloud#302: "Bedrock 上 Claude / Llama 都能 stream".
Both halves are now done — Claude streaming via Converse, Llama
via Converse (chat + stream). Live e2e against mock-bedrock
(#480) is the next sub-step, separate PR.
CopilotAI review requested due to automatic review settings May 25, 2026 00:07
@coderabbitai

coderabbitaiBot commented May 25, 2026

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

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 0a0e3bea-4faf-4241-be55-b99b16ae84ca

📥 Commits

Reviewing files that changed from the base of the PR and between f082564 and b97829c.

📒 Files selected for processing (1)
  • crates/aisix-provider-bedrock/src/bridge.rs

📝 Walkthrough

Walkthrough

This PR completes Bedrock Phase G by unifying chat and streaming dispatch through the Bedrock Converse API for all non-Anthropic publishers, while preserving Anthropic non-streaming on the legacy invoke path. New translation, streaming, and error-mapping helpers support the Converse integration, with test coverage validating the correct routing and request body shapes.

Changes

Bedrock Phase G Converse Unification

Layer / File(s)Summary
Phase G Documentation and Dependencies
crates/aisix-provider-bedrock/src/lib.rs, crates/aisix-provider-bedrock/Cargo.toml
Crate documentation updates mark Phase G completion with checked items for unified Converse dispatch and legacy Anthropic invoke path. aisix-provider-anthropic dependency is reintroduced in Cargo.toml alongside routing comments.
Chat Dispatch Routing and Setup
crates/aisix-provider-bedrock/src/bridge.rs
Updated imports include Converse SDK types. chat() method routes Anthropic to chat_anthropic and all others to chat_converse; chat_stream() always uses unified chat_converse_stream. Doc comments clarify legacy /invoke path usage and a small helper is allowed dead code.
Non-streaming & Streaming Converse Implementation
crates/aisix-provider-bedrock/src/bridge.rs
Adds build_client_from_ctx, implements chat_converse and chat_converse_stream, translates request/response shapes (build_converse_inputs, converse_output_into_chat_response, map_stop_reason, emit_converse_chunk), and centralizes AWS SDK error mapping with Retry-After preservation and deadline-aware classification.
Test Updates for Phase G Converse Routing
crates/aisix-provider-bedrock/src/bridge.rs
Removed tests asserting old "not yet implemented" publisher/streaming errors. Added Phase G tests verify /converse and /converse-stream URL routing and Converse request body shape: typed content-block arrays, top-level system field, and absent top-level model field; updated publisher-resolution regression tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 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.

CopilotAI 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.

Pull request overview

This PR switches aisix-provider-bedrock from per-publisher /invoke dispatch to the unified Bedrock Converse API for all non-Anthropic chat() calls and for allchat_stream() calls (including Anthropic), while keeping Anthropic non-streaming on the legacy /invoke path for backwards compatibility.

Changes:

  • Route non-Anthropic chat() requests via POST /model/<id>/converse and implement response translation to ChatResponse.
  • Implement chat_stream() for all publishers via POST /model/<id>/converse-stream, mapping typed SDK events into ChatChunks.
  • Update unit tests and add wiremock-based dispatch/body-shape guards; add async-stream dependency.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.

FileDescription
crates/aisix-provider-bedrock/src/lib.rsUpdates crate-level docs to reflect Converse-based dispatch and streaming support.
crates/aisix-provider-bedrock/src/bridge.rsImplements Converse + ConverseStream dispatch paths, request/response translation, and associated tests.
crates/aisix-provider-bedrock/Cargo.tomlAdds async-stream and updates comments to match the new dispatch split.
Cargo.lockRecords the new async-stream dependency in the lockfile.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +785 to +792
// Tool-result messages are part of Anthropic's tool-use
// protocol; Bedrock Converse supports them via
// `ContentBlock::ToolResult` but the gateway's ChatMessage
// surface doesn't carry the structured tool_use_id +
// content shape needed to round-trip cleanly. Skip
// silently for now; a tool-use-aware follow-up PR can
// wire this when the upstream ChatFormat extends to
// carry the structured payload.
Comment on lines +936 to +945
/// Map the SDK's `ConverseError` SdkError variant to BridgeError.
/// Same classification rules as `map_sdk_error` (UpstreamStatus for
/// retryable upstream failures, Timeout on dispatch timeout).
fn map_converse_sdk_error(
e: SdkError<ConverseError, aws_smithy_runtime_api::http::Response>,
started: Instant,
deadline: Option<Duration>,
) -> BridgeError {
map_aws_sdk_error_generic(e.to_string(), &e, started, deadline)
}
Comment on lines +979 to +988
match err {
SdkError::ServiceError(svc) => {
let status = svc.raw().status().as_u16();
BridgeError::upstream_status(status, msg)
}
SdkError::TimeoutError(_) => BridgeError::Timeout {
elapsed_ms: started.elapsed().as_millis() as u64,
},
_ => BridgeError::Transport(msg),
}
// unset by default so the upstream model's own defaults apply.
// A follow-up override-pipeline PR can wire temperature /
// max_tokens from RequestOverrides.
let _ = InferenceConfiguration::builder();
Comment on lines +802 to +820
fn converse_output_into_chat_response(
resp: aws_sdk_bedrockruntime::operation::converse::ConverseOutput,
upstream_id: &str,
) -> ChatResponse {
let (text, finish) = match resp.output() {
Some(aws_sdk_bedrockruntime::types::ConverseOutput::Message(msg)) => {
let text: String = msg
.content()
.iter()
.filter_map(|cb| match cb {
ContentBlock::Text(t) => Some(t.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("");
(text, map_stop_reason(resp.stop_reason()))
}
_ => (String::new(), FinishReason::Stop),
};
…audit HIGH/MEDIUM on #389)
Independent audit-aigw-389-bedrock-converse flagged 2 HIGH + 2
MEDIUM bugs in the original Converse wiring. All fixed in this
commit; 45/45 tests pass.
## HIGH-1 + HIGH-2 — `map_aws_sdk_error_generic` silently
regressed the legacy /invoke path's error envelope hardening
The original helper collapsed all ServiceError responses to
`BridgeError::upstream_status(status, msg)`, a convenience
constructor that sets `wire: Unknown` + `parsed: None` +
`retry_after: None`. The legacy `chat_anthropic` /invoke path
uses `map_service_error` which builds a fully-shaped
`BridgeError::UpstreamStatus` with:
- wire: Bedrock — required by error_translate to render Bedrock-
shape errors back to OpenAI/Anthropic-shape clients
- retry_after: parsed from upstream Retry-After header — required
by the cooldown layer to honour AWS throttle hints
- parsed.kind: extracted via .meta().code() — distinguishes a
ThrottlingException 429 from a different throttle source
Without this fix every non-Anthropic publisher + all streaming
silently shipped degraded errors vs. the legacy path — broke the
PR #323 audit (MEDIUM-2) hardening it had just restored.
Fix: introduce `bedrock_service_error_to_upstream_status` (generic
over `E: ProvideErrorMetadata`) that mirrors `map_service_error`'s
field-by-field construction. Both Converse + ConverseStream errors
now flow through it.
## MEDIUM-1 — `e.to_string()` produced opaque "service error"
strings as the customer-visible message
Same root cause; resolved by the HIGH fix above. The customer-
visible `message` now uses the legacy path's canned status-keyed
phrase ("upstream rate limited", "upstream authentication failed",
etc.) — preserving the operator-ARN redaction the legacy path
also enforces.
## MEDIUM-2 — `chat_converse` silently dropped ChatFormat's
temperature/max_tokens/top_p
The original `chat_converse` constructed an
`InferenceConfiguration::builder()` and immediately discarded it
with `let _ =`. Every non-Anthropic Bedrock customer using these
knobs would see them vanish — real behavioural regression vs. the
legacy chat_anthropic path which forwards them via build_request →
AnthropicRequest.
Fix: new `build_inference_config(req)` helper extracts
temperature / max_tokens / top_p when set, returns None when all
absent (so we don't emit an empty `inferenceConfig: {}` which 400s
on some Bedrock publishers). Wired into both chat_converse and
chat_converse_stream.
## Tests added (5 new for the audit fixes)
- chat_converse_maps_upstream_429_with_retry_after_and_bedrock_wire
pins the HIGH-1+2 fix: 429 + Retry-After: 42 → UpstreamStatus
with status=429, message="upstream rate limited",
wire=Bedrock, retry_after=Some(42s), parsed.kind="ThrottlingException"
- chat_converse_maps_upstream_4xx_to_canned_message_with_bedrock_wire
pins HIGH-1 for the non-throttle path: 403 with leaky ARN body
→ canned "upstream authentication failed" (NO ARN leak),
wire=Bedrock, parsed.kind="AccessDeniedException"
- chat_converse_wires_temperature_max_tokens_top_p_into_inference_config
pins MEDIUM-2: ChatFormat knobs reach Bedrock's body as
camelCase {temperature, maxTokens, topP}
- chat_converse_omits_inference_config_when_no_knobs_set
companion: empty knobs → no inferenceConfig field on the wire
`cargo test -p aisix-provider-bedrock` → 45/45 PASS (was 41; +4
new audit-regression tests).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings`
clean.
## LOWs (non-blocking, deferred per CLAUDE.md §8)
- LOW-1: Role::Tool messages silently dropped on Converse path —
deliberate no-op pending a structured tool-use surface on
ChatFormat. Documented in `build_converse_inputs`.
- LOW-2: Per-publisher dispatch test matrix coverage (Mistral /
Cohere / AmazonTitan / AI21 not individually pinned). The match
arm in chat() is structurally uniform (`_ => chat_converse`) so
routing is correct by construction; the two existing
Meta + Amazon Nova tests already cover the non-Anthropic
dispatch contract.
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit response — all HIGH/MEDIUM addressed in commit `b97829c`

Independent audit-aigw-389-bedrock-converse returned 2 HIGH + 2 MEDIUM + 2 LOW. All HIGH/MEDIUM fixed in this commit; LOWs explicitly deferred per CLAUDE.md §8.

HIGH-1 + HIGH-2 ✅ fixed

The original `map_aws_sdk_error_generic` collapsed all ServiceError responses to `BridgeError::upstream_status(status, msg)` — a convenience constructor that sets `wire: Unknown`, `parsed: None`, `retry_after: None`. The legacy `/invoke` path's `map_service_error` (hardened by PR #323's MEDIUM-2 audit) builds full UpstreamStatus with `wire: Bedrock`, parsed AWS error code, and parsed Retry-After. The Converse path silently regressed this hardening for every non-Anthropic publisher + all streaming.

Fix: new `bedrock_service_error_to_upstream_status` helper (generic over `E: ProvideErrorMetadata`) mirrors `map_service_error` field-by-field. Both Converse + ConverseStream errors flow through it.

MEDIUM-1 ✅ fixed

Same root cause as HIGH-1; resolved by the same change. Customer-visible messages now use the canned status-keyed phrases ("upstream rate limited", "upstream authentication failed") — preserving the operator-ARN redaction the legacy path enforces.

MEDIUM-2 ✅ fixed

`chat_converse` silently dropped `ChatFormat.temperature` / `.max_tokens` / `.top_p` — real behavioural regression for non-Anthropic Bedrock customers. New `build_inference_config` helper extracts the knobs when set (returns None when all absent to avoid emitting empty `inferenceConfig: {}` which 400s on some publishers). Wired into both chat_converse and chat_converse_stream.

Test coverage added (4 new tests pinning the audit fixes)

  • `chat_converse_maps_upstream_429_with_retry_after_and_bedrock_wire` — pins HIGH-1+2: 429 + Retry-After: 42 → UpstreamStatus with status=429, message="upstream rate limited", wire=Bedrock, retry_after=Some(42s), parsed.kind="ThrottlingException"
  • `chat_converse_maps_upstream_4xx_to_canned_message_with_bedrock_wire` — pins HIGH-1 for non-throttle: 403 with leaky ARN body → canned "upstream authentication failed" (NO ARN leak), wire=Bedrock, parsed.kind="AccessDeniedException"
  • `chat_converse_wires_temperature_max_tokens_top_p_into_inference_config` — pins MEDIUM-2: ChatFormat knobs → camelCase `{temperature, maxTokens, topP}` on the wire
  • `chat_converse_omits_inference_config_when_no_knobs_set` — companion: empty knobs → no inferenceConfig field

LOWs (deferred)

  • LOW-1 (Role::Tool silently dropped): deliberate no-op pending structured tool-use surface on ChatFormat; documented in `build_converse_inputs`.
  • LOW-2 (per-publisher dispatch matrix coverage): match arm in chat() is `_ => chat_converse` so routing is correct by construction; existing Meta + Amazon Nova tests already cover the non-Anthropic dispatch contract.

Result

`cargo test -p aisix-provider-bedrock` → 45/45 PASS (was 41; +4 new audit-regression tests).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings` clean.

All HIGH/MEDIUM findings addressed. Awaiting fresh CI green.

@moonming
moonming merged commit 9ed3fb9 into mainMay 25, 2026
8 checks passed
@moonming
moonming deleted the feat/bedrock-converse branch May 25, 2026 00:25
Sign up for freeto 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

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(bedrock): wire Converse API for all publishers + Anthropic streaming (#302 Phase G Step 3 / D7.2.b/D7.3/D7.4) - #389

Merged
moonming merged 2 commits into
mainfrom
feat/bedrock-converse
May 25, 2026
Merged

feat(bedrock): wire Converse API for all publishers + Anthropic streaming (#302 Phase G Step 3 / D7.2.b/D7.3/D7.4)#389
moonming merged 2 commits into
mainfrom
feat/bedrock-converse

Conversation

@moonming

@moonmingmoonming commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Replaces per-publisher `/invoke` dispatch with the unified Converse API path for everything except Anthropic non-stream (which keeps its existing `/invoke` path for backward compat with operator deployments + e2e test fixtures pinned in #320). All other dispatch — Meta / Mistral / Cohere / Amazon Titan / Amazon Nova / AI21 — and all `chat_stream()` requests (including Anthropic) — now flow through the SDK's `.converse()` / `.converse_stream()`.

Closes D7.2.b (Anthropic streaming), D7.3 (Meta dispatch), and D7.4 (Mistral / Amazon Titan/Nova / Cohere / AI21 dispatch) per #302 Phase G's audit-corrected roadmap.

Why Converse vs. extending per-publisher `/invoke`

AWS introduced the Converse API exactly to solve the per-publisher body-shaping problem: one JSON envelope (`messages[].content[].text` + `system[]` + `inferenceConfig`), one URL pattern, one response shape — works for every Bedrock publisher. The legacy `/invoke` path requires a per-publisher body translator on the gateway side (Anthropic Messages, Meta Llama prompt template, Cohere command shape, etc.) — that multiplies maintenance burden as AWS adds publishers.

Eventstream framing

`/converse-stream` emits `application/vnd.amazon.eventstream` binary frames. The aws-sdk-bedrockruntime SDK owns the frame decoder — we don't need our own (compare with the mock-bedrock fixture in #480 which hand-rolled the encoder for the test mock). The bridge layer just walks the typed `ConverseStreamOutput` event sequence and maps each event to zero, one, or two `ChatChunk`s:

EventEmits
`MessageStart`role chunk (first emission only)
`ContentBlockDelta` (text)content chunk
`MessageStop`finish_reason chunk
`Metadata`usage chunk
`ContentBlockStart` / `ContentBlockStop` / tool-use deltasno chunk

Dispatch shape

```rust
// chat() — Anthropic legacy /invoke for backward compat; everyone else Converse
match publisher {
BedrockPublisher::Anthropic => self.chat_anthropic(req, ctx, upstream_id).await,
_ => self.chat_converse(req, ctx, upstream_id).await,
}

// chat_stream() — all publishers through Converse (legacy /invoke had no stream variant)
self.chat_converse_stream(req, ctx, upstream_id).await
```

Test changes

Deleted (5 tests, obsolete after Converse wiring):

  • chat_rejects_non_anthropic_publishers_with_publisher_named
  • chat_stream_returns_clear_not_implemented_error
  • chat_stream_anthropic_returns_d7_2_b_specific_error
  • chat_stream_non_anthropic_publisher_returns_d7_3_specific_error
  • chat_publisher_not_implemented_error_includes_model_id_and_publisher_name

Updated (1 test):

  • chat_ignores_req_model_and_uses_ctx_model_name — re-anchored to publisher-unknown error path (only publisher-resolution failures still produce Config errors before dispatch)

Added (5 tests):

  • `chat_meta_publisher_dispatches_via_converse_url` — `/model/meta.*/converse` regex + response decode
  • `chat_amazon_nova_publisher_dispatches_via_converse_url` — same contract, different publisher prefix (regression guard against per-publisher hard-coding)
  • `chat_stream_for_anthropic_dispatches_via_converse_stream_url` — `/converse-stream` pattern for Anthropic streaming
  • `chat_stream_for_meta_dispatches_via_converse_stream_url` — same for non-Anthropic streaming
  • `chat_converse_request_body_uses_text_content_block_shape` — pins outbound body: system → top-level array, user → typed text blocks (NOT Anthropic-style flat string), no top-level `model`

Test plan

  • `cargo test -p aisix-provider-bedrock` → 41/41 PASS
  • `cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings` clean
  • `cargo fmt --all` applied
  • CI
  • Independent audit per CLAUDE.md §8

References (CLAUDE.md §7)

Out of scope (deferred follow-ups)

  • D7.2.a Anthropic /invoke → Converse migration. Keeping legacy path until cp-api / dashboard test fixtures are updated to assert the Converse outbound body shape instead of the Anthropic Messages shape. Customer-visible ChatResponse is identical on both paths.
  • Tool-use / image / document `ContentBlock` variants. Chat scenarios only carry `Text` blocks today; the `Role::Tool` arm in `build_converse_inputs` is a deliberate no-op pending a structured tool-use surface on `ChatFormat`.

Unblocks

AC.10 in api7/AISIX-Cloud#302: "Bedrock 上 Claude / Llama 都能 stream". Both halves now done — Claude streaming via Converse, Llama via Converse (chat + stream). Live e2e against mock-bedrock (#480) is the next sub-step, separate PR.

Summary by CodeRabbit

  • New Features

    • Unified routing: non-Anthropic providers now use Bedrock's Converse endpoints for both chat and streaming; Anthropic non-streaming retains legacy path.
  • Improvements

    • Stronger model-id validation, improved error handling including retry propagation and sensitive-info redaction.
  • Documentation

    • Status/docs updated to reflect Phase G routing and provider behavior.
  • Tests

    • Tests updated to remove old error expectations and verify Converse routing and request shape.

Review Change Stack

…ming (#302 Phase G Step 3 / D7.2.b / D7.3 / D7.4)
Replaces per-publisher /invoke dispatch with the unified Converse
API path for everything except Anthropic non-stream (which keeps
its existing /invoke path for backward compat with operator
deployments + the e2e test fixtures pinned in #320). All other
dispatch — Meta / Mistral / Cohere / Amazon Titan / Amazon Nova /
AI21 — and ALL chat_stream() requests — including Anthropic —
now flow through the SDK's `.converse()` / `.converse_stream()`.
This closes D7.2.b (Anthropic streaming), D7.3 (Meta dispatch),
and D7.4 (Mistral / Amazon / Cohere / AI21 dispatch) per #302
Phase G's audit-corrected roadmap.
## Why Converse (vs. extending the per-publisher /invoke path)
AWS introduced the Converse API exactly to solve the per-publisher
body-shaping problem: one JSON envelope (`messages[].content[].text`
+ `system[]` + `inferenceConfig`), one URL pattern
(`/model/<id>/converse[-stream]`), one response shape, works for
every Bedrock publisher. The legacy /invoke path requires a
per-publisher body translator on the gateway side (Anthropic's
Messages, Meta's Llama prompt template, Cohere's command shape,
etc.) — multiplying maintenance burden as AWS adds publishers.
## Eventstream framing
`/converse-stream` emits `application/vnd.amazon.eventstream`
binary frames. The aws-sdk-bedrockruntime SDK owns the frame
decoder — we don't need our own (compare with the mock-bedrock
fixture I wrote in api7/AISIX-Cloud#480 which hand-rolled the
encoder for the test mock). The bridge layer just walks the
typed `ConverseStreamOutput` event sequence and maps each event
to zero, one, or two `ChatChunk`s:
- `MessageStart` → role chunk (first emission only)
- `ContentBlockDelta` (text) → content chunk
- `MessageStop` → finish_reason chunk
- `Metadata` → usage chunk
- `ContentBlockStart` / `ContentBlockStop` / tool-use deltas → no chunk
## Error classification
The new `map_aws_sdk_error_generic` helper preserves the existing
4xx-vs-5xx classification: ServiceError → `UpstreamStatus` carrying
the HTTP code; TimeoutError or deadline-elapsed → `Timeout`;
everything else → `Transport`. Mirrors the pattern in the legacy
`map_sdk_error` + the audit-corrected Vertex/Azure token-mint
classifications (#387, #388).
## Test changes
Tests deleted (obsolete after Converse wiring):
- chat_rejects_non_anthropic_publishers_with_publisher_named
- chat_stream_returns_clear_not_implemented_error
- chat_stream_anthropic_returns_d7_2_b_specific_error
- chat_stream_non_anthropic_publisher_returns_d7_3_specific_error
- chat_publisher_not_implemented_error_includes_model_id_and_publisher_name
Tests updated:
- chat_ignores_req_model_and_uses_ctx_model_name — re-anchored to
the publisher-unknown error path (only publisher-resolution
failures still produce Config errors before dispatch)
Tests added (5 new for the Converse path):
- chat_meta_publisher_dispatches_via_converse_url —
`/model/meta.*/converse` path regex + Bedrock-shape response decode
- chat_amazon_nova_publisher_dispatches_via_converse_url —
same dispatch contract, different publisher prefix (regression
guard against a future change that hard-codes per-publisher
routing inside chat_converse)
- chat_stream_for_anthropic_dispatches_via_converse_stream_url —
`/converse-stream` URL pattern pin for Anthropic streaming
(legacy /invoke had no stream variant)
- chat_stream_for_meta_dispatches_via_converse_stream_url —
same pattern for non-Anthropic streaming
- chat_converse_request_body_uses_text_content_block_shape —
pins outbound body shape: system messages lifted to top-level
`system[]`, user messages emit typed `content: [{text: "..."}]`
blocks (NOT Anthropic-style flat string), no top-level `model`
field
`cargo test -p aisix-provider-bedrock` → 41/41 PASS (was 36 with
5 deletions + 1 update + 5 additions).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings`
clean. `cargo fmt --all` applied.
## References (CLAUDE.md §7)
- Converse API spec:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html
- Converse stream events:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html
- aws-sdk-bedrockruntime 1.130.0 operation modules:
https://docs.rs/aws-sdk-bedrockruntime/1.130.0/aws_sdk_bedrockruntime/operation/
- mock-bedrock fixture (api7/AISIX-Cloud#480) uses the same
`vnd.amazon.eventstream` frame format for its encoder; the bridge
here consumes the SDK-decoded events rather than re-implementing
the decoder
## Out of scope (deferred follow-ups)
- D7.2.a Anthropic /invoke → Converse migration. Keeping legacy
path until cp-api / dashboard test fixtures are updated to assert
the Converse outbound body shape instead of the Anthropic
Messages shape. The customer-visible ChatResponse is identical
on both paths.
- Tool-use / image / document `ContentBlock` variants — chat scenarios
only carry `Text` blocks today; the `Role::Tool` arm in
`build_converse_inputs` is a deliberate no-op pending a structured
tool-use surface on `ChatFormat`.
## Unblocks
AC.10 in api7/AISIX-Cloud#302: "Bedrock 上 Claude / Llama 都能 stream".
Both halves are now done — Claude streaming via Converse, Llama
via Converse (chat + stream). Live e2e against mock-bedrock
(#480) is the next sub-step, separate PR.
CopilotAI review requested due to automatic review settings May 25, 2026 00:07
@coderabbitai

coderabbitaiBot commented May 25, 2026

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

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 0a0e3bea-4faf-4241-be55-b99b16ae84ca

📥 Commits

Reviewing files that changed from the base of the PR and between f082564 and b97829c.

📒 Files selected for processing (1)
  • crates/aisix-provider-bedrock/src/bridge.rs

📝 Walkthrough

Walkthrough

This PR completes Bedrock Phase G by unifying chat and streaming dispatch through the Bedrock Converse API for all non-Anthropic publishers, while preserving Anthropic non-streaming on the legacy invoke path. New translation, streaming, and error-mapping helpers support the Converse integration, with test coverage validating the correct routing and request body shapes.

Changes

Bedrock Phase G Converse Unification

Layer / File(s)Summary
Phase G Documentation and Dependencies
crates/aisix-provider-bedrock/src/lib.rs, crates/aisix-provider-bedrock/Cargo.toml
Crate documentation updates mark Phase G completion with checked items for unified Converse dispatch and legacy Anthropic invoke path. aisix-provider-anthropic dependency is reintroduced in Cargo.toml alongside routing comments.
Chat Dispatch Routing and Setup
crates/aisix-provider-bedrock/src/bridge.rs
Updated imports include Converse SDK types. chat() method routes Anthropic to chat_anthropic and all others to chat_converse; chat_stream() always uses unified chat_converse_stream. Doc comments clarify legacy /invoke path usage and a small helper is allowed dead code.
Non-streaming & Streaming Converse Implementation
crates/aisix-provider-bedrock/src/bridge.rs
Adds build_client_from_ctx, implements chat_converse and chat_converse_stream, translates request/response shapes (build_converse_inputs, converse_output_into_chat_response, map_stop_reason, emit_converse_chunk), and centralizes AWS SDK error mapping with Retry-After preservation and deadline-aware classification.
Test Updates for Phase G Converse Routing
crates/aisix-provider-bedrock/src/bridge.rs
Removed tests asserting old "not yet implemented" publisher/streaming errors. Added Phase G tests verify /converse and /converse-stream URL routing and Converse request body shape: typed content-block arrays, top-level system field, and absent top-level model field; updated publisher-resolution regression tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 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.

CopilotAI 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.

Pull request overview

This PR switches aisix-provider-bedrock from per-publisher /invoke dispatch to the unified Bedrock Converse API for all non-Anthropic chat() calls and for allchat_stream() calls (including Anthropic), while keeping Anthropic non-streaming on the legacy /invoke path for backwards compatibility.

Changes:

  • Route non-Anthropic chat() requests via POST /model/<id>/converse and implement response translation to ChatResponse.
  • Implement chat_stream() for all publishers via POST /model/<id>/converse-stream, mapping typed SDK events into ChatChunks.
  • Update unit tests and add wiremock-based dispatch/body-shape guards; add async-stream dependency.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.

FileDescription
crates/aisix-provider-bedrock/src/lib.rsUpdates crate-level docs to reflect Converse-based dispatch and streaming support.
crates/aisix-provider-bedrock/src/bridge.rsImplements Converse + ConverseStream dispatch paths, request/response translation, and associated tests.
crates/aisix-provider-bedrock/Cargo.tomlAdds async-stream and updates comments to match the new dispatch split.
Cargo.lockRecords the new async-stream dependency in the lockfile.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +785 to +792
// Tool-result messages are part of Anthropic's tool-use
// protocol; Bedrock Converse supports them via
// `ContentBlock::ToolResult` but the gateway's ChatMessage
// surface doesn't carry the structured tool_use_id +
// content shape needed to round-trip cleanly. Skip
// silently for now; a tool-use-aware follow-up PR can
// wire this when the upstream ChatFormat extends to
// carry the structured payload.
Comment on lines +936 to +945
/// Map the SDK's `ConverseError` SdkError variant to BridgeError.
/// Same classification rules as `map_sdk_error` (UpstreamStatus for
/// retryable upstream failures, Timeout on dispatch timeout).
fn map_converse_sdk_error(
e: SdkError<ConverseError, aws_smithy_runtime_api::http::Response>,
started: Instant,
deadline: Option<Duration>,
) -> BridgeError {
map_aws_sdk_error_generic(e.to_string(), &e, started, deadline)
}
Comment on lines +979 to +988
match err {
SdkError::ServiceError(svc) => {
let status = svc.raw().status().as_u16();
BridgeError::upstream_status(status, msg)
}
SdkError::TimeoutError(_) => BridgeError::Timeout {
elapsed_ms: started.elapsed().as_millis() as u64,
},
_ => BridgeError::Transport(msg),
}
// unset by default so the upstream model's own defaults apply.
// A follow-up override-pipeline PR can wire temperature /
// max_tokens from RequestOverrides.
let _ = InferenceConfiguration::builder();
Comment on lines +802 to +820
fn converse_output_into_chat_response(
resp: aws_sdk_bedrockruntime::operation::converse::ConverseOutput,
upstream_id: &str,
) -> ChatResponse {
let (text, finish) = match resp.output() {
Some(aws_sdk_bedrockruntime::types::ConverseOutput::Message(msg)) => {
let text: String = msg
.content()
.iter()
.filter_map(|cb| match cb {
ContentBlock::Text(t) => Some(t.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("");
(text, map_stop_reason(resp.stop_reason()))
}
_ => (String::new(), FinishReason::Stop),
};
…audit HIGH/MEDIUM on #389)
Independent audit-aigw-389-bedrock-converse flagged 2 HIGH + 2
MEDIUM bugs in the original Converse wiring. All fixed in this
commit; 45/45 tests pass.
## HIGH-1 + HIGH-2 — `map_aws_sdk_error_generic` silently
regressed the legacy /invoke path's error envelope hardening
The original helper collapsed all ServiceError responses to
`BridgeError::upstream_status(status, msg)`, a convenience
constructor that sets `wire: Unknown` + `parsed: None` +
`retry_after: None`. The legacy `chat_anthropic` /invoke path
uses `map_service_error` which builds a fully-shaped
`BridgeError::UpstreamStatus` with:
- wire: Bedrock — required by error_translate to render Bedrock-
shape errors back to OpenAI/Anthropic-shape clients
- retry_after: parsed from upstream Retry-After header — required
by the cooldown layer to honour AWS throttle hints
- parsed.kind: extracted via .meta().code() — distinguishes a
ThrottlingException 429 from a different throttle source
Without this fix every non-Anthropic publisher + all streaming
silently shipped degraded errors vs. the legacy path — broke the
PR #323 audit (MEDIUM-2) hardening it had just restored.
Fix: introduce `bedrock_service_error_to_upstream_status` (generic
over `E: ProvideErrorMetadata`) that mirrors `map_service_error`'s
field-by-field construction. Both Converse + ConverseStream errors
now flow through it.
## MEDIUM-1 — `e.to_string()` produced opaque "service error"
strings as the customer-visible message
Same root cause; resolved by the HIGH fix above. The customer-
visible `message` now uses the legacy path's canned status-keyed
phrase ("upstream rate limited", "upstream authentication failed",
etc.) — preserving the operator-ARN redaction the legacy path
also enforces.
## MEDIUM-2 — `chat_converse` silently dropped ChatFormat's
temperature/max_tokens/top_p
The original `chat_converse` constructed an
`InferenceConfiguration::builder()` and immediately discarded it
with `let _ =`. Every non-Anthropic Bedrock customer using these
knobs would see them vanish — real behavioural regression vs. the
legacy chat_anthropic path which forwards them via build_request →
AnthropicRequest.
Fix: new `build_inference_config(req)` helper extracts
temperature / max_tokens / top_p when set, returns None when all
absent (so we don't emit an empty `inferenceConfig: {}` which 400s
on some Bedrock publishers). Wired into both chat_converse and
chat_converse_stream.
## Tests added (5 new for the audit fixes)
- chat_converse_maps_upstream_429_with_retry_after_and_bedrock_wire
pins the HIGH-1+2 fix: 429 + Retry-After: 42 → UpstreamStatus
with status=429, message="upstream rate limited",
wire=Bedrock, retry_after=Some(42s), parsed.kind="ThrottlingException"
- chat_converse_maps_upstream_4xx_to_canned_message_with_bedrock_wire
pins HIGH-1 for the non-throttle path: 403 with leaky ARN body
→ canned "upstream authentication failed" (NO ARN leak),
wire=Bedrock, parsed.kind="AccessDeniedException"
- chat_converse_wires_temperature_max_tokens_top_p_into_inference_config
pins MEDIUM-2: ChatFormat knobs reach Bedrock's body as
camelCase {temperature, maxTokens, topP}
- chat_converse_omits_inference_config_when_no_knobs_set
companion: empty knobs → no inferenceConfig field on the wire
`cargo test -p aisix-provider-bedrock` → 45/45 PASS (was 41; +4
new audit-regression tests).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings`
clean.
## LOWs (non-blocking, deferred per CLAUDE.md §8)
- LOW-1: Role::Tool messages silently dropped on Converse path —
deliberate no-op pending a structured tool-use surface on
ChatFormat. Documented in `build_converse_inputs`.
- LOW-2: Per-publisher dispatch test matrix coverage (Mistral /
Cohere / AmazonTitan / AI21 not individually pinned). The match
arm in chat() is structurally uniform (`_ => chat_converse`) so
routing is correct by construction; the two existing
Meta + Amazon Nova tests already cover the non-Anthropic
dispatch contract.
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit response — all HIGH/MEDIUM addressed in commit `b97829c`

Independent audit-aigw-389-bedrock-converse returned 2 HIGH + 2 MEDIUM + 2 LOW. All HIGH/MEDIUM fixed in this commit; LOWs explicitly deferred per CLAUDE.md §8.

HIGH-1 + HIGH-2 ✅ fixed

The original `map_aws_sdk_error_generic` collapsed all ServiceError responses to `BridgeError::upstream_status(status, msg)` — a convenience constructor that sets `wire: Unknown`, `parsed: None`, `retry_after: None`. The legacy `/invoke` path's `map_service_error` (hardened by PR #323's MEDIUM-2 audit) builds full UpstreamStatus with `wire: Bedrock`, parsed AWS error code, and parsed Retry-After. The Converse path silently regressed this hardening for every non-Anthropic publisher + all streaming.

Fix: new `bedrock_service_error_to_upstream_status` helper (generic over `E: ProvideErrorMetadata`) mirrors `map_service_error` field-by-field. Both Converse + ConverseStream errors flow through it.

MEDIUM-1 ✅ fixed

Same root cause as HIGH-1; resolved by the same change. Customer-visible messages now use the canned status-keyed phrases ("upstream rate limited", "upstream authentication failed") — preserving the operator-ARN redaction the legacy path enforces.

MEDIUM-2 ✅ fixed

`chat_converse` silently dropped `ChatFormat.temperature` / `.max_tokens` / `.top_p` — real behavioural regression for non-Anthropic Bedrock customers. New `build_inference_config` helper extracts the knobs when set (returns None when all absent to avoid emitting empty `inferenceConfig: {}` which 400s on some publishers). Wired into both chat_converse and chat_converse_stream.

Test coverage added (4 new tests pinning the audit fixes)

  • `chat_converse_maps_upstream_429_with_retry_after_and_bedrock_wire` — pins HIGH-1+2: 429 + Retry-After: 42 → UpstreamStatus with status=429, message="upstream rate limited", wire=Bedrock, retry_after=Some(42s), parsed.kind="ThrottlingException"
  • `chat_converse_maps_upstream_4xx_to_canned_message_with_bedrock_wire` — pins HIGH-1 for non-throttle: 403 with leaky ARN body → canned "upstream authentication failed" (NO ARN leak), wire=Bedrock, parsed.kind="AccessDeniedException"
  • `chat_converse_wires_temperature_max_tokens_top_p_into_inference_config` — pins MEDIUM-2: ChatFormat knobs → camelCase `{temperature, maxTokens, topP}` on the wire
  • `chat_converse_omits_inference_config_when_no_knobs_set` — companion: empty knobs → no inferenceConfig field

LOWs (deferred)

  • LOW-1 (Role::Tool silently dropped): deliberate no-op pending structured tool-use surface on ChatFormat; documented in `build_converse_inputs`.
  • LOW-2 (per-publisher dispatch matrix coverage): match arm in chat() is `_ => chat_converse` so routing is correct by construction; existing Meta + Amazon Nova tests already cover the non-Anthropic dispatch contract.

Result

`cargo test -p aisix-provider-bedrock` → 45/45 PASS (was 41; +4 new audit-regression tests).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings` clean.

All HIGH/MEDIUM findings addressed. Awaiting fresh CI green.

@moonming
moonming merged commit 9ed3fb9 into mainMay 25, 2026
8 checks passed
@moonming
moonming deleted the feat/bedrock-converse branch May 25, 2026 00:25
Sign up for freeto 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

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(bedrock): wire Converse API for all publishers + Anthropic streaming (#302 Phase G Step 3 / D7.2.b/D7.3/D7.4) - #389

Merged
moonming merged 2 commits into
mainfrom
feat/bedrock-converse
May 25, 2026
Merged

feat(bedrock): wire Converse API for all publishers + Anthropic streaming (#302 Phase G Step 3 / D7.2.b/D7.3/D7.4)#389
moonming merged 2 commits into
mainfrom
feat/bedrock-converse

Conversation

@moonming

@moonmingmoonming commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Replaces per-publisher `/invoke` dispatch with the unified Converse API path for everything except Anthropic non-stream (which keeps its existing `/invoke` path for backward compat with operator deployments + e2e test fixtures pinned in #320). All other dispatch — Meta / Mistral / Cohere / Amazon Titan / Amazon Nova / AI21 — and all `chat_stream()` requests (including Anthropic) — now flow through the SDK's `.converse()` / `.converse_stream()`.

Closes D7.2.b (Anthropic streaming), D7.3 (Meta dispatch), and D7.4 (Mistral / Amazon Titan/Nova / Cohere / AI21 dispatch) per #302 Phase G's audit-corrected roadmap.

Why Converse vs. extending per-publisher `/invoke`

AWS introduced the Converse API exactly to solve the per-publisher body-shaping problem: one JSON envelope (`messages[].content[].text` + `system[]` + `inferenceConfig`), one URL pattern, one response shape — works for every Bedrock publisher. The legacy `/invoke` path requires a per-publisher body translator on the gateway side (Anthropic Messages, Meta Llama prompt template, Cohere command shape, etc.) — that multiplies maintenance burden as AWS adds publishers.

Eventstream framing

`/converse-stream` emits `application/vnd.amazon.eventstream` binary frames. The aws-sdk-bedrockruntime SDK owns the frame decoder — we don't need our own (compare with the mock-bedrock fixture in #480 which hand-rolled the encoder for the test mock). The bridge layer just walks the typed `ConverseStreamOutput` event sequence and maps each event to zero, one, or two `ChatChunk`s:

EventEmits
`MessageStart`role chunk (first emission only)
`ContentBlockDelta` (text)content chunk
`MessageStop`finish_reason chunk
`Metadata`usage chunk
`ContentBlockStart` / `ContentBlockStop` / tool-use deltasno chunk

Dispatch shape

```rust
// chat() — Anthropic legacy /invoke for backward compat; everyone else Converse
match publisher {
BedrockPublisher::Anthropic => self.chat_anthropic(req, ctx, upstream_id).await,
_ => self.chat_converse(req, ctx, upstream_id).await,
}

// chat_stream() — all publishers through Converse (legacy /invoke had no stream variant)
self.chat_converse_stream(req, ctx, upstream_id).await
```

Test changes

Deleted (5 tests, obsolete after Converse wiring):

  • chat_rejects_non_anthropic_publishers_with_publisher_named
  • chat_stream_returns_clear_not_implemented_error
  • chat_stream_anthropic_returns_d7_2_b_specific_error
  • chat_stream_non_anthropic_publisher_returns_d7_3_specific_error
  • chat_publisher_not_implemented_error_includes_model_id_and_publisher_name

Updated (1 test):

  • chat_ignores_req_model_and_uses_ctx_model_name — re-anchored to publisher-unknown error path (only publisher-resolution failures still produce Config errors before dispatch)

Added (5 tests):

  • `chat_meta_publisher_dispatches_via_converse_url` — `/model/meta.*/converse` regex + response decode
  • `chat_amazon_nova_publisher_dispatches_via_converse_url` — same contract, different publisher prefix (regression guard against per-publisher hard-coding)
  • `chat_stream_for_anthropic_dispatches_via_converse_stream_url` — `/converse-stream` pattern for Anthropic streaming
  • `chat_stream_for_meta_dispatches_via_converse_stream_url` — same for non-Anthropic streaming
  • `chat_converse_request_body_uses_text_content_block_shape` — pins outbound body: system → top-level array, user → typed text blocks (NOT Anthropic-style flat string), no top-level `model`

Test plan

  • `cargo test -p aisix-provider-bedrock` → 41/41 PASS
  • `cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings` clean
  • `cargo fmt --all` applied
  • CI
  • Independent audit per CLAUDE.md §8

References (CLAUDE.md §7)

Out of scope (deferred follow-ups)

  • D7.2.a Anthropic /invoke → Converse migration. Keeping legacy path until cp-api / dashboard test fixtures are updated to assert the Converse outbound body shape instead of the Anthropic Messages shape. Customer-visible ChatResponse is identical on both paths.
  • Tool-use / image / document `ContentBlock` variants. Chat scenarios only carry `Text` blocks today; the `Role::Tool` arm in `build_converse_inputs` is a deliberate no-op pending a structured tool-use surface on `ChatFormat`.

Unblocks

AC.10 in api7/AISIX-Cloud#302: "Bedrock 上 Claude / Llama 都能 stream". Both halves now done — Claude streaming via Converse, Llama via Converse (chat + stream). Live e2e against mock-bedrock (#480) is the next sub-step, separate PR.

Summary by CodeRabbit

  • New Features

    • Unified routing: non-Anthropic providers now use Bedrock's Converse endpoints for both chat and streaming; Anthropic non-streaming retains legacy path.
  • Improvements

    • Stronger model-id validation, improved error handling including retry propagation and sensitive-info redaction.
  • Documentation

    • Status/docs updated to reflect Phase G routing and provider behavior.
  • Tests

    • Tests updated to remove old error expectations and verify Converse routing and request shape.

Review Change Stack

…ming (#302 Phase G Step 3 / D7.2.b / D7.3 / D7.4)
Replaces per-publisher /invoke dispatch with the unified Converse
API path for everything except Anthropic non-stream (which keeps
its existing /invoke path for backward compat with operator
deployments + the e2e test fixtures pinned in #320). All other
dispatch — Meta / Mistral / Cohere / Amazon Titan / Amazon Nova /
AI21 — and ALL chat_stream() requests — including Anthropic —
now flow through the SDK's `.converse()` / `.converse_stream()`.
This closes D7.2.b (Anthropic streaming), D7.3 (Meta dispatch),
and D7.4 (Mistral / Amazon / Cohere / AI21 dispatch) per #302
Phase G's audit-corrected roadmap.
## Why Converse (vs. extending the per-publisher /invoke path)
AWS introduced the Converse API exactly to solve the per-publisher
body-shaping problem: one JSON envelope (`messages[].content[].text`
+ `system[]` + `inferenceConfig`), one URL pattern
(`/model/<id>/converse[-stream]`), one response shape, works for
every Bedrock publisher. The legacy /invoke path requires a
per-publisher body translator on the gateway side (Anthropic's
Messages, Meta's Llama prompt template, Cohere's command shape,
etc.) — multiplying maintenance burden as AWS adds publishers.
## Eventstream framing
`/converse-stream` emits `application/vnd.amazon.eventstream`
binary frames. The aws-sdk-bedrockruntime SDK owns the frame
decoder — we don't need our own (compare with the mock-bedrock
fixture I wrote in api7/AISIX-Cloud#480 which hand-rolled the
encoder for the test mock). The bridge layer just walks the
typed `ConverseStreamOutput` event sequence and maps each event
to zero, one, or two `ChatChunk`s:
- `MessageStart` → role chunk (first emission only)
- `ContentBlockDelta` (text) → content chunk
- `MessageStop` → finish_reason chunk
- `Metadata` → usage chunk
- `ContentBlockStart` / `ContentBlockStop` / tool-use deltas → no chunk
## Error classification
The new `map_aws_sdk_error_generic` helper preserves the existing
4xx-vs-5xx classification: ServiceError → `UpstreamStatus` carrying
the HTTP code; TimeoutError or deadline-elapsed → `Timeout`;
everything else → `Transport`. Mirrors the pattern in the legacy
`map_sdk_error` + the audit-corrected Vertex/Azure token-mint
classifications (#387, #388).
## Test changes
Tests deleted (obsolete after Converse wiring):
- chat_rejects_non_anthropic_publishers_with_publisher_named
- chat_stream_returns_clear_not_implemented_error
- chat_stream_anthropic_returns_d7_2_b_specific_error
- chat_stream_non_anthropic_publisher_returns_d7_3_specific_error
- chat_publisher_not_implemented_error_includes_model_id_and_publisher_name
Tests updated:
- chat_ignores_req_model_and_uses_ctx_model_name — re-anchored to
the publisher-unknown error path (only publisher-resolution
failures still produce Config errors before dispatch)
Tests added (5 new for the Converse path):
- chat_meta_publisher_dispatches_via_converse_url —
`/model/meta.*/converse` path regex + Bedrock-shape response decode
- chat_amazon_nova_publisher_dispatches_via_converse_url —
same dispatch contract, different publisher prefix (regression
guard against a future change that hard-codes per-publisher
routing inside chat_converse)
- chat_stream_for_anthropic_dispatches_via_converse_stream_url —
`/converse-stream` URL pattern pin for Anthropic streaming
(legacy /invoke had no stream variant)
- chat_stream_for_meta_dispatches_via_converse_stream_url —
same pattern for non-Anthropic streaming
- chat_converse_request_body_uses_text_content_block_shape —
pins outbound body shape: system messages lifted to top-level
`system[]`, user messages emit typed `content: [{text: "..."}]`
blocks (NOT Anthropic-style flat string), no top-level `model`
field
`cargo test -p aisix-provider-bedrock` → 41/41 PASS (was 36 with
5 deletions + 1 update + 5 additions).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings`
clean. `cargo fmt --all` applied.
## References (CLAUDE.md §7)
- Converse API spec:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html
- Converse stream events:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html
- aws-sdk-bedrockruntime 1.130.0 operation modules:
https://docs.rs/aws-sdk-bedrockruntime/1.130.0/aws_sdk_bedrockruntime/operation/
- mock-bedrock fixture (api7/AISIX-Cloud#480) uses the same
`vnd.amazon.eventstream` frame format for its encoder; the bridge
here consumes the SDK-decoded events rather than re-implementing
the decoder
## Out of scope (deferred follow-ups)
- D7.2.a Anthropic /invoke → Converse migration. Keeping legacy
path until cp-api / dashboard test fixtures are updated to assert
the Converse outbound body shape instead of the Anthropic
Messages shape. The customer-visible ChatResponse is identical
on both paths.
- Tool-use / image / document `ContentBlock` variants — chat scenarios
only carry `Text` blocks today; the `Role::Tool` arm in
`build_converse_inputs` is a deliberate no-op pending a structured
tool-use surface on `ChatFormat`.
## Unblocks
AC.10 in api7/AISIX-Cloud#302: "Bedrock 上 Claude / Llama 都能 stream".
Both halves are now done — Claude streaming via Converse, Llama
via Converse (chat + stream). Live e2e against mock-bedrock
(#480) is the next sub-step, separate PR.
CopilotAI review requested due to automatic review settings May 25, 2026 00:07
@coderabbitai

coderabbitaiBot commented May 25, 2026

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

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 0a0e3bea-4faf-4241-be55-b99b16ae84ca

📥 Commits

Reviewing files that changed from the base of the PR and between f082564 and b97829c.

📒 Files selected for processing (1)
  • crates/aisix-provider-bedrock/src/bridge.rs

📝 Walkthrough

Walkthrough

This PR completes Bedrock Phase G by unifying chat and streaming dispatch through the Bedrock Converse API for all non-Anthropic publishers, while preserving Anthropic non-streaming on the legacy invoke path. New translation, streaming, and error-mapping helpers support the Converse integration, with test coverage validating the correct routing and request body shapes.

Changes

Bedrock Phase G Converse Unification

Layer / File(s)Summary
Phase G Documentation and Dependencies
crates/aisix-provider-bedrock/src/lib.rs, crates/aisix-provider-bedrock/Cargo.toml
Crate documentation updates mark Phase G completion with checked items for unified Converse dispatch and legacy Anthropic invoke path. aisix-provider-anthropic dependency is reintroduced in Cargo.toml alongside routing comments.
Chat Dispatch Routing and Setup
crates/aisix-provider-bedrock/src/bridge.rs
Updated imports include Converse SDK types. chat() method routes Anthropic to chat_anthropic and all others to chat_converse; chat_stream() always uses unified chat_converse_stream. Doc comments clarify legacy /invoke path usage and a small helper is allowed dead code.
Non-streaming & Streaming Converse Implementation
crates/aisix-provider-bedrock/src/bridge.rs
Adds build_client_from_ctx, implements chat_converse and chat_converse_stream, translates request/response shapes (build_converse_inputs, converse_output_into_chat_response, map_stop_reason, emit_converse_chunk), and centralizes AWS SDK error mapping with Retry-After preservation and deadline-aware classification.
Test Updates for Phase G Converse Routing
crates/aisix-provider-bedrock/src/bridge.rs
Removed tests asserting old "not yet implemented" publisher/streaming errors. Added Phase G tests verify /converse and /converse-stream URL routing and Converse request body shape: typed content-block arrays, top-level system field, and absent top-level model field; updated publisher-resolution regression tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 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.

CopilotAI 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.

Pull request overview

This PR switches aisix-provider-bedrock from per-publisher /invoke dispatch to the unified Bedrock Converse API for all non-Anthropic chat() calls and for allchat_stream() calls (including Anthropic), while keeping Anthropic non-streaming on the legacy /invoke path for backwards compatibility.

Changes:

  • Route non-Anthropic chat() requests via POST /model/<id>/converse and implement response translation to ChatResponse.
  • Implement chat_stream() for all publishers via POST /model/<id>/converse-stream, mapping typed SDK events into ChatChunks.
  • Update unit tests and add wiremock-based dispatch/body-shape guards; add async-stream dependency.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.

FileDescription
crates/aisix-provider-bedrock/src/lib.rsUpdates crate-level docs to reflect Converse-based dispatch and streaming support.
crates/aisix-provider-bedrock/src/bridge.rsImplements Converse + ConverseStream dispatch paths, request/response translation, and associated tests.
crates/aisix-provider-bedrock/Cargo.tomlAdds async-stream and updates comments to match the new dispatch split.
Cargo.lockRecords the new async-stream dependency in the lockfile.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +785 to +792
// Tool-result messages are part of Anthropic's tool-use
// protocol; Bedrock Converse supports them via
// `ContentBlock::ToolResult` but the gateway's ChatMessage
// surface doesn't carry the structured tool_use_id +
// content shape needed to round-trip cleanly. Skip
// silently for now; a tool-use-aware follow-up PR can
// wire this when the upstream ChatFormat extends to
// carry the structured payload.
Comment on lines +936 to +945
/// Map the SDK's `ConverseError` SdkError variant to BridgeError.
/// Same classification rules as `map_sdk_error` (UpstreamStatus for
/// retryable upstream failures, Timeout on dispatch timeout).
fn map_converse_sdk_error(
e: SdkError<ConverseError, aws_smithy_runtime_api::http::Response>,
started: Instant,
deadline: Option<Duration>,
) -> BridgeError {
map_aws_sdk_error_generic(e.to_string(), &e, started, deadline)
}
Comment on lines +979 to +988
match err {
SdkError::ServiceError(svc) => {
let status = svc.raw().status().as_u16();
BridgeError::upstream_status(status, msg)
}
SdkError::TimeoutError(_) => BridgeError::Timeout {
elapsed_ms: started.elapsed().as_millis() as u64,
},
_ => BridgeError::Transport(msg),
}
// unset by default so the upstream model's own defaults apply.
// A follow-up override-pipeline PR can wire temperature /
// max_tokens from RequestOverrides.
let _ = InferenceConfiguration::builder();
Comment on lines +802 to +820
fn converse_output_into_chat_response(
resp: aws_sdk_bedrockruntime::operation::converse::ConverseOutput,
upstream_id: &str,
) -> ChatResponse {
let (text, finish) = match resp.output() {
Some(aws_sdk_bedrockruntime::types::ConverseOutput::Message(msg)) => {
let text: String = msg
.content()
.iter()
.filter_map(|cb| match cb {
ContentBlock::Text(t) => Some(t.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("");
(text, map_stop_reason(resp.stop_reason()))
}
_ => (String::new(), FinishReason::Stop),
};
…audit HIGH/MEDIUM on #389)
Independent audit-aigw-389-bedrock-converse flagged 2 HIGH + 2
MEDIUM bugs in the original Converse wiring. All fixed in this
commit; 45/45 tests pass.
## HIGH-1 + HIGH-2 — `map_aws_sdk_error_generic` silently
regressed the legacy /invoke path's error envelope hardening
The original helper collapsed all ServiceError responses to
`BridgeError::upstream_status(status, msg)`, a convenience
constructor that sets `wire: Unknown` + `parsed: None` +
`retry_after: None`. The legacy `chat_anthropic` /invoke path
uses `map_service_error` which builds a fully-shaped
`BridgeError::UpstreamStatus` with:
- wire: Bedrock — required by error_translate to render Bedrock-
shape errors back to OpenAI/Anthropic-shape clients
- retry_after: parsed from upstream Retry-After header — required
by the cooldown layer to honour AWS throttle hints
- parsed.kind: extracted via .meta().code() — distinguishes a
ThrottlingException 429 from a different throttle source
Without this fix every non-Anthropic publisher + all streaming
silently shipped degraded errors vs. the legacy path — broke the
PR #323 audit (MEDIUM-2) hardening it had just restored.
Fix: introduce `bedrock_service_error_to_upstream_status` (generic
over `E: ProvideErrorMetadata`) that mirrors `map_service_error`'s
field-by-field construction. Both Converse + ConverseStream errors
now flow through it.
## MEDIUM-1 — `e.to_string()` produced opaque "service error"
strings as the customer-visible message
Same root cause; resolved by the HIGH fix above. The customer-
visible `message` now uses the legacy path's canned status-keyed
phrase ("upstream rate limited", "upstream authentication failed",
etc.) — preserving the operator-ARN redaction the legacy path
also enforces.
## MEDIUM-2 — `chat_converse` silently dropped ChatFormat's
temperature/max_tokens/top_p
The original `chat_converse` constructed an
`InferenceConfiguration::builder()` and immediately discarded it
with `let _ =`. Every non-Anthropic Bedrock customer using these
knobs would see them vanish — real behavioural regression vs. the
legacy chat_anthropic path which forwards them via build_request →
AnthropicRequest.
Fix: new `build_inference_config(req)` helper extracts
temperature / max_tokens / top_p when set, returns None when all
absent (so we don't emit an empty `inferenceConfig: {}` which 400s
on some Bedrock publishers). Wired into both chat_converse and
chat_converse_stream.
## Tests added (5 new for the audit fixes)
- chat_converse_maps_upstream_429_with_retry_after_and_bedrock_wire
pins the HIGH-1+2 fix: 429 + Retry-After: 42 → UpstreamStatus
with status=429, message="upstream rate limited",
wire=Bedrock, retry_after=Some(42s), parsed.kind="ThrottlingException"
- chat_converse_maps_upstream_4xx_to_canned_message_with_bedrock_wire
pins HIGH-1 for the non-throttle path: 403 with leaky ARN body
→ canned "upstream authentication failed" (NO ARN leak),
wire=Bedrock, parsed.kind="AccessDeniedException"
- chat_converse_wires_temperature_max_tokens_top_p_into_inference_config
pins MEDIUM-2: ChatFormat knobs reach Bedrock's body as
camelCase {temperature, maxTokens, topP}
- chat_converse_omits_inference_config_when_no_knobs_set
companion: empty knobs → no inferenceConfig field on the wire
`cargo test -p aisix-provider-bedrock` → 45/45 PASS (was 41; +4
new audit-regression tests).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings`
clean.
## LOWs (non-blocking, deferred per CLAUDE.md §8)
- LOW-1: Role::Tool messages silently dropped on Converse path —
deliberate no-op pending a structured tool-use surface on
ChatFormat. Documented in `build_converse_inputs`.
- LOW-2: Per-publisher dispatch test matrix coverage (Mistral /
Cohere / AmazonTitan / AI21 not individually pinned). The match
arm in chat() is structurally uniform (`_ => chat_converse`) so
routing is correct by construction; the two existing
Meta + Amazon Nova tests already cover the non-Anthropic
dispatch contract.
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit response — all HIGH/MEDIUM addressed in commit `b97829c`

Independent audit-aigw-389-bedrock-converse returned 2 HIGH + 2 MEDIUM + 2 LOW. All HIGH/MEDIUM fixed in this commit; LOWs explicitly deferred per CLAUDE.md §8.

HIGH-1 + HIGH-2 ✅ fixed

The original `map_aws_sdk_error_generic` collapsed all ServiceError responses to `BridgeError::upstream_status(status, msg)` — a convenience constructor that sets `wire: Unknown`, `parsed: None`, `retry_after: None`. The legacy `/invoke` path's `map_service_error` (hardened by PR #323's MEDIUM-2 audit) builds full UpstreamStatus with `wire: Bedrock`, parsed AWS error code, and parsed Retry-After. The Converse path silently regressed this hardening for every non-Anthropic publisher + all streaming.

Fix: new `bedrock_service_error_to_upstream_status` helper (generic over `E: ProvideErrorMetadata`) mirrors `map_service_error` field-by-field. Both Converse + ConverseStream errors flow through it.

MEDIUM-1 ✅ fixed

Same root cause as HIGH-1; resolved by the same change. Customer-visible messages now use the canned status-keyed phrases ("upstream rate limited", "upstream authentication failed") — preserving the operator-ARN redaction the legacy path enforces.

MEDIUM-2 ✅ fixed

`chat_converse` silently dropped `ChatFormat.temperature` / `.max_tokens` / `.top_p` — real behavioural regression for non-Anthropic Bedrock customers. New `build_inference_config` helper extracts the knobs when set (returns None when all absent to avoid emitting empty `inferenceConfig: {}` which 400s on some publishers). Wired into both chat_converse and chat_converse_stream.

Test coverage added (4 new tests pinning the audit fixes)

  • `chat_converse_maps_upstream_429_with_retry_after_and_bedrock_wire` — pins HIGH-1+2: 429 + Retry-After: 42 → UpstreamStatus with status=429, message="upstream rate limited", wire=Bedrock, retry_after=Some(42s), parsed.kind="ThrottlingException"
  • `chat_converse_maps_upstream_4xx_to_canned_message_with_bedrock_wire` — pins HIGH-1 for non-throttle: 403 with leaky ARN body → canned "upstream authentication failed" (NO ARN leak), wire=Bedrock, parsed.kind="AccessDeniedException"
  • `chat_converse_wires_temperature_max_tokens_top_p_into_inference_config` — pins MEDIUM-2: ChatFormat knobs → camelCase `{temperature, maxTokens, topP}` on the wire
  • `chat_converse_omits_inference_config_when_no_knobs_set` — companion: empty knobs → no inferenceConfig field

LOWs (deferred)

  • LOW-1 (Role::Tool silently dropped): deliberate no-op pending structured tool-use surface on ChatFormat; documented in `build_converse_inputs`.
  • LOW-2 (per-publisher dispatch matrix coverage): match arm in chat() is `_ => chat_converse` so routing is correct by construction; existing Meta + Amazon Nova tests already cover the non-Anthropic dispatch contract.

Result

`cargo test -p aisix-provider-bedrock` → 45/45 PASS (was 41; +4 new audit-regression tests).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings` clean.

All HIGH/MEDIUM findings addressed. Awaiting fresh CI green.

@moonming
moonming merged commit 9ed3fb9 into mainMay 25, 2026
8 checks passed
@moonming
moonming deleted the feat/bedrock-converse branch May 25, 2026 00:25
Sign up for freeto 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

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(bedrock): wire Converse API for all publishers + Anthropic streaming (#302 Phase G Step 3 / D7.2.b/D7.3/D7.4) - #389

Merged
moonming merged 2 commits into
mainfrom
feat/bedrock-converse
May 25, 2026
Merged

feat(bedrock): wire Converse API for all publishers + Anthropic streaming (#302 Phase G Step 3 / D7.2.b/D7.3/D7.4)#389
moonming merged 2 commits into
mainfrom
feat/bedrock-converse

Conversation

@moonming

@moonmingmoonming commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Replaces per-publisher `/invoke` dispatch with the unified Converse API path for everything except Anthropic non-stream (which keeps its existing `/invoke` path for backward compat with operator deployments + e2e test fixtures pinned in #320). All other dispatch — Meta / Mistral / Cohere / Amazon Titan / Amazon Nova / AI21 — and all `chat_stream()` requests (including Anthropic) — now flow through the SDK's `.converse()` / `.converse_stream()`.

Closes D7.2.b (Anthropic streaming), D7.3 (Meta dispatch), and D7.4 (Mistral / Amazon Titan/Nova / Cohere / AI21 dispatch) per #302 Phase G's audit-corrected roadmap.

Why Converse vs. extending per-publisher `/invoke`

AWS introduced the Converse API exactly to solve the per-publisher body-shaping problem: one JSON envelope (`messages[].content[].text` + `system[]` + `inferenceConfig`), one URL pattern, one response shape — works for every Bedrock publisher. The legacy `/invoke` path requires a per-publisher body translator on the gateway side (Anthropic Messages, Meta Llama prompt template, Cohere command shape, etc.) — that multiplies maintenance burden as AWS adds publishers.

Eventstream framing

`/converse-stream` emits `application/vnd.amazon.eventstream` binary frames. The aws-sdk-bedrockruntime SDK owns the frame decoder — we don't need our own (compare with the mock-bedrock fixture in #480 which hand-rolled the encoder for the test mock). The bridge layer just walks the typed `ConverseStreamOutput` event sequence and maps each event to zero, one, or two `ChatChunk`s:

EventEmits
`MessageStart`role chunk (first emission only)
`ContentBlockDelta` (text)content chunk
`MessageStop`finish_reason chunk
`Metadata`usage chunk
`ContentBlockStart` / `ContentBlockStop` / tool-use deltasno chunk

Dispatch shape

```rust
// chat() — Anthropic legacy /invoke for backward compat; everyone else Converse
match publisher {
BedrockPublisher::Anthropic => self.chat_anthropic(req, ctx, upstream_id).await,
_ => self.chat_converse(req, ctx, upstream_id).await,
}

// chat_stream() — all publishers through Converse (legacy /invoke had no stream variant)
self.chat_converse_stream(req, ctx, upstream_id).await
```

Test changes

Deleted (5 tests, obsolete after Converse wiring):

  • chat_rejects_non_anthropic_publishers_with_publisher_named
  • chat_stream_returns_clear_not_implemented_error
  • chat_stream_anthropic_returns_d7_2_b_specific_error
  • chat_stream_non_anthropic_publisher_returns_d7_3_specific_error
  • chat_publisher_not_implemented_error_includes_model_id_and_publisher_name

Updated (1 test):

  • chat_ignores_req_model_and_uses_ctx_model_name — re-anchored to publisher-unknown error path (only publisher-resolution failures still produce Config errors before dispatch)

Added (5 tests):

  • `chat_meta_publisher_dispatches_via_converse_url` — `/model/meta.*/converse` regex + response decode
  • `chat_amazon_nova_publisher_dispatches_via_converse_url` — same contract, different publisher prefix (regression guard against per-publisher hard-coding)
  • `chat_stream_for_anthropic_dispatches_via_converse_stream_url` — `/converse-stream` pattern for Anthropic streaming
  • `chat_stream_for_meta_dispatches_via_converse_stream_url` — same for non-Anthropic streaming
  • `chat_converse_request_body_uses_text_content_block_shape` — pins outbound body: system → top-level array, user → typed text blocks (NOT Anthropic-style flat string), no top-level `model`

Test plan

  • `cargo test -p aisix-provider-bedrock` → 41/41 PASS
  • `cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings` clean
  • `cargo fmt --all` applied
  • CI
  • Independent audit per CLAUDE.md §8

References (CLAUDE.md §7)

Out of scope (deferred follow-ups)

  • D7.2.a Anthropic /invoke → Converse migration. Keeping legacy path until cp-api / dashboard test fixtures are updated to assert the Converse outbound body shape instead of the Anthropic Messages shape. Customer-visible ChatResponse is identical on both paths.
  • Tool-use / image / document `ContentBlock` variants. Chat scenarios only carry `Text` blocks today; the `Role::Tool` arm in `build_converse_inputs` is a deliberate no-op pending a structured tool-use surface on `ChatFormat`.

Unblocks

AC.10 in api7/AISIX-Cloud#302: "Bedrock 上 Claude / Llama 都能 stream". Both halves now done — Claude streaming via Converse, Llama via Converse (chat + stream). Live e2e against mock-bedrock (#480) is the next sub-step, separate PR.

Summary by CodeRabbit

  • New Features

    • Unified routing: non-Anthropic providers now use Bedrock's Converse endpoints for both chat and streaming; Anthropic non-streaming retains legacy path.
  • Improvements

    • Stronger model-id validation, improved error handling including retry propagation and sensitive-info redaction.
  • Documentation

    • Status/docs updated to reflect Phase G routing and provider behavior.
  • Tests

    • Tests updated to remove old error expectations and verify Converse routing and request shape.

Review Change Stack

…ming (#302 Phase G Step 3 / D7.2.b / D7.3 / D7.4)
Replaces per-publisher /invoke dispatch with the unified Converse
API path for everything except Anthropic non-stream (which keeps
its existing /invoke path for backward compat with operator
deployments + the e2e test fixtures pinned in #320). All other
dispatch — Meta / Mistral / Cohere / Amazon Titan / Amazon Nova /
AI21 — and ALL chat_stream() requests — including Anthropic —
now flow through the SDK's `.converse()` / `.converse_stream()`.
This closes D7.2.b (Anthropic streaming), D7.3 (Meta dispatch),
and D7.4 (Mistral / Amazon / Cohere / AI21 dispatch) per #302
Phase G's audit-corrected roadmap.
## Why Converse (vs. extending the per-publisher /invoke path)
AWS introduced the Converse API exactly to solve the per-publisher
body-shaping problem: one JSON envelope (`messages[].content[].text`
+ `system[]` + `inferenceConfig`), one URL pattern
(`/model/<id>/converse[-stream]`), one response shape, works for
every Bedrock publisher. The legacy /invoke path requires a
per-publisher body translator on the gateway side (Anthropic's
Messages, Meta's Llama prompt template, Cohere's command shape,
etc.) — multiplying maintenance burden as AWS adds publishers.
## Eventstream framing
`/converse-stream` emits `application/vnd.amazon.eventstream`
binary frames. The aws-sdk-bedrockruntime SDK owns the frame
decoder — we don't need our own (compare with the mock-bedrock
fixture I wrote in api7/AISIX-Cloud#480 which hand-rolled the
encoder for the test mock). The bridge layer just walks the
typed `ConverseStreamOutput` event sequence and maps each event
to zero, one, or two `ChatChunk`s:
- `MessageStart` → role chunk (first emission only)
- `ContentBlockDelta` (text) → content chunk
- `MessageStop` → finish_reason chunk
- `Metadata` → usage chunk
- `ContentBlockStart` / `ContentBlockStop` / tool-use deltas → no chunk
## Error classification
The new `map_aws_sdk_error_generic` helper preserves the existing
4xx-vs-5xx classification: ServiceError → `UpstreamStatus` carrying
the HTTP code; TimeoutError or deadline-elapsed → `Timeout`;
everything else → `Transport`. Mirrors the pattern in the legacy
`map_sdk_error` + the audit-corrected Vertex/Azure token-mint
classifications (#387, #388).
## Test changes
Tests deleted (obsolete after Converse wiring):
- chat_rejects_non_anthropic_publishers_with_publisher_named
- chat_stream_returns_clear_not_implemented_error
- chat_stream_anthropic_returns_d7_2_b_specific_error
- chat_stream_non_anthropic_publisher_returns_d7_3_specific_error
- chat_publisher_not_implemented_error_includes_model_id_and_publisher_name
Tests updated:
- chat_ignores_req_model_and_uses_ctx_model_name — re-anchored to
the publisher-unknown error path (only publisher-resolution
failures still produce Config errors before dispatch)
Tests added (5 new for the Converse path):
- chat_meta_publisher_dispatches_via_converse_url —
`/model/meta.*/converse` path regex + Bedrock-shape response decode
- chat_amazon_nova_publisher_dispatches_via_converse_url —
same dispatch contract, different publisher prefix (regression
guard against a future change that hard-codes per-publisher
routing inside chat_converse)
- chat_stream_for_anthropic_dispatches_via_converse_stream_url —
`/converse-stream` URL pattern pin for Anthropic streaming
(legacy /invoke had no stream variant)
- chat_stream_for_meta_dispatches_via_converse_stream_url —
same pattern for non-Anthropic streaming
- chat_converse_request_body_uses_text_content_block_shape —
pins outbound body shape: system messages lifted to top-level
`system[]`, user messages emit typed `content: [{text: "..."}]`
blocks (NOT Anthropic-style flat string), no top-level `model`
field
`cargo test -p aisix-provider-bedrock` → 41/41 PASS (was 36 with
5 deletions + 1 update + 5 additions).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings`
clean. `cargo fmt --all` applied.
## References (CLAUDE.md §7)
- Converse API spec:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html
- Converse stream events:
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html
- aws-sdk-bedrockruntime 1.130.0 operation modules:
https://docs.rs/aws-sdk-bedrockruntime/1.130.0/aws_sdk_bedrockruntime/operation/
- mock-bedrock fixture (api7/AISIX-Cloud#480) uses the same
`vnd.amazon.eventstream` frame format for its encoder; the bridge
here consumes the SDK-decoded events rather than re-implementing
the decoder
## Out of scope (deferred follow-ups)
- D7.2.a Anthropic /invoke → Converse migration. Keeping legacy
path until cp-api / dashboard test fixtures are updated to assert
the Converse outbound body shape instead of the Anthropic
Messages shape. The customer-visible ChatResponse is identical
on both paths.
- Tool-use / image / document `ContentBlock` variants — chat scenarios
only carry `Text` blocks today; the `Role::Tool` arm in
`build_converse_inputs` is a deliberate no-op pending a structured
tool-use surface on `ChatFormat`.
## Unblocks
AC.10 in api7/AISIX-Cloud#302: "Bedrock 上 Claude / Llama 都能 stream".
Both halves are now done — Claude streaming via Converse, Llama
via Converse (chat + stream). Live e2e against mock-bedrock
(#480) is the next sub-step, separate PR.
CopilotAI review requested due to automatic review settings May 25, 2026 00:07
@coderabbitai

coderabbitaiBot commented May 25, 2026

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

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 0a0e3bea-4faf-4241-be55-b99b16ae84ca

📥 Commits

Reviewing files that changed from the base of the PR and between f082564 and b97829c.

📒 Files selected for processing (1)
  • crates/aisix-provider-bedrock/src/bridge.rs

📝 Walkthrough

Walkthrough

This PR completes Bedrock Phase G by unifying chat and streaming dispatch through the Bedrock Converse API for all non-Anthropic publishers, while preserving Anthropic non-streaming on the legacy invoke path. New translation, streaming, and error-mapping helpers support the Converse integration, with test coverage validating the correct routing and request body shapes.

Changes

Bedrock Phase G Converse Unification

Layer / File(s)Summary
Phase G Documentation and Dependencies
crates/aisix-provider-bedrock/src/lib.rs, crates/aisix-provider-bedrock/Cargo.toml
Crate documentation updates mark Phase G completion with checked items for unified Converse dispatch and legacy Anthropic invoke path. aisix-provider-anthropic dependency is reintroduced in Cargo.toml alongside routing comments.
Chat Dispatch Routing and Setup
crates/aisix-provider-bedrock/src/bridge.rs
Updated imports include Converse SDK types. chat() method routes Anthropic to chat_anthropic and all others to chat_converse; chat_stream() always uses unified chat_converse_stream. Doc comments clarify legacy /invoke path usage and a small helper is allowed dead code.
Non-streaming & Streaming Converse Implementation
crates/aisix-provider-bedrock/src/bridge.rs
Adds build_client_from_ctx, implements chat_converse and chat_converse_stream, translates request/response shapes (build_converse_inputs, converse_output_into_chat_response, map_stop_reason, emit_converse_chunk), and centralizes AWS SDK error mapping with Retry-After preservation and deadline-aware classification.
Test Updates for Phase G Converse Routing
crates/aisix-provider-bedrock/src/bridge.rs
Removed tests asserting old "not yet implemented" publisher/streaming errors. Added Phase G tests verify /converse and /converse-stream URL routing and Converse request body shape: typed content-block arrays, top-level system field, and absent top-level model field; updated publisher-resolution regression tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 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.

CopilotAI 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.

Pull request overview

This PR switches aisix-provider-bedrock from per-publisher /invoke dispatch to the unified Bedrock Converse API for all non-Anthropic chat() calls and for allchat_stream() calls (including Anthropic), while keeping Anthropic non-streaming on the legacy /invoke path for backwards compatibility.

Changes:

  • Route non-Anthropic chat() requests via POST /model/<id>/converse and implement response translation to ChatResponse.
  • Implement chat_stream() for all publishers via POST /model/<id>/converse-stream, mapping typed SDK events into ChatChunks.
  • Update unit tests and add wiremock-based dispatch/body-shape guards; add async-stream dependency.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.

FileDescription
crates/aisix-provider-bedrock/src/lib.rsUpdates crate-level docs to reflect Converse-based dispatch and streaming support.
crates/aisix-provider-bedrock/src/bridge.rsImplements Converse + ConverseStream dispatch paths, request/response translation, and associated tests.
crates/aisix-provider-bedrock/Cargo.tomlAdds async-stream and updates comments to match the new dispatch split.
Cargo.lockRecords the new async-stream dependency in the lockfile.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +785 to +792
// Tool-result messages are part of Anthropic's tool-use
// protocol; Bedrock Converse supports them via
// `ContentBlock::ToolResult` but the gateway's ChatMessage
// surface doesn't carry the structured tool_use_id +
// content shape needed to round-trip cleanly. Skip
// silently for now; a tool-use-aware follow-up PR can
// wire this when the upstream ChatFormat extends to
// carry the structured payload.
Comment on lines +936 to +945
/// Map the SDK's `ConverseError` SdkError variant to BridgeError.
/// Same classification rules as `map_sdk_error` (UpstreamStatus for
/// retryable upstream failures, Timeout on dispatch timeout).
fn map_converse_sdk_error(
e: SdkError<ConverseError, aws_smithy_runtime_api::http::Response>,
started: Instant,
deadline: Option<Duration>,
) -> BridgeError {
map_aws_sdk_error_generic(e.to_string(), &e, started, deadline)
}
Comment on lines +979 to +988
match err {
SdkError::ServiceError(svc) => {
let status = svc.raw().status().as_u16();
BridgeError::upstream_status(status, msg)
}
SdkError::TimeoutError(_) => BridgeError::Timeout {
elapsed_ms: started.elapsed().as_millis() as u64,
},
_ => BridgeError::Transport(msg),
}
// unset by default so the upstream model's own defaults apply.
// A follow-up override-pipeline PR can wire temperature /
// max_tokens from RequestOverrides.
let _ = InferenceConfiguration::builder();
Comment on lines +802 to +820
fn converse_output_into_chat_response(
resp: aws_sdk_bedrockruntime::operation::converse::ConverseOutput,
upstream_id: &str,
) -> ChatResponse {
let (text, finish) = match resp.output() {
Some(aws_sdk_bedrockruntime::types::ConverseOutput::Message(msg)) => {
let text: String = msg
.content()
.iter()
.filter_map(|cb| match cb {
ContentBlock::Text(t) => Some(t.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("");
(text, map_stop_reason(resp.stop_reason()))
}
_ => (String::new(), FinishReason::Stop),
};
…audit HIGH/MEDIUM on #389)
Independent audit-aigw-389-bedrock-converse flagged 2 HIGH + 2
MEDIUM bugs in the original Converse wiring. All fixed in this
commit; 45/45 tests pass.
## HIGH-1 + HIGH-2 — `map_aws_sdk_error_generic` silently
regressed the legacy /invoke path's error envelope hardening
The original helper collapsed all ServiceError responses to
`BridgeError::upstream_status(status, msg)`, a convenience
constructor that sets `wire: Unknown` + `parsed: None` +
`retry_after: None`. The legacy `chat_anthropic` /invoke path
uses `map_service_error` which builds a fully-shaped
`BridgeError::UpstreamStatus` with:
- wire: Bedrock — required by error_translate to render Bedrock-
shape errors back to OpenAI/Anthropic-shape clients
- retry_after: parsed from upstream Retry-After header — required
by the cooldown layer to honour AWS throttle hints
- parsed.kind: extracted via .meta().code() — distinguishes a
ThrottlingException 429 from a different throttle source
Without this fix every non-Anthropic publisher + all streaming
silently shipped degraded errors vs. the legacy path — broke the
PR #323 audit (MEDIUM-2) hardening it had just restored.
Fix: introduce `bedrock_service_error_to_upstream_status` (generic
over `E: ProvideErrorMetadata`) that mirrors `map_service_error`'s
field-by-field construction. Both Converse + ConverseStream errors
now flow through it.
## MEDIUM-1 — `e.to_string()` produced opaque "service error"
strings as the customer-visible message
Same root cause; resolved by the HIGH fix above. The customer-
visible `message` now uses the legacy path's canned status-keyed
phrase ("upstream rate limited", "upstream authentication failed",
etc.) — preserving the operator-ARN redaction the legacy path
also enforces.
## MEDIUM-2 — `chat_converse` silently dropped ChatFormat's
temperature/max_tokens/top_p
The original `chat_converse` constructed an
`InferenceConfiguration::builder()` and immediately discarded it
with `let _ =`. Every non-Anthropic Bedrock customer using these
knobs would see them vanish — real behavioural regression vs. the
legacy chat_anthropic path which forwards them via build_request →
AnthropicRequest.
Fix: new `build_inference_config(req)` helper extracts
temperature / max_tokens / top_p when set, returns None when all
absent (so we don't emit an empty `inferenceConfig: {}` which 400s
on some Bedrock publishers). Wired into both chat_converse and
chat_converse_stream.
## Tests added (5 new for the audit fixes)
- chat_converse_maps_upstream_429_with_retry_after_and_bedrock_wire
pins the HIGH-1+2 fix: 429 + Retry-After: 42 → UpstreamStatus
with status=429, message="upstream rate limited",
wire=Bedrock, retry_after=Some(42s), parsed.kind="ThrottlingException"
- chat_converse_maps_upstream_4xx_to_canned_message_with_bedrock_wire
pins HIGH-1 for the non-throttle path: 403 with leaky ARN body
→ canned "upstream authentication failed" (NO ARN leak),
wire=Bedrock, parsed.kind="AccessDeniedException"
- chat_converse_wires_temperature_max_tokens_top_p_into_inference_config
pins MEDIUM-2: ChatFormat knobs reach Bedrock's body as
camelCase {temperature, maxTokens, topP}
- chat_converse_omits_inference_config_when_no_knobs_set
companion: empty knobs → no inferenceConfig field on the wire
`cargo test -p aisix-provider-bedrock` → 45/45 PASS (was 41; +4
new audit-regression tests).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings`
clean.
## LOWs (non-blocking, deferred per CLAUDE.md §8)
- LOW-1: Role::Tool messages silently dropped on Converse path —
deliberate no-op pending a structured tool-use surface on
ChatFormat. Documented in `build_converse_inputs`.
- LOW-2: Per-publisher dispatch test matrix coverage (Mistral /
Cohere / AmazonTitan / AI21 not individually pinned). The match
arm in chat() is structurally uniform (`_ => chat_converse`) so
routing is correct by construction; the two existing
Meta + Amazon Nova tests already cover the non-Anthropic
dispatch contract.
@moonming

Copy link
Copy Markdown
MemberAuthor

Audit response — all HIGH/MEDIUM addressed in commit `b97829c`

Independent audit-aigw-389-bedrock-converse returned 2 HIGH + 2 MEDIUM + 2 LOW. All HIGH/MEDIUM fixed in this commit; LOWs explicitly deferred per CLAUDE.md §8.

HIGH-1 + HIGH-2 ✅ fixed

The original `map_aws_sdk_error_generic` collapsed all ServiceError responses to `BridgeError::upstream_status(status, msg)` — a convenience constructor that sets `wire: Unknown`, `parsed: None`, `retry_after: None`. The legacy `/invoke` path's `map_service_error` (hardened by PR #323's MEDIUM-2 audit) builds full UpstreamStatus with `wire: Bedrock`, parsed AWS error code, and parsed Retry-After. The Converse path silently regressed this hardening for every non-Anthropic publisher + all streaming.

Fix: new `bedrock_service_error_to_upstream_status` helper (generic over `E: ProvideErrorMetadata`) mirrors `map_service_error` field-by-field. Both Converse + ConverseStream errors flow through it.

MEDIUM-1 ✅ fixed

Same root cause as HIGH-1; resolved by the same change. Customer-visible messages now use the canned status-keyed phrases ("upstream rate limited", "upstream authentication failed") — preserving the operator-ARN redaction the legacy path enforces.

MEDIUM-2 ✅ fixed

`chat_converse` silently dropped `ChatFormat.temperature` / `.max_tokens` / `.top_p` — real behavioural regression for non-Anthropic Bedrock customers. New `build_inference_config` helper extracts the knobs when set (returns None when all absent to avoid emitting empty `inferenceConfig: {}` which 400s on some publishers). Wired into both chat_converse and chat_converse_stream.

Test coverage added (4 new tests pinning the audit fixes)

  • `chat_converse_maps_upstream_429_with_retry_after_and_bedrock_wire` — pins HIGH-1+2: 429 + Retry-After: 42 → UpstreamStatus with status=429, message="upstream rate limited", wire=Bedrock, retry_after=Some(42s), parsed.kind="ThrottlingException"
  • `chat_converse_maps_upstream_4xx_to_canned_message_with_bedrock_wire` — pins HIGH-1 for non-throttle: 403 with leaky ARN body → canned "upstream authentication failed" (NO ARN leak), wire=Bedrock, parsed.kind="AccessDeniedException"
  • `chat_converse_wires_temperature_max_tokens_top_p_into_inference_config` — pins MEDIUM-2: ChatFormat knobs → camelCase `{temperature, maxTokens, topP}` on the wire
  • `chat_converse_omits_inference_config_when_no_knobs_set` — companion: empty knobs → no inferenceConfig field

LOWs (deferred)

  • LOW-1 (Role::Tool silently dropped): deliberate no-op pending structured tool-use surface on ChatFormat; documented in `build_converse_inputs`.
  • LOW-2 (per-publisher dispatch matrix coverage): match arm in chat() is `_ => chat_converse` so routing is correct by construction; existing Meta + Amazon Nova tests already cover the non-Anthropic dispatch contract.

Result

`cargo test -p aisix-provider-bedrock` → 45/45 PASS (was 41; +4 new audit-regression tests).
`cargo clippy -p aisix-provider-bedrock --all-targets -- -D warnings` clean.

All HIGH/MEDIUM findings addressed. Awaiting fresh CI green.

@moonming
moonming merged commit 9ed3fb9 into mainMay 25, 2026
8 checks passed
@moonming
moonming deleted the feat/bedrock-converse branch May 25, 2026 00:25
Sign up for freeto 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

@moonming