Lab 01 — Unified Model Invocation & the Capacity Model
Phase 24 · Lab 01 · Phase README · Warmup
The problem
Bedrock's pitch is "one API over many foundation models." That is true, but it hides a real design fork you must be able to explain in an interview:
InvokeModelis a raw HTTP passthrough. You send provider-native JSON and get back provider-native JSON. Anthropic's Messages format, Amazon Titan's single-inputTextformat, Meta Llama's raw[INST]-templated prompt string, and Cohere'smessage+chat_historyformat are four genuinely different schemas — you must know each one.Converse/ConverseStreamis Bedrock's normalized API: one request/response shape (system prompt, multi-turn messages, tool use, streaming) that works identically no matter which model you point it at. Bedrock builds this by translating to/from each provider's native shape under the hood — Converse is not a separate model capability, it is a translation layer sitting on top of the exact sameInvokeModelprimitive.
That normalization has a real cost: Converse's feature set is the least common denominator across every provider it fronts. Not every model supports every Converse feature (tool use is the sharpest example) — and you build that constraint into the adapters themselves.
Then there is capacity: the three different ways Bedrock schedules and prices inference — On-Demand (token-bucket rate limiting), Provisioned Throughput (a purchased, fixed pool of Model Units that queues instead of throttling), and Batch (an async job with a discount and no real-time SLA) — plus cross-region routing and the cost math to compare them.
What you build
| Piece | What it does | The lesson |
|---|---|---|
AnthropicAdapter / TitanAdapter / LlamaAdapter / CohereAdapter | four distinct native request/response schemas | Bedrock federates real vendor differences, it doesn't erase them |
BedrockRuntime.invoke_model | raw provider-native passthrough | why every InvokeModel caller needs per-provider code |
BedrockRuntime.converse / converse_stream | one normalized shape, built from to_native_request → invoke_model → from_native_response | Converse is a translation layer, not new model capability |
TokenBucket | On-Demand rate limiting | ThrottlingException is a token-bucket miss, not magic |
ProvisionedThroughputPool | fixed Model Unit pool, FIFO queue on saturation | Provisioned Throughput queues, On-Demand throttles — different failure mode entirely |
BatchInferenceManager | SUBMITTED → IN_PROGRESS → COMPLETED/FAILED job lifecycle | async, discounted, no real-time SLA |
CrossRegionRouter | pick a region by lowest simulated queue depth | inference profiles raise effective throughput |
estimate_on_demand_cost / estimate_batch_cost / provisioned_breakeven_requests | the whiteboard cost math | when Provisioned Throughput actually pays for itself |
File map
| File | Role |
|---|---|
lab.py | your implementation (fill the # TODOs) |
solution.py | reference implementation + worked example (python solution.py) |
test_lab.py | 40 tests: adapters, Converse normalization, tool-use gating, streaming, capacity modes, routing, pricing |
requirements.txt | pytest only |
Run it
pytest test_lab.py -v # your lab.py (red until you implement)
LAB_MODULE=solution pytest test_lab.py -v # the reference (green)
python solution.py # worked example
Success criteria
-
Four provider adapters each round-trip their own native
invoke_modelschema correctly. -
conversereturns the SAME response shape (output/stopReason/usage) for every provider, built entirely on top ofinvoke_model+ per-adapter translation. -
A
tool_configagainst a provider withsupports_tools = FalseraisesUnsupportedFeatureError— the least-common-denominator tradeoff, made concrete. -
converse_streamreturns a deterministic list of chunk events ending inmessageStop+metadata.usage. -
TokenBucket.consumeraisesThrottlingExceptionover capacity and never goes negative;advancerefills, capped at capacity. -
ProvisionedThroughputPoolruns immediately within capacity, queues FIFO once saturated (not throttles), andcompletedrains the queue in submission order. -
BatchInferenceManagermovesSUBMITTED → IN_PROGRESS → COMPLETED/FAILED, processes requests in deterministic submission order, and rejects a re-processof a finished job. -
CrossRegionRouter.routepicks the lowest-queue-depth available region, deterministically. -
All 40 tests pass under both
labandsolution.
How this maps to the real stack
InvokeModelis the real low-level Bedrock Runtime API: you build the provider's native JSON body yourself (Anthropic Messages, TitaninputText, Llama's prompt template, Cohere'schat_history) and parse its native response. Every SDK example you've seen that special-cases "if model starts withanthropic.... elifamazon.titan..." is working around exactly this.Converse/ConverseStreamare the real unified APIs AWS shipped specifically to kill that special-casing: one message/tool/streaming schema across (most) text models. Ourto_native_request→invoke_model→from_native_responsepipeline is the actual implementation strategy — Converse is Bedrock translating on your behalf, not a different model path.ConverseStream's real transport is Server-Sent Events withcontentBlockDeltaevents; our list-of-events is the deterministic, testable equivalent of the same event shapes.- Tool use support varies by model in real Bedrock — not every model Converse fronts supports
the
toolConfigfield, and calling it against one that doesn't is a real, documented failure mode.UnsupportedFeatureErroronTitanAdapter/LlamaAdapteris that constraint made visible instead of silently ignored. - On-Demand really is a token-bucket-shaped quota per account/model/region (Service Quotas),
and exceeding it really does raise
ThrottlingException(HTTP 429) with an SDK-side exponential-backoff retry convention. Provisioned Throughput really is a purchased, time-committed pool of Model Units; requests within capacity never throttle, and AWS's own guidance is that sustained high-volume traffic should move off On-Demand once volume justifies the commitment — the queuing behavior here is the honest mental model of "reserved capacity," even though real Provisioned Throughput will still reject over a hard ceiling in some configurations rather than queue indefinitely. Batch inference really is submitted as an async job over an S3 input/output manifest, processed with no real-time SLA, at a meaningful discount versus On-Demand (our 50% is "on the order of" the real number, not a guaranteed rate card figure). - Cross-region inference profiles are real: an inference-profile
modelIdroutes a single logical request across a defined set of regions, which both increases effective throughput against per-region quotas and can reach models not yet available in your home region. Real routing is AWS-internal load-based routing; ours is the same idea (route to the least-loaded available candidate) with a simulated queue-depth signal instead of AWS's live telemetry.
Limits of the miniature. Real tokenization is BPE and provider-specific; ours is a crude
len(text)//4 estimate — good enough to reason about ratios, not to bill against. Real
Provisioned Throughput purchase has commitment terms (1-month/6-month) and per-model Model Unit
throughput that varies by model size; ours is a flat capacity_per_unit. Real batch jobs read
from and write to S3 and do not guarantee output order matches input order; ours processes
in-memory in submission order specifically so the test suite can assert on it.
Extensions (your own machine)
- Add a retry-with-backoff wrapper around
TokenBucket.consumethat retries onThrottlingExceptionwith a fake, injected clock (never realtime.sleep). - Add a fifth provider adapter (Mistral or AI21) to prove the adapter interface generalizes.
- Model Custom Model Import as an adapter whose native schema you define yourself, to show Converse can front a model Bedrock didn't originally know about.
- Wire
estimate_on_demand_cost/estimate_batch_cost/provisioned_breakeven_requestsinto a small "which capacity mode should I use" calculator that takes a traffic shape and recommends On-Demand, Provisioned Throughput, or Batch.
Interview / resume signal
"Built a miniature Bedrock Runtime: four provider adapters (Anthropic, Titan, Llama, Cohere) each with a genuinely different native request/response schema, unified behind a Converse-style normalization layer that fails closed on unsupported features (tool use) rather than silently dropping them — plus the three capacity models (On-Demand token-bucket throttling, Provisioned Throughput's queue-not-throttle semantics, Batch's async job lifecycle) and the cost math that decides between them."