Skip to content

feat(bedrock,vertex): serve /v1/messages to Claude natively on Bedrock and Vertex AI - #1318

Merged
jarvis9443 merged 7 commits into
mainfrom
feat/bedrock-vertex-native-messages
Oct 20, 2026
Merged

jarvis9443 merged 7 commits into
mainfrom
feat/bedrock-vertex-native-messages

Conversation

@jarvis9443

@jarvis9443 jarvis9443 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

/v1/messages and /v1/messages/count_tokens addressed to Claude on Amazon Bedrock or Vertex AI now go to the platform's own Anthropic Messages routes with the caller's body, instead of being parsed into the chat form and rebuilt for InvokeModel (non-stream) or ConverseStream (stream). That translation was lossy for agent clients such as Claude Code: every thinking block came back with signature: "" and no signature_delta (so replayed thinking got stripped and the model lost its own reasoning each tool loop, which also invalidated message-level prompt-cache breakpoints), an image inside a tool_result never reached the model, streaming message_start usage was 0/0 with no cache fields, the streamed message id was gateway-made and no provider request id was recorded for streams, metadata.user_id/stop_sequences leaked as unknown keys, every anthropic-beta value was dropped, context_management/is_error/citations were dropped, several stop reasons collapsed to end_turn, and count_tokens answered 400.

What is sent now:

  • Bedrock: InvokeModel / InvokeModelWithResponseStream (/model/{id}/invoke, /invoke-with-response-stream) and CountTokens (/model/{base id}/count-tokens, invokeModel input when the body carries typed content blocks, converse input when every message is a plain string).
  • Vertex AI: :rawPredict / :streamRawPredict on publishers/anthropic/models/{id}, and publishers/anthropic/models/count-tokens:rawPredict with the version suffix (@…) dropped from model.
  • count_tokens bodies get the same adjustments as the Messages body toward that platform (field allowlist, metadata, cache_control); the CountTokens invokeModel body carries a max_tokens above any thinking budget (it does not change the count). A platform refusing to count a model moves a routing group on to its next Anthropic-wire target instead of ending the request.
  • Bedrock count_tokens falls back to Bedrock Mantle for a Claude model bedrock-runtime's CountTokens refuses (400/404), such as models Bedrock serves only through cross-region inference (Claude Opus 5.5): POST {mantle}/anthropic/v1/messages/count_tokens with the base model id, messages/system/tools and the thinking/tool_choice/output_config options, anthropic-version: 2023-06-01, signed SigV4 (service bedrock) from the provider key's access-key credentials. New startup key upstream.bedrock.mantle_endpoint (default null: the public regional host https://bedrock-mantle.{region}.api.aws from the provider key's region) for VPC-endpoint / GovCloud deployments; the chart declares it in feat(aisix): declare upstream.media_fetch, upstream.bedrock and outbound.ssrf_protection in values.yaml api7-helm-chart#409.
  • Which targets: the bedrock adapter with an anthropic. model id (region / inference-profile prefix allowed) or an ARN naming a Claude model, and the vertex adapter with a claude-* model. An ARN that hides its model (application inference profile) keeps the translation. /v1/chat/completions and /v1/responses are unchanged.

What the body goes through on the way, and nothing else:

  • model leaves the body; anthropic_version is bedrock-2023-05-31 / vertex-2023-10-16; stream is removed on Bedrock and kept on Vertex AI.
  • Top-level fields are filtered to what the platform accepts. Bedrock: anthropic_version, anthropic_beta, max_tokens, messages, system, stop_sequences, temperature, top_p, top_k, tools, tool_choice, thinking, metadata, output_config, safeguards, context_management. Vertex AI: anthropic_version, max_tokens, messages, metadata, stop_sequences, stream, system, temperature, thinking, tool_choice, tools, top_k, top_p, inference_geo, mcp_servers, context_management, compaction, container, output_config, speed, cache_control, safeguards. A legacy top-level output_format moves into output_config.format.
  • metadata keeps user_id only.
  • anthropic-beta values (the caller's header, plus ones the body's own features need) are filtered to each platform's known set and mapped where the platform names them differently (advanced-tool-use-2025-11-20 → tool-search-tool-2025-10-19). Bedrock reads them from the body's anthropic_beta; Vertex AI from the anthropic-beta header, which is filtered the same way whatever set it. Bedrock gets effort-2025-11-24 added when output_config.effort is set on a family before Claude 4.6, tool-search-tool-2025-10-19 when a tool-search tool is present, and its context_management.edits filtered to the ones InvokeModel runs (compact_20260112, clear_tool_uses_20250919, each with its beta; the field is dropped when none is left). Vertex AI gets the context-management, web-search, tool-search, safeguards and per-turn-control betas when the body uses those features.
  • cache_control keeps type and ttl on Bedrock, drops scope on Vertex AI.
  • Media given by URL (including inside tool_result) is downloaded and inlined through upstream.media_fetch (same SSRF, size and per-request limits); with media fetching off, a URL source is a 400.
  • The billing-attribution system line and unsigned thinking blocks are stripped, as for every non-first-party / signature-verifying upstream.
  • A Bedrock request with no turn is still refused locally with the same 400 the chat rails give.
  • A structured-output request to a Claude family without native structured output (before 4.5) keeps the chat translation, whose synthetic-tool route is what makes it work there.

The answer is relayed in the Anthropic wire through the same passthrough pipeline Anthropic's own API uses, so guardrails on request and streamed response, the usage event and cost (including cache read / creation), the access log, the provider request id (the upstream message id, stream and non-stream), model-name projection, failover before the first byte and mid-stream handling all hold. Bedrock's event stream is re-framed as Anthropic SSE; an exception Bedrock raises inside it becomes an Anthropic error event with an Anthropic error type (throttlingException → rate_limit_error, serviceUnavailableException → overloaded_error, modelStreamErrorException by its original status, internalServerException → api_error, validationException → invalid_request_error, modelTimeoutException → timeout_error), so an Anthropic client's retry logic reads it. An exception raised before any content fails the attempt instead, so routing can fail over. Errors carry the upstream's own message text, as every Bedrock and Vertex AI error does since #1319 (a status phrase only when the upstream sent none).

Behavior change for existing callers: a /v1/messages caller on Claude-on-Bedrock/Vertex now gets the platform's response verbatim (signatures, redacted_thinking, upstream message id, native stop reasons, amazon-bedrock-invocationMetrics on Bedrock's message_stop), forwarded anthropic-beta/metadata.user_id/context_management, and a working count_tokens. Nothing has to be edited in configuration.

Tests: claude-platform-native-messages-e2e drives mock Bedrock (InvokeModel, event stream incl. an in-band throttlingException, CountTokens) and mock Vertex AI upstreams and asserts the outbound body/headers and the client-visible output: signatures both directions (non-stream + stream), the image inside a tool result, message_start usage with cache counters, upstream message id and provider request id on the usage event, beta filtering, field allowlist, metadata, URL image inlining, count_tokens on both, the throttle mapped to rate_limit_error, and an output guardrail on the passthrough stream. All of them fail with the platform branch disabled. claude-platform-prompt-caching, bedrock-converse-stream-reasoning and anthropic-stream-tool-calls are updated for the streamed /v1/messages cases that pinned ConverseStream / the translated stream.

🤖 Generated with Claude Code

Fixes api7/AISIX-Cloud#1885

…k and Vertex AI

/v1/messages and /v1/messages/count_tokens addressed to Claude on Amazon
Bedrock or Vertex AI no longer go through the chat translation. The
caller's Anthropic body goes to Bedrock InvokeModel /
InvokeModelWithResponseStream / CountTokens and to Vertex AI rawPredict /
streamRawPredict / the count-tokens model, and the answer comes back in
the Anthropic wire: thinking signatures (and signature_delta),
redacted_thinking, images inside tool_result, stop reasons, message_start
usage with the cache counters, and the upstream message id.

On the way the body is adapted per platform: model leaves the body,
the platform's anthropic_version goes in, top-level fields are filtered to
what the platform accepts, metadata keeps user_id only, anthropic-beta
values are filtered to the platform's known set (body anthropic_beta on
Bedrock, header on Vertex AI), the effort beta is added before Claude 4.6,
Bedrock context_management edits are filtered to the ones it runs,
cache_control keeps type+ttl (Bedrock) / drops scope (Vertex), and media
URLs are downloaded through upstream.media_fetch. A Bedrock stream
exception surfaces as an Anthropic error event (rate_limit_error,
overloaded_error, api_error, ...); one raised before any content fails the
attempt so routing can fail over.

The shared passthrough pipeline (guardrails, usage event, cost, access
log, model echo, failover) is reused.
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

This review includes 28 billable files and costs up to $7.00.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Or wait 6 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 3b52fdef-fccb-474d-bf38-695b11d6802a
📥 Commits

Reviewing files that changed from the base of the PR and between b1d3251 and 888c20c.

⛔ Files ignored due to path filters: 1
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing: 28
  • config.example.yaml
  • config.managed.yaml
  • config.reference.json
  • crates/aisix-core/src/config.rs
  • crates/aisix-gateway/src/bridge.rs
  • crates/aisix-gateway/src/lib.rs
  • crates/aisix-gateway/src/media_fetch.rs
  • crates/aisix-provider-anthropic/Cargo.toml
  • crates/aisix-provider-anthropic/src/lib.rs
  • crates/aisix-provider-anthropic/src/platform.rs
  • crates/aisix-provider-anthropic/src/wire.rs
  • crates/aisix-provider-bedrock/Cargo.toml
  • crates/aisix-provider-bedrock/src/bridge.rs
  • crates/aisix-provider-bedrock/src/lib.rs
  • crates/aisix-provider-bedrock/src/native.rs
  • crates/aisix-provider-vertex/src/bridge.rs
  • crates/aisix-provider-vertex/src/native.rs
  • crates/aisix-proxy/src/count_tokens.rs
  • crates/aisix-proxy/src/dispatch.rs
  • crates/aisix-proxy/src/effort_mapping.rs
  • crates/aisix-proxy/src/messages.rs
  • crates/aisix-server/src/main.rs
  • tests/e2e/src/cases/anthropic-stream-tool-calls-e2e.test.ts
  • tests/e2e/src/cases/bedrock-converse-stream-reasoning-e2e.test.ts
  • tests/e2e/src/cases/claude-platform-native-messages-e2e.test.ts
  • tests/e2e/src/cases/claude-platform-prompt-caching-e2e.test.ts
  • tests/e2e/src/harness/aws-eventstream.ts
  • tests/e2e/src/harness/index.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

…errides, counts tokens strictly

- The provider key's request overrides are applied by the proxy as on the
  Anthropic passthrough; content_list_to_string (a response override) no
  longer rewrites the forwarded Messages body.
- The platform bridge takes the body value instead of re-parsing its bytes.
- Vertex AI count-tokens sends no anthropic-beta and treats an answer
  without input_tokens as a decode error rather than a count of 0.
- A downloaded text/* body becomes a text source only for a document block.
- The count_tokens 400 names Claude on Bedrock / Vertex AI as a valid target.
…up's next target

Bedrock CountTokens and Vertex AI count-tokens serve fewer models and
regions than the Messages routes, so a non-retryable refusal from one no
longer ends a count_tokens a later Anthropic-wire target can answer.
Media downloads on a streamed platform call are bounded by the request's
end-to-end budget rather than the stream chunk gap.
… /v1/messages gets

The CountTokens invokeModel body and the Vertex AI count-tokens body are
shaped like the Messages passthrough toward the same platform: top-level
field allowlist, metadata.user_id only, cache_control type+ttl on Bedrock
and without scope on Vertex AI. The invokeModel body's max_tokens is
raised above a thinking budget so InvokeModel's schema accepts it; the
value does not change the count.
…drock Mantle

bedrock-runtime's CountTokens refuses Claude models Bedrock serves only
through cross-region inference (Claude Opus 5.5 among them); AWS counts
them on Bedrock Mantle's Anthropic count_tokens. When the runtime call
answers 400/404 for a Claude model, count_tokens is retried on
POST {mantle}/anthropic/v1/messages/count_tokens with the base model id,
messages/system/tools and the thinking/tool_choice/output_config options,
signed SigV4 (service bedrock) from the provider key's access-key
credentials.

New startup key upstream.bedrock.mantle_endpoint (default null: the
public regional host https://bedrock-mantle.{region}.api.aws derived from
the provider key's region) for VPC-endpoint and GovCloud deployments.
jarvis9443 added a commit to api7/api7-helm-chart that referenced this pull request Oct 20, 2026
Pairs with api7/aisix#1318: the Bedrock Mantle endpoint count_tokens
falls back to for Claude models bedrock-runtime cannot count. Null derives
https://bedrock-mantle.{region}.api.aws from the provider key's region.
…ative-messages

# Conflicts:
#	crates/aisix-gateway/src/lib.rs
#	crates/aisix-gateway/src/media_fetch.rs
#	crates/aisix-provider-bedrock/Cargo.toml
#	crates/aisix-provider-bedrock/src/bridge.rs
#	crates/aisix-provider-bedrock/src/lib.rs
@jarvis9443
jarvis9443 merged commit 4213dbb into main Oct 20, 2026
17 checks passed
@jarvis9443
jarvis9443 deleted the feat/bedrock-vertex-native-messages branch October 20, 2026 17:32
jarvis9443 added a commit that referenced this pull request Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant