AI EngineeringZero to ProductionHome·About·What’s new·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

Here's a moment that catches teams off guard. You've shipped a Claude feature against the direct Anthropic API and it works beautifully. Six months later your company signs an enterprise AWS deal, and the security team says: "AI traffic has to stay inside our AWS account, under our audit controls." Your stomach drops — do you rewrite everything? The happy answer is almost nothing changes. The same Claude you've been calling is also served through Amazon Bedrock, and moving to it touches two lines of your code. This chapter is about why that's true and how to build so it stays true.

Think of Claude as a person who works in one building but has three separate lobbies you can walk in through — a public front entrance (the direct API), a door that opens into an AWS-controlled corridor (Bedrock), and one into a Google-controlled corridor (Vertex). Whichever lobby you use, you reach the same person and have the same conversation. What changes is the security desk you pass on the way in — who checks your badge, who logs your visit, whose building rules apply. The model is the person; the provider is just the lobby.

The common mistake is to treat these as three different products that each need their own integration. They don't. Bedrock and Vertex are access routes to Claude, not different models — the weights, the behaviour, the response format are identical. So the work of "supporting three providers" is not three times the work; it's one integration plus a thin layer that picks the door. Getting that mental model right up front is what saves you from the panicked rewrite, and it's why portability is worth designing for on day one rather than bolting on under deadline pressure.

three lobbies · one Claude behind them Direct API public front door Amazon Bedrock AWS corridor Google Vertex GCP corridor the same Claude same weights · same messages.create Three doors, one model. The direct API, Bedrock, and Vertex are separate lobbies with different security desks — but every one of them reaches the identical Claude. The provider is the entrance; the model is the person you meet inside.
🗺️ How to read this diagram

Read it top to bottom: three entrances across the top, all funneling down to the one box at the bottom.

  • The three coloured boxes are the front doors — the direct Anthropic API, Amazon Bedrock (inside AWS), and Google Vertex (inside GCP). Each has its own "security desk": who checks your badge and whose rules apply.
  • The three arrows all point at the same target, showing that the door you pick doesn't change where you end up.
  • The bottom box is the payoff: the identical Claude — same weights, same messages.create — no matter which lobby you walked in through.

In short: Supporting three providers isn't triple the work — it's one integration plus a thin layer that picks the door.

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

Portability isn't something you get for free — it's something you protect. The SDK makes the three clients interchangeable, but that only helps if the rest of your code stays ignorant of which one is in use. Scatter AnthropicBedrock imports and anthropic.claude-… strings across twenty files and you've quietly welded your app to one provider; the swap that could have been two lines becomes a twenty-file archaeology dig. So the real lesson of this chapter is less about the SDK and more about where you allow provider knowledge to live.

The analogy is a power adapter for international travel. Your laptop doesn't care which country it's in — it just wants clean power. The messy part (different plug shapes, different voltages) is handled by one adapter you carry, not by rewiring the laptop in every hotel. In software terms, that adapter is a client factory: a single function that absorbs all the per-provider ugliness so everything plugged into it can stay blissfully unaware. Your business logic is the laptop; the factory is the adapter; the three providers are the three countries' outlets.

Why does putting the branch in one function matter so much? Because the number of places that know about providers is the number of places that can break when a provider changes a naming rule, a region, or an auth mechanism. Keep it at one and a change is one edit; let it sprawl and every new provider becomes a hunt-and-patch across the codebase. The mistake beginners make is thinking "I'll add the abstraction later, once I actually need a second provider" — but by then the provider-specific strings have already leaked everywhere, and untangling them is precisely the work you were trying to avoid. Build the funnel first, even if today it only has one exit.

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

Master this one call and you've essentially learned all three. That's not a figure of speech — the next two recipes are this exact code with two lines swapped. So rather than skim it as "the obvious one," read it as the reference specimen: everything that follows is defined by how little it differs from this. If you can explain every line here, the Bedrock and Vertex recipes will feel like spotting the difference in two nearly-identical photos.

The one idea worth pausing on is where the secret lives. Notice you never type your API key in the code — you write Anthropic() with empty parentheses, and it quietly picks up ANTHROPIC_API_KEY from the environment. This is deliberate and it's a habit, not a nicety: a key hard-coded in a source file is a key that ends up in Git history, in a screen-share, in a pasted snippet on a forum. Reading it from the environment means the same code runs on your laptop and in production with no edits, and the secret stays out of the repo. The common rookie mistake — Anthropic(api_key="sk-ant-...") pasted straight in — works for exactly as long as it takes someone to git push it to a public repo.

Environment ANTHROPIC_API_KEY Anthropic() reads key, not code messages.create the ordinary call resp .content The key never appears in code. Anthropic() pulls ANTHROPIC_API_KEY from the environment, then the same messages.create returns the reply. Empty parentheses is the whole security habit.
🗺️ How to read this diagram

Follow the arrows left to right — the secret enters from the left and the answer comes out on the right.

  • The green box is the environment holding ANTHROPIC_API_KEY — the key lives here, outside your source file and out of Git history.
  • The middle boxes are the two code lines: Anthropic() with empty parentheses quietly reads that key, then messages.create makes the ordinary request.
  • The arrow out to resp is the reply you read via resp.content[0].text.

In short: Read the key from the environment, and the same code runs on your laptop and in production with the secret never touching the repo.

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

This is the recipe most enterprises actually ship. If your company runs on AWS — and a huge share do — Bedrock is the door that lets you use Claude without a single byte of AI traffic leaving the account your security and finance teams already govern. The request goes to Claude, gets answered by Claude, and never appears on a separate Anthropic invoice or under a separate data-processing agreement. For a regulated shop, that difference isn't cosmetic; it's the difference between "approved" and "blocked."

The mental shift from Recipe 1 is about who vouches for you. On the direct API you carry a bearer key — like showing a membership card. On Bedrock there's no key at all; instead you arrive as an identity your AWS account already knows (an IAM role attached to your Lambda or EC2, or your local ~/.aws profile), the way an employee badge opens the office because the building's own system recognizes it. Auth becomes AWS's job, not Anthropic's. That's why the constructor takes an aws_region and no secret — the credential is ambient, supplied by the environment your code runs in.

Two gotchas trip nearly everyone the first time, and both are AWS-side, not code. First, your IAM principal needs permission to call bedrock:InvokeModel — without it you get an access-denied error that looks like a bug in your code but isn't. Second, and more surprising: a fresh AWS account has Claude switched off until you explicitly request model access in the Bedrock console for that region. Miss this and the call fails even with perfect credentials. And note the model-ID quirk that defines this door: Bedrock IDs carry an anthropic. prefix (anthropic.claude-opus-4-8). Send a bare alias here and it 404s; send this prefixed form to Vertex and that 404s — which is exactly the sharp edge the mapper in section 7 exists to hide.

AWS identity IAM role · no API key AnthropicBedrock inside your AWS account model id anthropic.claude… two AWS-side preconditions must be true first: IAM: bedrock:InvokeModel Bedrock console: model access on No key — an identity. Bedrock authenticates you as an AWS IAM principal, routes the request inside your account, and demands the anthropic.-prefixed id. Two AWS-side gates (invoke permission and enabled model access) must be set before the first call succeeds.
🗺️ How to read this diagram

Read the top row left to right — that's the request path — then the two red boxes below, which are the setup that must already be in place.

  • The amber box on the left is your AWS identity: an IAM role or local profile. There is no API key — the credential is ambient.
  • The middle box is AnthropicBedrock, which sends the request inside your own AWS account, and the right box is the model id with its Bedrock-only anthropic. prefix.
  • The two red boxes are the gotchas: your principal needs bedrock:InvokeModel, and Claude must be enabled in the Bedrock console for that region. Miss either and the call fails even with perfect code.

In short: Same request as the direct API, but auth is AWS's job and the id gains a prefix — plus two console/IAM gates you set once.

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

Same story, third cloud. If your organization lives on Google Cloud instead of AWS, Vertex AI is your matching door — the exact analogue of Bedrock, just wearing Google's badge system instead of Amazon's. By now you should be able to predict what changes before you even read the code: the client constructor and the model-ID, and nothing else. If that prediction feels obvious, the chapter has done its job; the whole point of "run anywhere" is that the third example teaches you nothing new about the conversation, only about the doorway.

The doorway detail here is Application Default Credentials (ADC), and it's worth understanding rather than memorizing. Google's idea is that your code should never hold a Google secret directly; instead it asks the surrounding environment "who am I?" and the environment answers. On your laptop you establish that identity once by running gcloud auth application-default login — like signing in to your work laptop in the morning so every app inherits your identity. On GCP compute, the attached service account plays the same role automatically. So you hand the constructor a project_id and a region — where to run — but never a credential; the credential is discovered, just as Bedrock discovered your IAM role.

The mistake to avoid is assuming the model-ID rule matches Bedrock's. It doesn't, and that asymmetry is the whole reason model-IDs are the top portability footgun. On Vertex, current-generation models use the bare alias — claude-opus-4-8, no prefix — the same string the direct API uses and the opposite of Bedrock's prefixed form. (Older dated snapshots on Vertex use an @date suffix, but that's a versioning detail, not the everyday case.) Two clouds, two different naming conventions, one bare-vs-prefixed trap: this is precisely why you'll centralize the ID in a single mapper rather than typing it at each call site.

Google ADC discovered, not passed AnthropicVertex project_id · region model id claude-opus-4-8 bare alias — same as direct API, opposite of Bedrock's prefix you pass where to run, never a credential Discovered identity, bare id. Vertex authenticates through Application Default Credentials — you hand it project_id and region but never a secret — and its model id is the plain alias, matching the direct API and diverging from Bedrock.
🗺️ How to read this diagram

Follow the top row left to right; the two lines beneath it call out the model-id rule.

  • The red box is Google Application Default Credentials — established once with gcloud auth application-default login or supplied by the compute's service account. The credential is discovered, never typed into code.
  • The middle box is AnthropicVertex, taking a project_id and region — where to run, not who you are.
  • The green box is the model id: the bare alias, identical to the direct API and the opposite of Bedrock's prefixed form — the exact asymmetry the mapper exists to absorb.

In short: Vertex mirrors Bedrock (identity, not key) but flips the id rule back to bare — two clouds, two conventions, one trap.

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's ever going to confuse you about these three doors fits in the table below — that's genuinely good news. You've now written the same request three times; the reason it felt repetitive is that it is repetitive by design. This section collapses the three recipes into one grid so you can see, at a glance, the small set of things that actually differ. When a call fails after a provider swap, this table is your first stop: the bug is almost always in one of these four rows, never in the fifth.

Read the table as two zones with very different meanings. The top four rows (client, auth, model-ID, install extra) are the per-provider knobs — the plug shape and voltage from the adapter analogy. The bottom row is the promise: messages.create is identical everywhere. That single "identical" is what portability rests on. Everything you know about prompts, system messages, tools, streaming, and caching transfers untouched; you are only ever relearning the four setup rows, and even those you'll soon hand to a factory so you never think about them again.

The subtle trap in this table is treating the four variable rows as independent when they travel together. You don't mix a Bedrock client with a bare model-ID, or a Vertex client with the anthropic. prefix — each column is a consistent set. Pick a provider and you inherit its whole column at once: its client class, its auth mechanism, its ID format, its install extra. That coupling is exactly why the next section encodes the columns as a lookup rather than leaving four independent decisions scattered across your code, where they can drift out of sync.

what varies per provider vs what stays the same Client — Anthropic / AnthropicBedrock / AnthropicVertex Auth — API key / AWS IAM / GCP ADC Model-ID — bare / anthropic. prefix / bare Install extra — anthropic[bedrock] / [vertex] ↑ four knobs the factory sets from one config value messages.create — identical everywhere Four knobs, one promise. Only the top four rows differ across providers; the request body is byte-for-byte the same on all three. Portability rests entirely on that single "identical" bottom row.
🗺️ How to read this diagram

Read it top to bottom as two zones separated by the amber note.

  • The four amber rows are the per-provider knobs — client class, auth mechanism, model-id format, and pip install extra. Each one changes depending on which door you pick.
  • The amber caption line is the key insight: those four rows are exactly what a factory sets from a single config value, so you never touch them by hand.
  • The green bottom row is the promise — messages.create is identical on every provider. Everything you know about prompts, tools, streaming, and caching transfers untouched.

In short: When a swap breaks, the bug is in one of the four amber rows — never in the green one.

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

Here's the small function that quietly prevents the most common production incident in a multi-provider app. Remember the bare-vs-prefixed model-ID trap? Left to chance, it surfaces at the worst possible moment: you flip your app to Bedrock for a compliance deadline, and every request 404s because a model-ID string buried in some helper was written for Vertex. The fix isn't to "be more careful" — it's to make it impossible to be careless, by giving the naming rules exactly one home.

Think of this mapper as the single source of truth for a fact that would otherwise be scattered as folklore across your codebase. It's the difference between a company where everyone remembers the office alarm code (until one person misremembers) and a company where the code is written in one locked, auditable place. The value isn't the three lines of logic — it's that the anthropic. prefix rule now exists in one location you can point to, test, and change. When Anthropic ships a new model or a naming convention shifts, you edit one dict instead of grepping the repo and praying you caught every occurrence.

Notice a deliberate design choice that pays off enormously: this helper is pure stdlib — no SDK import, no key, no network — so it runs with a plain python file.py and, crucially, in your CI test suite. The temptation is to bake the ID formatting into the client-building code, but that welds a pure decision (which string?) to an impure one (build a real client, needs creds). Keeping them apart means the sharp, error-prone part — the naming — is the part you can unit-test to death offline, while the part that needs credentials stays thin. That separation is what makes the whole abstraction trustworthy rather than just tidy.

input provider, "opus" pick_provider() applies the naming rule api / vertex → bare claude-opus-4-8 bedrock → prefixed anthropic.claude-opus-4-8 One function, one home for the rule. Feed the mapper a provider and a logical name; it returns the client class and the exact model id — adding the anthropic. prefix only on the Bedrock branch, so no call site has to remember it.
🗺️ How to read this diagram

Read left to right: a request goes in, the transform happens in the middle, two possible outputs come out on the right.

  • The left box is the input — a provider plus a logical model name like "opus", so your app never types a raw id.
  • The middle box is pick_provider, the one place the naming rule lives.
  • The two right boxes are the outcomes: the green one shows api and vertex getting the bare alias, while the amber one shows bedrock getting the anthropic. prefix. The branch is the entire value of the function.

In short: The prefix rule exists in exactly one testable place — add a new model and it inherits the rule for free.

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

Sooner or later someone senior will ask you: "why are we on Bedrock and not just calling Anthropic directly?" — and "the model is the same either way" is the answer that gets you in trouble. It's true, and it's beside the point. The person asking already assumes the model is identical; what they want to know is which non-model factors drove the choice. This section is your cheat sheet for that conversation, because portability is only worth having if you can articulate why you'd actually exercise it.

Here's the reframe that makes the decision click: you're not choosing a model, you're choosing a billing relationship, an identity system, and a data-residency guarantee. Picture buying the identical bottle of water from a hotel minibar, a corner shop, or your own fridge — same water, wildly different context around it. The direct API is the corner shop: quick, no commitment, but a separate receipt. Bedrock and Vertex are your own fridge: the water is already inside the house you control, on the bill you already pay, behind the locks you already manage. For a regulated enterprise, "inside the house" is frequently non-negotiable, and that — not model quality — settles the choice.

The mistake to avoid is optimizing the wrong axis. Teams sometimes agonize over latency micro-differences or chase whichever door is fractionally cheaper this quarter, when the real driver is sitting in the compliance column. A blunt but reliable heuristic: if you already live in AWS or GCP, use the matching door — your AI spend, identity, and data then stay in the cloud your organization already trusts and audits, and you inherit its committed-use discounts for free. If you have no such constraint, the direct API wins on simplicity and gets new features first. The table below sorts the genuine drivers so you can point at the one that actually applies rather than defending a gut call.

what's driving the choice Data residency / compliance Existing AWS / GCP commitment Identity & access controls Simplicity / newest features first the matching cloud door the direct API You choose a context, not a model. Compliance, existing cloud spend, and identity push you to the matching cloud door (Bedrock or Vertex); a free hand and a wish for the newest features point to the direct API. Model quality never enters it — it's identical.
🗺️ How to read this diagram

Read left to right: the drivers on the left, the arrows carrying them to the door they select on the right.

  • The three amber boxes — data residency, existing cloud commitment, and identity — all point to the matching cloud door: use Bedrock if you're on AWS, Vertex if you're on GCP.
  • The green box — simplicity and getting new features first — points to the direct API, the least-setup path.
  • Notice what's absent: model quality. Every arrow is about billing, governance, and location — never the model, because the model is the same on all three.

In short: If you already live in AWS or GCP, use the matching door; with no such constraint, the direct API wins on simplicity.

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

This is the section that keeps a cheerful "it's fully portable!" from becoming a broken cutover. Everything so far has sold you on sameness — same model, same call, same response. Now the honest caveat: the models are identical, but the surrounding features are not all present on every door at the same time. New capabilities land on the direct Anthropic API first and roll out to Bedrock and Vertex on their own schedules. Ignore that and you can ship a workload to a cloud where a feature it depends on simply doesn't exist — a failure that no amount of clean factory code prevents.

The reframe that saves you is this: portability is a property of your feature set, not of the SDK. The SDK will happily build you a Bedrock client; whether your app survives on Bedrock depends entirely on which features it leans on. Think of it like electrical appliances abroad — the plug adapter (the SDK) fits every socket, but a 110-volt appliance still won't run on 240-volt power. An app that uses only messages.create with tools and caching is the dual-voltage appliance: it runs anywhere. An app built on the Message Batches API or the Files API is single-voltage — it will not run on Bedrock or Vertex, because those endpoints aren't there at all, adapter or no adapter.

So the discipline is to know which bucket each code path is in before you assume a swap is free. The core — messages, streaming, tools, prompt caching, extended thinking — is everywhere, which is why portability is real for the large majority of applications. The gaps live in the newer, peripheral surfaces: Batches, Files, and some server-side tools like web fetch and code execution. The common and expensive mistake is discovering this at runtime, with a 404 in production during a compliance-driven migration. The fix is cheap and offline: enumerate the features your workload actually uses, check them against the table below, and gate anything provider-specific loudly at startup rather than letting it fail on a customer's request.

Portable core works on all three doors messages · streaming tools · prompt caching extended thinking dual-voltage — runs anywhere Peripheral gaps direct-only or partial Message Batches API Files API web fetch / code execution single-voltage — gate the swap Portability is a property of your feature set. The core (green) is everywhere, so most apps are portable. The newer, peripheral surfaces (red) aren't on Bedrock/Vertex — an app that leans on them won't survive a swap, adapter or not.
🗺️ How to read this diagram

Two side-by-side buckets — sort each feature your app uses into one of them.

  • The green bucket is the portable core: messages, streaming, tools, prompt caching, extended thinking. These work identically on all three doors — the "dual-voltage appliance" that runs anywhere.
  • The red bucket is the peripheral gaps: Message Batches, Files, and some server tools like web fetch and code execution. These are direct-API-only or partial — "single-voltage," so a swap to Bedrock/Vertex breaks them.
  • The payoff is the discipline: enumerate your features, drop each into a bucket, and gate anything in the red bucket loudly at startup instead of 404-ing in production.

In short: Only-core app → portable today; Batches/Files app → not a drop-in swap. Know which bucket you're in before you assume a move is free.

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

Everything in this chapter builds to one sentence a tech-lead should be able to say with a straight face: "moving providers is a config change, not a code change." That sentence is a promise to your security team, your finance team, and your future on-call self — and this section is the code that lets you keep it. When the compliance review lands and someone asks "how fast can we get this onto Bedrock?", the answer you want to give is "flip one environment variable and redeploy," not "give me a sprint."

The pattern is a single funnel. Recall the power-adapter and the "one locked room for the alarm code" analogies — here they finally converge into one factory function that is the only place in the entire application allowed to name a provider. Business logic asks the factory for a client and then calls messages.create; it never imports AnthropicBedrock, never sees an aws_region, never learns which cloud answered. The two provider-aware concerns — which client (the factory) and which model-ID (the mapper from section 7) — are quarantined into two small, testable functions, and the rest of your codebase stays provider-blind.

The mistake that silently erodes this — and it happens on real teams — is the "just this once" leak: a developer under deadline imports AnthropicVertex directly in some module because it's faster than plumbing through the factory. Each such shortcut re-welds a piece of the app to one provider, and the config-change promise quietly dies the death of a thousand cuts. So the discipline is enforceable, not aspirational: no module except the factory imports a provider client, and you can grep for violations in CI. Read the code below as the template you'd wire into a real service, then study the tech-lead checklist that follows — it turns this single pattern into the handful of rules that keep an integration portable under real-world pressure.

business module business module business module make_client() only provider-aware code messages.create provider-blind ✗ "just this once" import of AnthropicVertex — disallowed One funnel, one exit. Every business module routes through make_client() — the sole place allowed to name a provider — then calls a provider-blind messages.create. A direct provider import (red) bypasses the funnel and quietly re-welds the app to one cloud.
🗺️ How to read this diagram

Read it left to right as a funnel, then note the red box below as the anti-pattern.

  • The three blue boxes on the left are your business modules — they all converge on one point instead of each knowing about providers.
  • The middle box is make_client(), the only provider-aware code; the arrows fan into it because it's the single funnel. From there the green box is the provider-blind messages.create that never learns which cloud answered.
  • The red box is the "just this once" leak — a module importing AnthropicVertex directly, bypassing the funnel. The dashed line shows it sneaking past the factory; this is the shortcut you grep for and forbid in CI.

In short: Keep exactly one door out of the funnel and "moving providers is a config change, not a code change" stays true.

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