« Phase 24 README · Warmup · Hitchhiker's · Principal Deep Dive · Core Contributor · Staff Notes
Phase 24 — Deep Dive: Amazon Bedrock, the Managed Foundation-Model Platform
The load-bearing mechanism in this phase is not "a model behind an API." It is a schema translation layer wrapped in three admission-control disciplines (throttle, queue, defer) and a two-phase content-safety filter. Everything else is packaging. This doc traces those mechanisms at the level a person implementing them has to reason about — data shapes, ordering, invariants, failure surfaces — because that is exactly what Lab 01 and Lab 02 make you build.
The translation layer is the whole game
InvokeModel is a passthrough: bytes in provider schema, bytes out provider schema. There is no
abstraction — Bedrock is plumbing, not a contract. The moment you route to more than one provider,
your call sites grow an if model_id.startswith("anthropic") ladder that leaks four incompatible
JSON shapes into your application. Converse exists to move that ladder inside the platform.
The mechanism is a pair of pure functions per provider: to_native(unified_request) and
from_native(native_response). The unified request is a small, fixed record — a system string, an
ordered list of {role, content} turns, optional toolConfig, and inference parameters
(maxTokens, temperature, topP). Each adapter maps that record onto its provider's native
shape:
- Anthropic — system as a top-level field, messages as a typed
contentarray, tools as a first-classtoolslist. Nearly 1:1 with the unified shape because Converse was modeled on it. - Titan — a single
inputTextcompletion string and atextGenerationConfig. The multi-turn message list has to be flattened into one prompt string, and there is no native tool concept — soto_nativemust rejecttoolConfigrather than silently drop it. - Llama — a raw prompt string with
[INST] ... [/INST]instruction templating; system and turns are serialized into that template. Also no structured tool calling. - Cohere — a
messageplus achat_historyarray of{role: USER|CHATBOT, message}— close in spirit to the unified shape but with different role tokens and field names.
The invariant that makes this a layer and not a leak: from_native(to_native(x)) must return
the same unified response record regardless of which adapter ran. Lab 01's forty tests are, at
their core, a proof of that round-trip commutativity across four schemas.
Why the naive approach fails at the mechanism level. The tempting shortcut is a lowest-common
JSON with optional fields and "just ignore what a provider can't do." That fails precisely on tool
use. Titan and Llama have no tool-calling grammar; there is nothing to translate toolConfig
into. If you silently drop it, the model answers in prose, your caller's tool-dispatch code sees
no toolUse block, and the failure surfaces three layers away as "the agent stopped calling
tools" — undebuggable. The correct mechanism is to make the adapter raise
UnsupportedFeatureError at translation time, at the exact boundary where the capability gap is
knowable. Real Bedrock does the same thing with a ValidationException. The ceiling of a
translation layer is the least-capable target, and the only safe design surfaces that ceiling
loudly at the edge instead of hiding it.
Admission control: three shapes of "no"
A model endpoint is a finite resource. Every serving platform must decide what happens when demand exceeds capacity, and Bedrock exposes three mechanically distinct answers. Getting these straight is the difference between "add more retries" and "do the capacity math."
On-Demand is a token bucket. State: capacity (burst), tokens (current), refill_per_tick.
consume(n) succeeds and decrements iff tokens >= n; otherwise it raises
ThrottlingException. Refill is monotonic up to capacity. The invariant is 0 <= tokens <= capacity at all times, and the key property is that rejection is immediate and total — the
request is not held. The caller owns the retry, canonically exponential backoff with jitter. Lab
01 injects the tick instead of reading a wall clock so the bucket is deterministic; the algorithm
is otherwise the real one. The failure mode is a 429 storm, and the bug is treating that storm
as an application error rather than a signal you have crossed a capacity threshold.
Provisioned Throughput is a bounded queue. State: capacity (reserved slots), in_flight, a
FIFO _queue. invoke() occupies a slot if one is free; if all slots are saturated it enqueues
rather than rejects. complete() frees a slot and admits the head of the queue. The invariant is
in_flight <= capacity, and the load-bearing behavioral difference from On-Demand is that
saturation produces latency, not an error. You paid for capacity, so the platform holds your
request. This is why "we're getting throttled" and "requests are slow" are diagnoses of two
different capacity regimes, and why the fix for one (back off and retry) is wrong for the other
(you are already admitted, retrying just adds load).
Batch is deferral. A job moves SUBMITTED → IN_PROGRESS → COMPLETED|FAILED. There is no
real-time SLA; the platform schedules the work when it has slack, in exchange for roughly a 50%
discount on the same per-token rate. The mechanism is an async state machine, not an admission
gate on a live request. The lab processes in submission order in-memory for testability and
explicitly notes real Bedrock reads/writes S3 and does not guarantee output order matches
input order — an ordering invariant you must not assume.
The crossover is arithmetic, and you should be able to derive it live. Provisioned Throughput
is fixed cost C_PT per Model-Unit-hour; On-Demand is variable cost C_OD per request. Set them
equal over H hours with U units: N* = (C_PT × U × H) / C_OD. Below N* requests in the
window, On-Demand is cheaper; above it, Provisioned Throughput is and you get throttle-free
latency as a bonus. Plugging illustrative numbers ($21/unit-hour, 1 unit, 24 h, $0.02/request)
gives ~25,200 requests/day. This is the calculation the "just add retries" reflex skips.
Cross-region routing sits on top
A cross-region inference profile is one logical modelId (ARN-shaped, conventionally prefixed
like us.anthropic.claude-...) that fans out across a fixed member-region set. The mechanism is a
router that, per request, picks an available member region — the lab models it as "lowest simulated
queue depth wins." Two concrete wins: your per-region On-Demand quotas sum into one logical
stream (higher effective throughput before you hit any single region's token bucket), and you can
reach a model live in a region where it is available even if your home region lacks it. The lab
reproduces the decision shape; real AWS routing telemetry is internal.
The two-phase safety filter
Guardrails is a filter over text, evaluated at up to two points: input (before the model is invoked) and output (after generation, before the caller sees it). The load-bearing invariant, which Lab 02 proves with a direct test: a blocked input guardrail means the model is never called at all — zero tokens spent, and denied content never enters the context window. This is why a guardrail is cheaper and safer than a post-hoc filter bolted onto the response.
Five independent policies compose into one verdict:
- Denied topics — natural-language topic classification; the only subject-matter policy.
- Content filters — six categories (hate, insults, sexual, violence, misconduct, prompt attack), each scored None/Low/Medium/High, each with a configurable filter strength. The threshold is the tuning knob, not the detector — a High-severity hit under a Low-strength config behaves differently than under High-strength.
- Word filters — exact denylist plus built-in profanity.
- PII filters — per entity type, either mask (redact just the matched span, keep the rest of the response) or block (replace the whole response). This is a real per-type UX decision, not a fixed behavior.
- Contextual grounding — output-only, scores groundedness (is the claim supported by supplied sources?) and relevance against thresholds. It is the only policy that catches a fluent, polite, non-toxic, entirely fabricated answer — content filters look for toxicity, not truth.
Every intervention emits a trace naming the policy, the rule, and the action; the aggregate result
is one GuardrailAction: NONE or GUARDRAIL_INTERVENED. Contextual grounding is output-only by
construction — there is nothing to ground a bare prompt against.
Knowledge Bases: retrieval as a pipeline, not a database
A Knowledge Base is Phase 05's hybrid retriever run as a managed pipeline. Ingestion: a data source feeds documents through a chunking strategy, chunks are embedded, and vectors are written to a store you provision (OpenSearch Serverless, Aurora pgvector, Pinecone, Redis, MongoDB Atlas). Bedrock owns the pipeline; you own the store.
The mechanism worth tracing is hierarchical parent-child chunking. Split each document into
large parent chunks, then split each parent into small child chunks. Embed and index only
the children (small chunks make cosine similarity sharper and more precise). On query, match a
child, but return the child's parent text for generation (large chunks give the model more
context). This is the one strategy that buys precision and context instead of trading them; the
data structure is simply a child→parent map alongside the vector index, and retrieve does the
parent lookup after the nearest-child search. Fixed-size-with-overlap, by contrast, forces one
chunk size to compromise both goals.
Retrieval exposes two APIs with different ownership boundaries: Retrieve returns ranked chunks
and lets you build the prompt (right when the KB is one of several context sources);
RetrieveAndGenerate does retrieval, prompt construction, generation, and citation assembly in one
call, returning output.text plus a citations array tying each span back to the source chunk.
Metadata filtering constrains retrieval to tagged chunks — the mechanism that makes one KB serve
coarse multi-tenant RAG without one index per tenant.
What to hold onto
Strip the AWS branding and the mechanisms are provider-agnostic: normalize invocation with a per-target translation pair whose ceiling is the weakest target; pick admission control by failure shape (reject/queue/defer) and by the breakeven arithmetic; filter text in two phases with the model-never-sees-blocked-input invariant; retrieve by embedding small and returning large over a store you own. Learn the mechanism and every vendor's docs read the same.