AI EngineeringZero to ProductionHome·About·Contact
Anthropic API in Practice · Part 6

Run Claude anywhere

The same code runs on the direct Anthropic API, on Amazon Bedrock, and on Google Vertex — you swap the client and keep messages.create identical. Learn the three real SDK clients, the auth each one needs, how model-IDs differ per provider, when and why teams pick each door, and a config-driven factory that makes the provider a one-line setting.

⏱️ ~1.5 hours🧪 5 recipes🎯 Beginner→Tech-lead

Learning objectives

  • Say what "run Claude anywhere" means: one messages.create call, three clients.
  • Construct each client — Anthropic(), AnthropicBedrock(...), AnthropicVertex(...).
  • Match each provider to its auth: ANTHROPIC_API_KEY vs AWS IAM vs GCP ADC.
  • Map a logical model name to the right model-ID string for each provider.
  • Reason about feature availability and lag across providers before you commit.
  • Write a config-driven make_client(provider) factory so switching providers is one setting.

1 · One Claude, three front doors essential

You already know the everyday call: build a client, hand it a list of messages, read the reply. What most people don't realize is that the exact same call works whether Claude is served by Anthropic directly, by Amazon Bedrock inside your AWS account, or by Google Vertex AI inside your GCP project. Same model, same Messages API, same response shape — the only thing that changes is which client object you build.

The anthropic Python SDK ships three client classes for exactly this: Anthropic, AnthropicBedrock, and AnthropicVertex. All three are real, first-class classes; all three expose the identical client.messages.create(...) method. Pick the client that matches where you want the request billed and governed — and leave every line about the conversation untouched.

Swap the client, keep the callThis is the whole lesson in one sentence: the client construction and the model-ID string are provider-specific; the messages you send and the content you read back are not. Build the portability into the client factory and the rest of your app never learns which provider it's on.

2 · The shape of portability essential

Picture your app calling one client factory. The factory reads a config value and returns the right client; from there a single messages.create lands on whichever provider you chose:

Your app one code path make_client() picks by config Anthropic API Anthropic() Bedrock AnthropicBedrock() Vertex AnthropicVertex()
🗺️ How to read this diagram

This picture is the whole chapter on one line: your application talks to one factory, and the factory — not your app — decides which of the three Claude providers the request goes to. Read it left to right.

  • Your app — never names a provider. It just asks the factory for a client and then calls messages.create. This is the code that stays the same no matter where Claude runs.
  • make_client() — the one function that knows about providers. It reads a single config value and hands back the matching client object.
  • Anthropic API / Bedrock / Vertex — the three doors. The factory returns Anthropic(), AnthropicBedrock(), or AnthropicVertex() depending on the config; behind all three sits the same Claude.
  • The arrows fan out from the factory because only one door is chosen per run — the config picks it, and everything after the factory is identical.

In short: Same brain, three lobbies. Whenever provider code feels tangled, come back to this shape: keep the provider choice inside the factory, and let the rest of the app stay provider-blind.

Read it left to right. Your app never names a provider — it calls make_client(). The factory looks at one config value and returns Anthropic(), AnthropicBedrock(...), or AnthropicVertex(...). All three converge on the same messages.create. The branch that picks a provider lives in one function; everything downstream is provider-agnostic.

Why three doors exist at allAnthropic serves Claude directly, and it also partners with the big clouds so enterprises can consume Claude through the cloud they already buy from. The model is the same; what differs is the billing relationship, the identity system, and the data-residency guarantees.

3 · Recipe 1 — the direct Anthropic API essential

Start with the baseline everyone knows. The direct client reads ANTHROPIC_API_KEY from the environment, so the key never appears in code. Note the model-ID: on the direct API it's the plain, current alias claude-opus-4-8.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Python · anthropic SDK — direct API ▶ needs API key + network
direct_api.py# needs: pip install anthropic
# Anthropic: ANTHROPIC_API_KEY in the environment; makes a real API call
from anthropic import Anthropic

client = Anthropic()   # reads ANTHROPIC_API_KEY from the environment

resp = client.messages.create(
    model="claude-opus-4-8",              # direct API: plain model alias
    max_tokens=300,
    system="You are a concise SRE assistant.",
    messages=[{"role": "user", "content": "In one sentence, what is an SLO?"}],
)
print(resp.content[0].text)
▶ How this works

This is the baseline call — the ordinary first request to the direct Anthropic API. It needs pip install anthropic and an ANTHROPIC_API_KEY, and it makes a real network call. Hold its shape in mind: the next two recipes change only the first couple of lines.

  1. Anthropic() builds the direct client. It reads your key from the environment, so the secret never appears in the code.
  2. The model string is the plain alias for the current Opus model — no prefix, no region. On the direct API that's all you write.
  3. messages.create(...) sends the request: max_tokens caps the reply, system sets the assistant's role, messages is the conversation (one user question here).
  4. resp.content[0].text pulls the words out of the reply — the content is a list of blocks, so you grab the first block's text.

What the output means: Prints Claude's one-sentence answer describing an SLO. (Marked "needs API key + network" because it makes a live call.)

Try this: Copy these lines. In the next two recipes, watch how only the import, the client line, and the model string change — this whole request block is reused verbatim.

This is the call you already writeNothing here is portability-specific yet — it's the ordinary first call from the Anthropic API lessons. Keep it in your mind's eye: the next two recipes change only the first two lines and the model string. Everything else is byte-for-byte identical.

4 · Recipe 2 — Amazon Bedrock intermediate

AnthropicBedrock routes the same request through your AWS account. There is no API key — auth is your AWS credentials (an IAM role on Lambda/EC2/ECS, or your local ~/.aws profile), and you must have requested model access to Claude in the Bedrock console for your region. The model-ID takes an anthropic. prefix.

Python · anthropic SDK — Amazon Bedrock ▶ needs API key + network
bedrock.py# needs: pip install "anthropic[bedrock]"
# Bedrock: AWS creds (IAM role / ~/.aws profile) + Bedrock model access; real API call
from anthropic import AnthropicBedrock

client = AnthropicBedrock(aws_region="us-east-1")   # no API key — uses AWS creds

resp = client.messages.create(
    model="anthropic.claude-opus-4-8",    # Bedrock: anthropic. prefix on the id
    max_tokens=300,
    system="You are a concise SRE assistant.",
    messages=[{"role": "user", "content": "In one sentence, what is an SLO?"}],
)
print(resp.content[0].text)
▶ How this works

The same request, now routed through your AWS account with AnthropicBedrock. Compare it to Recipe 1 line by line: the import and the client changed, the model string gained a prefix, and nothing else did.

  1. AnthropicBedrock(aws_region="us-east-1") builds the Bedrock client. There is no API key — it uses your AWS credentials (an IAM role on your compute, or your local AWS profile), so auth is AWS's job, not Anthropic's.
  2. The model id carries an anthropic. prefix — Bedrock's naming convention. A bare alias here would 404.
  3. The messages.create(...) block is byte-for-byte identical to the direct API recipe — same arguments, same reply handling.
  4. Two AWS-side things must be true first: an IAM policy allowing bedrock:InvokeModel, and model access enabled for Claude in the Bedrock console for that region.

What the output means: Prints the same one-sentence SLO answer — but billed and governed inside your AWS account. (Marked "needs API key + network": here that means valid AWS creds.)

Try this: Diff this file against direct_api.py. Only lines 3–4 (import + client) and the model string differ — proof that the conversation code is portable.

Two things must be true on BedrockFirst, your AWS principal needs an IAM policy allowing bedrock:InvokeModel. Second, you must enable model access for Claude in the Bedrock console for that aws_region — a fresh account has it off by default, and the request 400s until you do. Both are AWS-side setup, not SDK code.

5 · Recipe 3 — Google Vertex AI intermediate

AnthropicVertex routes the request through your GCP project. Auth is Google Application Default Credentials (ADC) — run gcloud auth application-default login locally, or use the attached service account on GCP compute. You pass a project_id and a region. The model-ID for current-generation models is the bare alias (no prefix).

Python · anthropic SDK — Google Vertex AI ▶ needs API key + network
vertex.py# needs: pip install "anthropic[vertex]"
# Vertex: gcloud ADC + a GCP project with the Vertex AI API enabled; real API call
from anthropic import AnthropicVertex

client = AnthropicVertex(project_id="my-gcp-project", region="us-east5")

resp = client.messages.create(
    model="claude-opus-4-8",              # Vertex: bare id (dated snapshots use @date)
    max_tokens=300,
    system="You are a concise SRE assistant.",
    messages=[{"role": "user", "content": "In one sentence, what is an SLO?"}],
)
print(resp.content[0].text)
▶ How this works

The third door: AnthropicVertex routes the request through your Google Cloud project. Again, only the client and the model string are Vertex-specific — the request itself is the same one you've now written three times.

  1. AnthropicVertex(project_id=..., region=...) builds the Vertex client. You pass a GCP project and region instead of a key; auth comes from Google Application Default Credentials (the gcloud auth application-default login you ran, or the compute's service account).
  2. The model id is the bare alias — no anthropic. prefix (that's a Bedrock thing). Older dated-snapshot models on Vertex use an @date suffix, noted in the comment.
  3. The messages.create(...) block is, once more, identical to both earlier recipes.

What the output means: Prints the same SLO answer, served from your GCP project. (Marked "needs API key + network": here that means working gcloud ADC.)

Try this: Line the three recipes up in one editor. The messages.create block never changes — the model id and the client constructor are the only moving parts across providers.

Put the three recipes side by side and the pattern jumps out: lines 3–4 (the import and the client) and the model string are the only differences. The messages.create(...) block — max_tokens, system, messages — is copy-pasted verbatim across all three.

Model-ID naming is the sharp edgeThe anthropic. prefix on Bedrock and the bare alias on Vertex trip people constantly. A Bedrock id sent to Vertex (or vice-versa) 404s. This is exactly the difference the offline mapper in section 7 exists to encapsulate.

6 · Auth & model-IDs at a glance advanced

Everything that differs between the three doors fits in one table. Learn this and the rest is muscle memory:

Direct APIAmazon BedrockGoogle Vertex
ClientAnthropic()AnthropicBedrock(aws_region=…)AnthropicVertex(project_id=…, region=…)
AuthANTHROPIC_API_KEYAWS creds / IAM roleGCP ADC (gcloud login)
Model-IDclaude-opus-4-8anthropic.claude-opus-4-8claude-opus-4-8
Install extraanthropicanthropic[bedrock]anthropic[vertex]
messages.createidenticalidenticalidentical

The bottom row is the payoff. The request body — system prompt, messages, tools, streaming, prompt caching, structured output — is the same object on every provider. The top four rows are the only per-provider knobs, and a factory can set all of them from one config value.

Keys vs identityThe direct API uses a bearer secret you manage (ANTHROPIC_API_KEY). Bedrock and Vertex use the cloud's own identity system — IAM roles and GCP service accounts — so there's no Anthropic key to rotate or leak, and access is governed by the same policies as the rest of your cloud.

7 · The mapper (this one runs offline) professional

Before wiring a live factory, encode the per-provider differences in a tiny, testable function. The helper below is pure stdlib and runs with a plain python file.py — no keys, no network. Given a logical model name and a provider, it returns the right client class name and the correctly-formatted model-ID, so the naming rules live in one auditable place.

Python · provider / model-ID mapper (runs offline)
pick_provider.pydef pick_provider(provider, logical_model="opus"):
    """Map (provider, logical_model) -> the client class name + provider-specific
    model-id string. Pure stdlib: no SDK, no network. Encapsulates the naming rules
    so no other code has to remember the anthropic. prefix or the bare-alias quirk."""
    base = {"opus": "claude-opus-4-8", "haiku": "claude-haiku-4-5"}[logical_model]
    table = {
        # provider  -> (client class,        model-id transform)
        "api":     ("Anthropic",        base),
        "bedrock": ("AnthropicBedrock",  "anthropic." + base),   # Bedrock: prefix
        "vertex":  ("AnthropicVertex",   base),                  # Vertex: bare alias
    }
    client_class, model_id = table[provider]
    return {"client_class": client_class, "model_id": model_id}

for prov in ["api", "bedrock", "vertex"]:
    r = pick_provider(prov, "opus")
    print(f"{prov:8} {r['client_class']:18} model_id={r['model_id']}")
api      Anthropic          model_id=claude-opus-4-8
bedrock  AnthropicBedrock   model_id=anthropic.claude-opus-4-8
vertex   AnthropicVertex    model_id=claude-opus-4-8
▶ How this works

Unlike the three recipes above, this helper is pure Python — no key, no network — so it runs with a plain python pick_provider.py. It captures the per-provider differences (client class + model-id format) in one small, testable function.

  1. base looks up the real model id for a logical name you choose ("opus" or "haiku") — so your app can say "opus" and let the mapper resolve the exact string.
  2. The table maps each provider to a pair: the client class name and the correctly-formatted model id. Only the Bedrock row adds the anthropic. prefix; the direct and Vertex rows keep the bare alias.
  3. It returns a small dict — client_class and model_id — so every call site asks the mapper instead of hard-coding a provider-specific string.
  4. The loop prints one aligned row per provider using f-string width padding (:8, :18) so the columns line up.

What the output means: Three aligned rows: api and vertex show the bare claude-opus-4-8, while bedrock shows anthropic.claude-opus-4-8 — exactly the console output shown below the code.

Try this: Add a line for "sonnet": "claude-sonnet-4-6" to base and call pick_provider("bedrock", "sonnet"). The prefix rule is applied for free, because it lives in this one function.

One place to be rightThe value of the mapper isn't cleverness — it's that the anthropic. prefix and the client-class choice are decided once. Every call site asks the mapper instead of hard-coding a string, so a new provider or a renamed model is a one-line change, not a repo-wide find-and-replace.

8 · Why teams pick each door professional

Portability is only useful if you know why you'd move. The choice is almost never about the model — it's identical — and almost always about compliance, existing cloud commitments, and latency:

DriverPoints you toBecause
Data residency / complianceBedrock or VertexRequests and data stay inside your AWS/GCP account and its region, under your existing audit and governance controls.
Existing cloud commitmentthe cloud you're already onClaude spend rolls onto one bill and one committed-use / EDP discount instead of a separate Anthropic invoice.
Identity & accessBedrock or VertexAccess is an IAM policy or GCP role — same model your security team already manages — with no extra API key to rotate.
Latency / regionwhichever is closestServing from a region near your workload cuts round-trip time; pick the provider with a Claude region where you run.
Simplicity / newest featuresthe direct APIThe direct API gets the fullest, earliest feature set (see the lag note below) with the least setup — just a key.

A useful rule of thumb: if you already live in AWS or GCP, use the matching door so AI spend, identity, and data stay in the cloud you already trust and audit. If you have no such constraint, the direct API is the simplest path and the first to get new capabilities.

9 · Feature availability & lag advanced

The models are the same, but not every API feature ships on every provider at the same time. New capabilities land on the direct Anthropic API first and reach the cloud providers on their own schedule. A few concrete gaps worth knowing before you commit a workload:

FeatureDirect APIBedrockVertex
Messages, streaming, tool useyesyesyes
Prompt cachingyesyesyes
Message Batches APIyesnono
Files APIyesnono
Web search (server tool)yesnoyes (basic only)
Web fetch / code executionyesnono

The core — messages, streaming, tools, caching, thinking — is everywhere, so portability is real for the vast majority of apps. The gaps are in the newer, peripheral surfaces (Batches, Files, some server-side tools). Check availability for the specific features your workload needs before you assume a provider is a drop-in swap.

Portability is a property of your feature setIf your app only uses messages.create with tools and caching, it is fully portable today. If it depends on the Batches API or the Files API, it is not portable to Bedrock or Vertex — those endpoints simply aren't there. Know which bucket you're in.

10 · Tech-lead — the provider-agnostic wrapper tech-lead

A lead's job is to make provider choice a configuration decision, not a code decision. The pattern: one factory that turns a config value into the right client, so business logic calls messages.create and never learns which door it went through. Here is the shape — the client construction is real SDK; treat it as the template you'd wire into a real app.

Python · config-driven client factory ▶ needs API key + network
make_client.py# needs: pip install "anthropic[bedrock,vertex]"
# Anthropic: ANTHROPIC_API_KEY | Bedrock: AWS creds + model access | Vertex: gcloud ADC
import os
from anthropic import Anthropic, AnthropicBedrock, AnthropicVertex

def make_client(provider):
    """Return the right client for the configured provider. The ONLY provider-aware
    code in the app — everything downstream calls client.messages.create unchanged."""
    if provider == "api":
        return Anthropic()                                    # ANTHROPIC_API_KEY
    if provider == "bedrock":
        return AnthropicBedrock(aws_region=os.environ["AWS_REGION"])
    if provider == "vertex":
        return AnthropicVertex(project_id=os.environ["GCP_PROJECT"],
                               region=os.environ["GCP_REGION"])
    raise ValueError(f"unknown provider: {provider}")

# ONE config value decides the provider; the model-id comes from the offline mapper.
provider = os.environ.get("LLM_PROVIDER", "api")
client = make_client(provider)
model_id = pick_provider(provider, "opus")["model_id"]   # from section 7

resp = client.messages.create(              # <-- identical on every provider
    model=model_id,
    max_tokens=300,
    system="You are a concise SRE assistant.",
    messages=[{"role": "user", "content": "In one sentence, what is an SLO?"}],
)
print(resp.content[0].text)
▶ How this works

This is the pattern a tech-lead ships: one factory turns a config value into the right client, so the rest of the app never learns which provider it's on. The client construction is the real SDK; treat it as the template for a real integration.

  1. make_client(provider) is the only provider-aware function. Each branch returns the matching real client — Anthropic(), AnthropicBedrock(...), or AnthropicVertex(...) — reading its credentials from the environment.
  2. One config value — LLM_PROVIDER — decides the provider. The model id comes from the offline mapper (section 7), so naming rules stay centralized too.
  3. The messages.create(...) call is provider-blind: it takes the resolved model_id and the same messages, and works on whichever client the factory returned.
  4. To move a workload from the direct API to Bedrock — say, for a compliance review — you change one environment variable and ship. No business logic, no prompt changes.

What the output means: Prints the SLO answer from whichever provider LLM_PROVIDER selected — the app code is identical across all three.

Try this: Set LLM_PROVIDER=vertex then =bedrock and re-run (with the right creds). Only the environment variable changed — that's the whole point of the factory.

Notice how the provider-aware surface has shrunk to two functions: make_client (which client) and pick_provider (which model-ID). The messages.create call is provider-blind. To move a workload from the direct API to Bedrock for a compliance review, you change one environment variable — LLM_PROVIDER=bedrock — and ship. No business logic changes, no prompt changes, no rewrite.

Tech-lead checklist for a portable Claude integration

  1. One factory, one config value. All provider selection lives in make_client(provider), driven by a single env var / config key. No other module imports AnthropicBedrock or AnthropicVertex.
  2. Centralize model-IDs. Never hard-code anthropic.claude-… at a call site — go through the mapper so the prefix rule and alias choices live in one place.
  3. Feature-gate on availability. If a code path uses Batches or Files, it isn't portable to Bedrock/Vertex — guard it and fail loudly rather than 404 at runtime.
  4. Auth per environment, not in code. Direct = key from env; Bedrock = IAM role on the compute; Vertex = ADC / service account. Keep secrets out of the codebase on every door.
  5. Test the swap. Run your smoke test against each provider you support in CI (or a scheduled job) so a provider-specific regression is caught before a customer hits it.
Compliance & data residency drive the choiceFor regulated workloads the deciding factor is usually where the data may live and under whose controls. Bedrock keeps traffic inside your AWS account and region; Vertex keeps it inside your GCP project and region — each under the cloud's own compliance certifications and your existing audit trail. The direct API is simplest but is a separate data-processing relationship. Decide this before you build, because it dictates the default provider your factory returns.

Exercise AP6.1 — Port a call and abstract it

Context: The whole run-anywhere promise is that your first-call code moves to a second cloud untouched — only the constructor and model id change — and a small factory collapses even that into a single config value.

Your task: Take your first-call code, get it running unchanged on a second provider by swapping Anthropic() for AnthropicBedrock(aws_region=...) (or AnthropicVertex(project_id=..., region=...)) and changing only the model id, then fold both into a make_client(provider) factory.

Requirements:

  • Run the same messages.create body on a second provider, changing only the client and model id
  • Add the anthropic. model-id prefix for Bedrock only
  • Build a make_client(provider) factory that returns the right client per provider
  • Show the same app code runs on either door by changing one config value
  • The live call needs creds; the factory routing runs offline

💡 Hint: Once the factory returns a ready client, downstream code should never mention a provider again — that's the signal you've abstracted it correctly.

Exercise AP6.2 — Extend the mapper

Context: A portability mapper is only useful if it stays correct as you add models — extending it and re-checking that Bedrock (and only Bedrock) gains the prefix is how you keep that one sharp edge from leaking into app code.

Your task: Add a third logical model (e.g. "sonnet" → "claude-sonnet-4-6") to pick_provider, re-run it for all three providers, confirm the Bedrock id gains the anthropic. prefix while direct and Vertex stay bare, and name one feature that would block a move to Bedrock or Vertex.

Requirements:

  • Add the new logical-model mapping and re-run pick_provider across direct/bedrock/vertex
  • Confirm only the Bedrock id carries the anthropic. prefix
  • Verify the direct and Vertex ids remain bare
  • Name one feature your app depends on (e.g. Batches or Files API) that isn't on Bedrock/Vertex
  • Run entirely offline — no keys

💡 Hint: Reuse the availability table from the ladder's industry rung to answer the blocking-feature question — Batches and Files are the usual direct-only culprits.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Call the direct Anthropic APIBeginner

Context: The direct Anthropic API is the baseline every portability story starts from: the client reads your key from the environment, and everything after the constructor is identical no matter which cloud eventually hosts the call.

Your task: Write the minimal call that sends one user message to Claude via the direct Anthropic API and prints the reply text.

Requirements:

  • Construct anthropic.Anthropic(), which reads ANTHROPIC_API_KEY from the environment
  • Call client.messages.create with a model, max_tokens, and a single user message
  • Print the reply via resp.content[0].text
  • Requires an API key to run

💡 Hint: Don't pass the key explicitly — the constructor picks it up from the environment, which is what makes the rest of the code provider-agnostic.

Show solution

The direct client reads ANTHROPIC_API_KEY from the environment. Everything after the client construction is identical across providers.

import anthropic

client = anthropic.Anthropic()          # reads ANTHROPIC_API_KEY
resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=256,
    messages=[{"role": "user", "content": "Say hello in one short sentence."}],
)
print(resp.content[0].text)
Exercise 2 · Swap in the Bedrock clientIntermediate

Context: The portability payoff is concrete: moving a working call from the direct API to Amazon Bedrock changes only the client constructor and the model-id prefix — the messages.create body is byte-for-byte identical.

Your task: Take the beginner call and make it run on Amazon Bedrock instead, changing only the client and the model id, and note what stays the same.

Requirements:

  • Swap anthropic.Anthropic() for AnthropicBedrock(aws_region=...), which uses IAM creds
  • Prefix the model id with anthropic. (e.g. anthropic.claude-sonnet-4-6) — Bedrock requires it
  • Leave the messages.create arguments unchanged
  • Call out that only the constructor and the id prefix differ
  • Requires AWS creds to run

💡 Hint: The anthropic. prefix is the one sharp edge — Bedrock is the only door that needs it; direct and Vertex ids stay bare.

Show solution

Only the constructor and the model-id prefix change; messages.create() is byte-for-byte identical. Bedrock requires the anthropic. prefix on the id.

from anthropic import AnthropicBedrock

client = AnthropicBedrock(aws_region="us-east-1")   # uses IAM creds
resp = client.messages.create(
    model="anthropic.claude-sonnet-4-6",            # note the anthropic. prefix
    max_tokens=256,
    messages=[{"role": "user", "content": "Say hello in one short sentence."}],
)
print(resp.content[0].text)
Exercise 3 · Build the offline provider mapperAdvanced

Context: Before you can build a factory you need the pure mapping it hides: which client class and which model-id format each provider wants. Capturing this in one offline lookup isolates the single sharp edge (Bedrock's prefix) so nothing else has to know about it.

Your task: Implement pick_provider(provider, logical_model) that returns the client class name and correctly-formatted model id for direct, bedrock, and vertex.

Requirements:

  • Return ("Anthropic", model) for direct
  • Return ("AnthropicBedrock", "anthropic." + model) for bedrock
  • Return ("AnthropicVertex", model) for vertex
  • Only Bedrock gets the anthropic. prefix
  • Raise on an unknown provider; run entirely offline with no keys

💡 Hint: A single dict keyed by provider name is the whole mapper — put the id formatting inside the table value so callers never special-case Bedrock.

Show solution

This is the lesson's mapper: a pure-stdlib lookup that hides the one sharp edge (Bedrock's id prefix). Runnable with no keys.

def pick_provider(provider, logical_model):
    table = {
        "direct":  ("Anthropic",        logical_model),
        "bedrock": ("AnthropicBedrock", f"anthropic.{logical_model}"),
        "vertex":  ("AnthropicVertex", logical_model),
    }
    if provider not in table:
        raise ValueError(f"unknown provider: {provider}")
    return table[provider]

for p in ("direct", "bedrock", "vertex"):
    print(p, pick_provider(p, "claude-sonnet-4-6"))
# bedrock -> ('AnthropicBedrock', 'anthropic.claude-sonnet-4-6')
Exercise 4 · A provider-agnostic factoryExpert

Context: The factory pattern lets business code call client.messages.create(...) and never name a provider — but the routing logic still has to be testable in CI where no SDK or cloud creds exist, so construction must stay lazy.

Your task: Wrap the mapper in make_client(provider, **kw) that returns a ready client, and keep the routing logic runnable offline without SDKs installed.

Requirements:

  • make_client returns the right client (Anthropic/AnthropicBedrock/AnthropicVertex) per provider
  • Import anthropic lazily inside the function so the module loads without the SDK
  • Expose a pure model_id(provider, logical) that adds the anthropic. prefix only for Bedrock
  • Raise ValueError on an unknown provider
  • Include an offline assertion that the id routing is correct without building a client

💡 Hint: Split the two concerns: id formatting is pure and always testable; client construction is the only part that needs the SDK, so defer that import.

Show solution

The factory centralizes the branching. Import is lazy so the routing logic is testable offline.

def make_client(provider, **kw):
    import anthropic
    if provider == "direct":
        return anthropic.Anthropic(**kw)
    if provider == "bedrock":
        return anthropic.AnthropicBedrock(**kw)
    if provider == "vertex":
        return anthropic.AnthropicVertex(**kw)
    raise ValueError(provider)

def model_id(provider, logical):
    return f"anthropic.{logical}" if provider == "bedrock" else logical

# offline sanity check of the routing (no SDK needed):
assert model_id("bedrock", "claude-sonnet-4-6") == "anthropic.claude-sonnet-4-6"
assert model_id("direct",  "claude-sonnet-4-6") == "claude-sonnet-4-6"
print("routing ok")
Exercise 5 · Config-driven provider selectionProfessional

Context: In real deployments the provider is an operational decision, not a code decision — reading it from a single env var with a safe default lets you re-route the whole app by flipping a flag, with one call site that never learns which cloud it's on.

Your task: Read the provider from LLM_PROVIDER with a safe default, resolve the model id, and keep a single provider-blind call site, then show that flipping the env var re-routes without touching business logic.

Requirements:

  • Read os.environ.get("LLM_PROVIDER", "direct") so the default is safe
  • Resolve the id with a helper that adds the anthropic. prefix only for Bedrock
  • The business function (e.g. ask) names no provider — it just calls messages.create
  • Demonstrate that changing the env value re-routes the model id with no code change
  • Run offline — routing only, no live call needed

💡 Hint: Keep exactly one place that reads the env var and one that formats the id; everything downstream should take a client and a resolved model, staying blind to the provider.

Show solution

One config value decides the door; the rest of the app stays provider-blind.

import os

PROVIDER = os.environ.get("LLM_PROVIDER", "direct")
LOGICAL  = "claude-sonnet-4-6"

def model_id(provider, logical):
    return f"anthropic.{logical}" if provider == "bedrock" else logical

def ask(client, prompt):                       # provider-blind
    return client.messages.create(
        model=model_id(PROVIDER, LOGICAL),
        max_tokens=256,
        messages=[{"role": "user", "content": prompt}],
    )

for env in ("direct", "bedrock", "vertex"):    # simulate the flag flip
    print(env, "->", model_id(env, LOGICAL))
Exercise 6 · Feature-availability guard for a multi-cloud migrationIndustry scenario

Context: Portability isn't a property of the SDK — it's a property of the feature set. A contract that forces a workload onto Bedrock can silently break code paths that rely on the Batches or Files APIs, which aren't available there, so a cheap preflight beats a broken cutover.

Your task: Write an offline preflight that, given a target provider and the features a job needs, returns whether the job can move and lists any blockers.

Requirements:

  • Model a feature → supported-providers table (e.g. messages/streaming/tools everywhere, batches/files direct-only)
  • Compute blockers as the needed features the target provider doesn't support
  • Return a result with can_move, the target, and the blockers list
  • A Bedrock job needing batches reports can_move=False with batches as a blocker
  • Run entirely offline — pure lookup, no keys

💡 Hint: Invert the question: for each needed feature, ask whether the target is in that feature's supported set — the ones that fail are exactly your blockers.

Show solution

Portability is a property of the feature set, not the SDK. A cheap preflight prevents a broken cutover.

FEATURES = {                       # feature -> providers that support it
    "messages": {"direct", "bedrock", "vertex"},
    "streaming": {"direct", "bedrock", "vertex"},
    "tools": {"direct", "bedrock", "vertex"},
    "batches": {"direct"},          # not on Bedrock/Vertex
    "files":   {"direct"},
}

def preflight(target, needed):
    blockers = [f for f in needed if target not in FEATURES.get(f, set())]
    return {"can_move": not blockers, "target": target, "blockers": blockers}

job = ["messages", "streaming", "batches"]
r = preflight("bedrock", job)
print(r)   # can_move False, blockers ['batches']
if not r["can_move"]:
    print("Refactor these off before cutover:", r["blockers"])

✓ Checkpoint — you can move on when you can…

  • Explain "run Claude anywhere": one messages.create, three interchangeable clients.
  • Construct Anthropic(), AnthropicBedrock(...), and AnthropicVertex(...).
  • Match each provider to its auth: ANTHROPIC_API_KEY / AWS IAM / GCP ADC.
  • Map a logical model name to the right id (bare vs anthropic. prefix) per provider.
  • Say which features (Batches, Files, some server tools) are NOT portable to the clouds.
  • Write a config-driven make_client factory so provider choice is one setting.

Knowledge check check yourself

✓ Knowledge check

What is the model-ID naming quirk across the direct API, Bedrock, and Vertex, and why is it the top portability footgun?

Show answer
Bedrock IDs carry an anthropic. prefix (e.g. anthropic.claude-opus-4-8) while the direct API and Vertex use the bare alias (claude-opus-4-8). Sending a Bedrock ID to Vertex (or vice versa) 404s, so centralizing IDs in one mapper is what prevents that mistake when swapping providers.
✓ Knowledge check

Why is portability a property of your feature set rather than of the SDK, and which features break a drop-in provider swap?

Show answer
Core messages, streaming, tools, and caching work on all three doors, so an app using only those is portable. But Message Batches and the Files API aren't on Bedrock or Vertex, and web search/code execution vary, so any app depending on those is not a drop-in swap and must be feature-gated per provider.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in