Wardian docs

ADR-2610071106: Claude through Amazon Bedrock

Status: Accepted Date: 2026-10-07 Drivers: Teams that buy Claude through AWS reach it through Amazon Bedrock, under their AWS account, region and IAM policies, and often may not hold an Anthropic API key at all. Wardian can only use the Anthropic API today, so for them "Make an app" and claude:sample do not work.

Context#

Claude reaches Wardian through one port, ports/llm.rs: Llm::messages(auth, body), Llm::test_key(auth) and Llm::model(tier), with Tier::Main and Tier::Quick. One adapter implements it, adapters/secondary/anthropic_inference.rs, and it alone names models, as hexa's no-model-name-outside-inference rule requires. The use case (usecases/studio.rs) builds each request body in the Messages format, retries 429 and 5xx, and keeps the key and workspace in the data folder (anthropic-key, anthropic-workspace).

Bedrock serves the same Claude models and the same Messages body, with four differences:

Anthropic APIAmazon Bedrock
Addresshttps://api.anthropic.com/v1/messageshttps://bedrock-runtime.<region>.amazonaws.com/model/<model id>/invoke
Model"model" in the bodyin the URL; the body carries "anthropic_version": "bedrock-2023-05-31" and no "model"
Model namesclaude-…Bedrock ids or inference profiles, such as anthropic.claude-…-v1:0 or us.anthropic.claude-…
Sign-inx-api-key (+ anthropic-workspace-id)a Bedrock API key (Authorization: Bearer …), or AWS access keys signed with Signature Version 4

Errors differ too: Bedrock answers 403 AccessDeniedException when the account has not been granted the model, 400 ValidationException for a bad body, and 429 ThrottlingException.

Decision#

Add Bedrock as a second provider behind the same port. The use cases do not change what they ask; the composition root and the settings decide where it goes.

  1. Port. LlmAuth becomes the credentials of a provider: Anthropic { key, workspace } or Bedrock { region, auth }, where auth is ApiKey(token) or AccessKeys { id, secret, session }. Llm::messages keeps taking a complete Messages body; each adapter maps it to its wire format.
  2. Adapter. adapters/secondary/bedrock_inference.rs: moves the model from the body to the URL, adds anthropic_version, signs the request (bearer token, or SigV4 with ring's HMAC-SHA256 and SHA-256, which Wardian already carries for TLS — no AWS SDK and no new crate, so the release binary stays under 12 MB, which tests/run-all.sh checks (ADR-2610081041)), and maps Bedrock's errors onto LlmError so the use case's retries and messages keep working (403 says the model has not been enabled for the account in that region). It owns the default Bedrock model ids for both tiers; WARDIAN_BEDROCK_MODEL and WARDIAN_BEDROCK_QUICK_MODEL override them.
  3. Choice of provider. Settings → Make apps with Claude gets a provider choice: Anthropic API or Amazon Bedrock (region, and a Bedrock API key or access keys). The use case holds the current provider behind the port and swaps it when the settings change, as the catalog swaps its source. Bedrock settings are saved private in <data dir>/bedrock.json and never sent back to the browser, like the Splunk password. From the environment: WARDIAN_AI_PROVIDER=bedrock, AWS_REGION, and AWS_BEARER_TOKEN_BEDROCK or AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN.
  4. Testing a key. Bedrock's runtime endpoint has no model lookup, so the test is one invocation of the quick model with max_tokens: 1: exactly one request, and a refused key is not saved. Not yet tested (ADR-2610081041): tests/splunk-e2e.js checks that a wrong key is refused, but no test counts the requests or reads max_tokens. It proves the region, the sign-in and the model access together.

Not in this decision: AWS profiles, SSO and instance roles (the full AWS credential chain), streaming, and the Converse API. Each can follow in its own ADR.

Consequences#

Implementation#

Gate: cargo build --release && cargo test --release && hexa analyze . --grade A, then tests/run-background-e2e.sh and tests/run-splunk-e2e.sh run once against the fake Anthropic API and once against the fake Bedrock, and the SigV4 unit tests against AWS's signing test vectors.

Enforced-By: hexa adr gates (run on demand)#

Gate#

env CARGO_TARGET_DIR=target/verify cargo test --release bedrock_inference

Rerun by hexa adr gates. It builds into target/verify, never into the copy of Wardian a user runs.

References#

Evidence#

bash -c 'cargo test --release --offline 2>&1 | grep -E "bedrock_inference|test result: ok. [1-9]"; hexa analyze . --grade A 2>&1 | grep -E "Architecture grade|coverage"' at ca4abb0 with uncommitted changes on 2026-10-07 16:24 UTC:

test adapters::secondary::bedrock_inference::tests::dates_and_hosts ... ok
test adapters::secondary::bedrock_inference::tests::model_ids_are_encoded_in_the_path_and_twice_in_the_signature ... ok
test adapters::secondary::bedrock_inference::tests::signing_key_matches_aws_example ... ok
test adapters::secondary::bedrock_inference::tests::signature_matches_aws_iam_example ... ok
test result: ok. 27 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.06s
  ⬡ Architecture grade: A+ — score 100/100
    coverage 40/40 files in a layer

This page is docs/adrs/ADR-2610071106-claude-through-amazon-bedrock.md in the repository. Something wrong or missing? Change that file.