AI EngineeringZero to ProductionHome·About·What’s new·Contact
Anthropic Skills · Chapter K7

Claude on Vertex / GCP

The third front door to Claude — Vertex AI on Google Cloud: ADC auth, the SDK, streaming/tool use, mapping across API/Bedrock/Vertex, and abstracting the provider behind one client.

⏱️ ~2.5 hours🧪 6 labs🎯 Beginner→Tech-lead

Learning objectives

  • Explain the three front doors to Claude (API, Bedrock, Vertex).
  • Authenticate to Vertex with Google Cloud ADC.
  • Call Claude on Vertex incl. streaming and tool use.
  • Choose a front door and standardize it for a team.
▶ Runnable companionCode saved under code/ak7-claude-vertex-gcp/. Python runs offline; configs are ready to use.

1 · Three front doors, one Claude essential

Imagine your company just signed a big Google Cloud contract, and legal has ruled that customer data may not leave GCP. You already have a Claude-powered feature humming along on the direct Anthropic API — and now someone asks, "Can we keep it, but run it inside Google Cloud?" The panic-inducing assumption is that you'll rewrite the whole thing against some foreign Google API. You won't. This chapter is the reassuring answer: it's the same Claude, reached through a different lobby, and the rewrite is about three lines.

Here's the mental model that makes the rest of the chapter click. Think of Claude as a single artist whose paintings you want to buy. You can walk into the artist's own gallery (the Anthropic API), or you can buy the exact same painting through two big department stores that resell it — Amazon's (Bedrock) and Google's (Vertex AI). The painting is identical no matter where you buy it. What changes is whose till you pay at, which membership card you show at the door, and which loading dock the delivery comes from. That's billing, authentication, and region — nothing about the art itself.

Why would anyone use a reseller instead of the gallery? Because big organizations don't buy on convenience, they buy on governance. If your company already runs on Google Cloud, then routing Claude through Vertex means the AI spend shows up on the same invoice you already reconcile, access is controlled by the same Google IAM roles your security team already audits, and — the clincher for regulated industries — the data stays inside the GCP project and region you've already had approved for compliance. None of that makes Claude smarter; it makes Claude adoptable inside an enterprise that has already drawn its trust boundaries around one cloud.

The common mistake is to treat these three as three different products to learn. They aren't. Every prompt you've written, every streaming loop, every tool-use schema from the earlier chapters transfers verbatim. Internalize that now and you'll read the rest of this page as "spot the two things that differ" rather than "learn a new SDK." So, the precise version:

You've reached Claude via the Anthropic API and Bedrock. Vertex AI is the third — Claude on Google Cloud, billed through GCP with Google's auth, IAM, and data residency. Same model, same Messages concepts; different plumbing.

Anthropic API api key Bedrock (AWS) IAM role Vertex (GCP) gcloud ADC
🗺️ How to read this diagram

This picture makes one reassuring point: there are three ways in to the very same Claude models, and picking one is mostly about whose bill and login you use, not about learning a new AI. Read the three boxes left to right.

  • Anthropic API (left) — the direct door. You sign in with an api key (a secret string from Anthropic). Simplest to start with.
  • Bedrock (AWS) (middle) — Claude served through Amazon's cloud. Instead of an API key you use an IAM role (AWS's permission system), so the bill and access control live in AWS.
  • Vertex (GCP) (right) — Claude served through Google Cloud. You sign in with gcloud ADC (Google's Application Default Credentials). This lesson is about this door.
  • The arrows just show these are three entrances; behind all of them sits the same Claude. What changes between doors is only the client object you create and the auth (key vs IAM vs ADC) — not how you write prompts.

In short: Same brain, three lobbies. Enterprises already living in AWS or GCP pick the matching door so AI spend, logins, and data stay in the cloud they already trust.

2 · Authenticate with ADC essential

The single most common way a Vertex integration fails on day one is authentication — and almost always because someone goes looking for the "Vertex API key," doesn't find one, and assumes something is broken. There is no key. That absence is not a bug; it's the whole design, and once you understand why, ADC stops feeling mysterious and starts feeling safer than what you were doing before.

Think about how a hotel used to work versus how it works now. The old way is a physical brass key you carry around: lose it, and whoever finds it can open your room forever. An API key is exactly that brass key — a long secret string that grants access to anyone holding it, which is why keys leaked in Git commits are such a recurring disaster. Google's Application Default Credentials (ADC) are the modern keycard system instead: you prove who you are at the front desk once, and the building quietly issues short-lived, automatically-refreshed passes to your code behind the scenes. Your program never holds a permanent secret; it just says "I'm the process running as this identity," and Google's libraries find and present the right credential for you.

Concretely, ADC means the same code works in two very different places without change. On your laptop, you run one gcloud login command and Google caches a credential in a well-known file on your machine; the SDK finds it automatically. In production — a Cloud Run service, a GKE pod, a Compute Engine VM — there's no login at all: the machine has a service account attached to it, and the SDK picks that up instead. Your code literally doesn't know or care which of the two it's running under, because in both cases it just asks the environment "who am I?" and gets an answer.

The mistake to avoid is trying to "help" ADC by pasting a downloaded service-account JSON key file into your repo or an environment variable in your source. That reintroduces the exact brass-key problem ADC was built to eliminate. If you did the boto3 work in W1, this is the identical discipline — no long-lived secrets in code, ever — just spelled in Google's dialect. Now the precise mechanics:

Vertex uses Google Application Default Credentials — no API key. Locally you authenticate once with gcloud; on GCP compute you use the attached service account. Same "no keys in code" discipline as boto3 (W1).

brass key · a permanent secret ANTHROPIC_API_KEY long-lived secret in code/env keycard · short-lived passes ADC no secret in your code ADC asks the environment "who am I?" → same code, two sources laptop gcloud login → cached file GCP compute attached service account No key, by design. An API key is a permanent secret you carry (and can leak); ADC issues short-lived credentials the SDK finds automatically — from a cached gcloud login on your laptop, or an attached service account in production.
🗺️ How to read this diagram

Read the top row as the contrast, then the bottom row as the two places ADC gets its answer.

  • The top-left red box is the old way: an ANTHROPIC_API_KEY — a long-lived secret string that lives in your code or environment and grants access to anyone who copies it.
  • The top-right green box is ADC: your code holds no secret. It just asks the environment for a short-lived credential, so there is nothing permanent to leak.
  • The two bottom boxes show the payoff — the same code works in both. On a laptop, ADC reads the file cached by your one-time gcloud login; on GCP compute, it reads the machine's attached service account. The arrows show both feeding the one "who am I?" question.

In short: There is no Vertex API key on purpose — ADC resolves a short-lived credential from the environment, so the identical code authenticates on your laptop and in production without any secret in the source.

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.
shell · one-time setup
setup.shpip install "anthropic[vertex]"
gcloud auth application-default login
gcloud config set project my-gcp-project
gcloud services enable aiplatform.googleapis.com
▶ How this works

Before any code can reach Claude on Vertex, you do a one-time setup in your terminal: install the right library and log in to Google Cloud. These are shell commands (you run them in a terminal, not in Python).

  1. pip install "anthropic[vertex]" installs Anthropic's Python library plus the extra Vertex pieces (the [vertex] part). This is what gives you the AnthropicVertex client used later.
  2. gcloud auth application-default login opens your browser to sign in to Google. It saves credentials on your machine so your code can authenticate without any key in the code — that saved login is what "ADC" (Application Default Credentials) means.
  3. gcloud config set project my-gcp-project tells Google which project (billing + resources bucket) to use. gcloud services enable aiplatform.googleapis.com switches on the Vertex AI service for that project — like flipping the breaker before you use the appliance.

What the output means: Nothing prints a result you keep — these commands just prepare your machine and project. After they succeed, your Python can talk to Vertex.

Try this: Run gcloud config list afterward to confirm your project is set. If a later call fails with a permissions error, it's almost always one of these four steps that was skipped.

3 · Call Claude on Vertex intermediate

This is the moment the chapter's promise pays off, and it's almost anticlimactic — which is exactly the point. If you were bracing for a new request format, new parameters, a new way to read the response, relax: there isn't one. The call you're about to write is the call you already know from C2, with two small substitutions. When people say "porting to Vertex is easy," this section is the proof.

Picture a wall socket. In your home country you plug an appliance straight into the wall. Travel abroad and the appliance still works identically — you just clip on a plug adapter first. AnthropicVertex is that adapter. The appliance (your messages.create call, your prompts, your response handling) is unchanged; you've only swapped the thing that connects it to the wall. Instead of Anthropic(), which reads an API key, you build AnthropicVertex(region=..., project_id=...), which quietly reaches for the ADC credentials from the previous section. Same appliance, different plug.

The one genuinely new detail worth slowing down for is the model ID format, because it's where a first attempt usually errors out. On the direct API a model is named plainly, like claude-3-5-sonnet-v2. On Vertex the same model carries an @ and a date-stamped version suffix — claude-3-5-sonnet-v2@20241022. That @date is Vertex's way of pinning you to an exact, immutable version so a model update can't silently change your outputs. It's the same idea as pinning a package version in requirements.txt rather than grabbing "latest": predictability over surprise.

The classic mistake here is copying a model ID from the wrong place — pasting the direct-API name into a Vertex call, or an old date suffix that the region no longer serves — and then reading the resulting error as an auth problem. It usually isn't; it's a model-ID mismatch. When a Vertex call fails, check the model string against the current Vertex docs before you go re-running gcloud logins. With that caveat noted, here's the real thing:

The AnthropicVertex client mirrors the base SDK — set region + project, then use the same messages.create from C2. Model IDs use Vertex naming (an @ version suffix).

direct Anthropic API Anthropic() claude-3-5-sonnet-v2 Vertex — swap the plug AnthropicVertex(...) ...v2@20241022 client.messages.create(...)— unchanged the appliance you plug in stays the same Two substitutions, nothing else. Swapping Anthropic() for AnthropicVertex(...) and the plain model name for the @date-pinned Vertex id is the entire port — messages.create is byte-for-byte the same.
🗺️ How to read this diagram

Read left (direct API) to right (Vertex): the two arrows mark the only two things that change, and the box at the bottom is what stays shared.

  • The top pair of boxes is the client: Anthropic() becomes AnthropicVertex(region, project). The arrow is the "swap the plug" step.
  • The middle pair is the model id: the plain name gains an @ and a date suffix (@20241022), which pins you to one immutable version so an update can't silently change your outputs.
  • The bottom box — client.messages.create(...) — sits below both columns because it is unchanged. Your prompts, parameters, and response handling all transfer verbatim.

In short: Change the client constructor and the model id (add the @date); leave the messages.create call exactly as it was.

Python · a first call (needs GCP creds to run)
vertex_call.pyfrom anthropic import AnthropicVertex
client = AnthropicVertex(region="us-east5", project_id="my-gcp-project")

resp = client.messages.create(
    model="claude-3-5-sonnet-v2@20241022",
    max_tokens=300,
    system="You are a concise SRE assistant.",
    messages=[{"role": "user", "content": "In one sentence, what is Vertex AI?"}])
print(resp.content[0].text)
▶ How this works

This is the whole shape of a Claude call on Vertex — and it's deliberately almost identical to the direct-API call from earlier lessons. Only the first two lines (the client) and the model id are Vertex-specific.

  1. from anthropic import AnthropicVertex imports the Vertex flavor of the client. client = AnthropicVertex(region="us-east5", project_id="my-gcp-project") builds it — you pass a Google Cloud region and your project instead of an API key, because the login you did in setup is picked up automatically.
  2. client.messages.create(...) is the exact same request method as the direct API. max_tokens caps the reply length; system sets the assistant's role; messages is the conversation (here one user question).
  3. model="claude-3-5-sonnet-v2@20241022" uses Vertex's naming style — note the @ and date version suffix. That naming is the main thing that differs from the direct API's model ids.
  4. resp.content[0].text pulls the text out of the reply. The reply's content is a list of blocks, so [0] grabs the first block and .text its words.

What the output means: Prints Claude's one-sentence answer describing Vertex AI. (This block is marked "needs GCP creds" because a real network call to Google requires the login from step 2.)

Try this: Compare this to the first-call code in the Anthropic-API lesson: swap Anthropic() for AnthropicVertex(region=..., project_id=...) and change the model id — everything else is the same. That's the whole point of this chapter.

The runnable blocks below need no cloudReal Vertex calls need GCP credentials, so that call is marked. The mapping/normalization logic below runs offline so you can execute and understand the portability without an account.

4 · Map concepts across all three doors intermediate

Six months from now, someone on your team will ask "how hard would it be to move this off Vertex?" — and your answer decides whether that's a weekend or a quarter. The difference comes down to a habit you can build right now: knowing, precisely, which parts of your code are provider-specific and which parts are universal. This section draws that line so sharply that the migration answer becomes "change one config value."

Here's a way to feel it. Think of driving a rental car in a new city. Everything that matters to actually driving — the steering wheel, the pedals, reading road signs — is identical to your own car; that knowledge transfers completely. What differs is trivial and local: which slot the key goes in, where the fuel cap release hides, which side the indicator stalk is on. If you catalogue those three quirks up front, you drive away in thirty seconds. If you don't, you sit in the parking lot hunting for the handbrake. The three Claude front doors are exactly this: the "driving" (your Messages request, streaming, tools) is shared, and only a tiny, enumerable set of quirks differs — the client class, the auth mechanism, and the model-ID string.

Making that mapping explicit — writing it down as data, as the lab below does — is what turns a scary rewrite into a lookup. When the three differences live in one small table instead of scattered across your codebase, swapping providers means editing that table, not hunting through business logic for hard-coded assumptions. This is the same instinct behind a good config file: pull the things that vary into one obvious place so the code that uses them can stay ignorant of the choice.

The trap to watch for is quietly assuming more transfers than actually does. The core Messages surface is genuinely identical — but not every server-side feature is available on every door (you'll map those capability gaps in the ladder exercises). So "everything transfers" is true of the request shape, and you should still verify individual features per provider before you promise them. With that honest caveat, the mapping itself:

Everything transfers — only the client construction and model id change. Model the mapping so switching front doors is a config change, not a rewrite.

differs per door (the enumerable quirks) api Anthropic() ANTHROPIC_API_KEY bedrock boto3 / IAM role anthropic.claude-…:0 vertex AnthropicVertex() gcloud ADC · …@date messages.create(...) — IDENTICAL prompts · streaming · tools schema catalogue the 3 quirks once → switching doors is a config edit Draw the line explicitly. Three doors differ in only client, auth, and model-id string; the whole Messages request shape is shared — so the mapping is a tiny table, and a provider switch edits that table, not your business logic.
🗺️ How to read this diagram

Read the top band (three doors, side by side) as "what differs," and the green box below as "what everyone shares."

  • The three amber boxes — api, bedrock, vertex — each list that door's quirks: its client class, its auth, and its model-id format. These are the only things that vary.
  • The arrows converge from all three onto the green box: messages.create(...). That box holds everything that is identical — your prompts, streaming, and tool schemas.
  • The payoff is the caption's point: because the differences are a short, enumerable list, you write them down as data (a table) once; a migration then means editing that table, not hunting through code.

In short: Only client + auth + model-id differ; the request shape is shared — so switching providers is a config change, not a rewrite.

Python · one config, three front doors (runs offline)
front_doors.pydef client_config(door):
    return {
        "api":     {"client": "Anthropic()",       "auth": "ANTHROPIC_API_KEY",
                    "model": "claude-opus-4-8"},
        "bedrock": {"client": "boto3 bedrock-runtime","auth": "IAM role",
                    "model": "anthropic.claude-3-5-sonnet-20241022-v2:0"},
        "vertex":  {"client": "AnthropicVertex()",  "auth": "gcloud ADC",
                    "model": "claude-3-5-sonnet-v2@20241022"},
    }[door]

for door in ["api", "bedrock", "vertex"]:
    c = client_config(door)
    print(f"{door:8} {c['client']:26} auth={c['auth']}")
print("messages shape is IDENTICAL across all three")
api      Anthropic()                auth=ANTHROPIC_API_KEY
bedrock  boto3 bedrock-runtime       auth=IAM role
vertex   AnthropicVertex()           auth=gcloud ADC
messages shape is IDENTICAL across all three
▶ How this works

This tiny program (which runs offline — no cloud needed) proves the chapter's big idea: the three doors differ in only three things — the client you build, the auth you use, and the model id. Everything else about a request is identical.

  1. client_config(door) is a function returning a small dictionary of facts for one door. The { ... }[door] pattern builds a lookup table of all three doors and then immediately picks the one named by door.
  2. Each entry lists that door's client (Anthropic(), boto3, or AnthropicVertex()), its auth method (API key / IAM role / gcloud ADC), and its model id string. Notice the model ids differ in format but name the same models.
  3. for door in ["api", "bedrock", "vertex"]: loops over all three and prints a lined-up row for each, using f-string alignment (:8, :26 pad to fixed widths so the columns line up).
  4. The last print states the punchline in words: the messages shape — how you phrase the actual request — is identical across all three.

What the output means: Three aligned rows (one per door) showing client / auth / model, then the line messages shape is IDENTICAL across all three — exactly the console output shown just below the code.

Try this: Add a fourth fictional door to the dictionary and to the loop list, then re-run. The table grows automatically — because the loop, not repeated code, does the printing.

5 · Advanced — streaming & tool use on Vertex advanced

It's tempting to assume the "hard" features — streaming responses, tool use — are where the provider abstraction finally breaks down. That's the natural fear: the basic call ports easily, sure, but surely the fancy stuff has provider-specific catches? Here's the payoff of everything you've read so far — it doesn't. Streaming and tool use on Vertex are byte-for-byte the same code as on the direct API. This section exists mainly to convince you of that, because believing it is what lets you commit to the abstraction in the next section.

Streaming is worth a plain-language beat first, since it's easy to mis-picture. A normal call is like ordering a coffee and waiting at the counter until the whole drink is done before it's handed to you. Streaming is like watching the barista pour — you get the response as it's produced, token by token, so a user sees words appear in real time instead of staring at a spinner. That matters enormously for perceived speed: the total time is similar, but the experience of a chatbot that types feels dramatically more responsive than one that goes blank and then dumps a paragraph. The magic that makes this ergonomic in Python is the with ... as stream context manager, which opens the live connection and — crucially — guarantees it gets closed cleanly even if your loop throws halfway through.

Tool use is the other "advanced" capability, and the same reassurance holds: you hand Claude a tools=[...] schema describing functions it may call, and you run the same request/response loop you built in C2. The definitions, the loop structure, the way you feed tool results back — all identical across doors. The point being hammered, again, is that the only Vertex-specific lines are the client constructor and the model ID.

The one honest asterisk — and the mistake would be to forget it — is that "the SDK surface is identical" is not the same as "every server-side tool is enabled on every door." The client-side patterns transfer perfectly; whether a specific built-in server tool is actually available on Vertex is a per-provider fact to check against the capability map, not to assume from the code looking the same. Keeping those two ideas separate (identical code shape vs. per-door feature availability) is what stops you promising a feature that isn't there. Now the code:

Streaming uses the same with client.messages.stream(...) context manager; tool use uses the same tools=[...] schema and loop from C2. Only client + model id differ.

non-streaming: wait, then dump spinner … (blank) whole paragraph at once streaming: tokens as produced to·ken·by·token with ... as stream: closes cleanly SAME code on Vertex & direct API — only client + model id differ Streaming = watch the barista pour. A plain call blanks until the whole reply lands; streaming hands back tokens as they are produced, so the UI feels responsive. The with … as stream block guarantees the connection closes even if the loop throws — and it's the identical code on Vertex.
🗺️ How to read this diagram

Compare the two columns top to bottom; the divider separates the slow experience from the responsive one.

  • The left column is a non-streaming call: first a blank spinner, then — after the arrow — the whole paragraph dumped at once. Total time is fine, but the wait feels long.
  • The right column is streaming: the green cells are tokens arriving one after another along the arrow, so words appear in real time. The note reminds you the with … as stream context manager opens and cleanly closes the live connection.
  • The bottom line is the chapter's payoff: this streaming (and tool-use) code is the same on Vertex as on the direct API — only the client and model id are Vertex-specific.

In short: Streaming trades a blank wait for real-time tokens, and the loop is byte-for-byte identical across doors.

Python · streaming on Vertex (needs GCP creds)
vertex_stream.pyfrom anthropic import AnthropicVertex
client = AnthropicVertex(region="us-east5", project_id="my-gcp-project")
with client.messages.stream(
    model="claude-3-5-sonnet-v2@20241022", max_tokens=200,
    messages=[{"role": "user", "content": "Count to five slowly."}]) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
print()
▶ How this works

Streaming shows Claude's reply as it's typed, chunk by chunk, instead of waiting for the whole answer. The key lesson here: streaming on Vertex uses the exact same code as the direct API — only the client and model id are Vertex-specific.

  1. The first two lines build the same AnthropicVertex client as before (region + project, no key).
  2. with client.messages.stream(...) as stream: opens a live streaming connection. The with block guarantees it closes cleanly when finished. This is the identical method name used on the direct API.
  3. for text in stream.text_stream: receives small text chunks as Claude produces them. print(text, end="", flush=True) prints each chunk immediately with no line break, so words appear to flow in real time. The final bare print() just adds a newline at the end.

What the output means: You'd watch "Count to five slowly." get answered progressively, the digits appearing one after another rather than all at once. (Marked "needs GCP creds" — a real stream requires the Vertex login.)

Try this: Put this side by side with the streaming code from the Anthropic-API lesson. Only the client = ... line and the model id changed — the streaming loop is the same, which is exactly why moving providers is easy.

6 · Professional — choosing a front door professional

Sooner or later you'll be in a room where someone asks "so which one should we actually use?" — and the wrong instinct is to answer with a benchmark. New engineers reach for "which door is fastest / cheapest / most capable?" as if this were a performance bake-off. It isn't, and answering that way marks you as someone who's missed the point. The model is the same through every door, so the decision has almost nothing to do with the AI and almost everything to do with your organization's existing plumbing.

Think of it like choosing which bank to route a payment through when the recipient is the same either way. You don't pick based on "which bank has better money" — the money is identical. You pick based on which bank you already hold an account with, whose statements your accountant already knows how to read, and which one your compliance rules already bless. That's the whole shape of this choice: pick the door that matches the cloud your company already lives in, because it collapses three separate governance problems — billing, access control, and data residency — into ones you've already solved.

So the real inputs to the decision are things like: are you already an AWS shop or a GCP shop? Does a contract or regulation pin your data to a specific cloud or region? Do you want AI spend consolidated onto an invoice your finance team already reconciles, and access governed by the IAM roles your security team already audits? A team deep in GCP naturally reaches for Vertex; a team deep in AWS reaches for Bedrock; a small team or a prototype with no such constraints is usually best served by the simplest path — the direct API. The value of writing this as an explicit decision function, as the lab does, is that it forces the criteria into the open where they can be reviewed, rather than living as one senior engineer's gut feeling.

The mistake to avoid is letting a single feature quietly override the whole framework without saying so. If your workload genuinely needs a capability that only the direct API offers, that requirement trumps the "match your existing cloud" rule — and a good decision function makes that override visible and ordered, not buried. Watch how the lab's rules read top-to-bottom, first-match-wins, so the ordering itself encodes the priority. Here's the framing:

Enterprises pick Bedrock or Vertex to keep AI spend and data in the cloud they already use — one bill, one IAM model, one data-residency story. Model the decision.

which front door? data residency = AWS, or AWS-only? bedrock data residency = GCP, or GCP-only? vertex otherwise → api (direct) read top-to-bottom · first rule that matches wins · order = priority The choice is governance, not benchmarks. The model is identical through every door, so you route on existing cloud, residency, and billing — and a first-match-wins ordering makes the priorities explicit and reviewable.
🗺️ How to read this diagram

Follow the arrows straight down: the question falls through each rule until one matches, and that rule wins.

  • The top box is the question — "which front door?". Each amber box below is one condition, checked in order.
  • A match branches right to a result (bedrock if data/residency points at AWS, vertex if it points at GCP). If a rule doesn't match, control falls to the next box down.
  • The green box at the bottom is the default: when no cloud constraint applies, you land on the direct API — the simplest path.

In short: Rules read top-to-bottom, first match wins, so the order of the conditions encodes your priorities — cloud/residency first, simplest-path last.

Python · pick the front door (runs)
choose_door.pydef choose_door(on_aws, on_gcp, wants_simplest, data_residency):
    if data_residency == "aws" or (on_aws and not on_gcp): return "bedrock"
    if data_residency == "gcp" or (on_gcp and not on_aws): return "vertex"
    if wants_simplest: return "api (direct)"
    return "api (direct)"

print(choose_door(on_aws=True, on_gcp=False, wants_simplest=False, data_residency=None))
print(choose_door(on_aws=False, on_gcp=True, wants_simplest=False, data_residency=None))
print(choose_door(on_aws=False, on_gcp=False, wants_simplest=True, data_residency=None))
bedrock
vertex
api (direct)
▶ How this works

This little decision function (it runs offline) encodes how a team actually picks a front door. It reads top to bottom and returns the first rule that matches — so order matters.

  1. The parameters describe your situation: are you already on_aws / on_gcp, do you just want the wants_simplest path, and is there a data_residency rule (data must stay in a specific cloud).
  2. Rule 1: if data must stay in AWS, or you're on AWS and not GCP, choose Bedrock. Rule 2: the mirror image for Google → Vertex. A return stops the function immediately, so a matched earlier rule wins.
  3. If no cloud constraint applies, if wants_simplest: return falls back to the direct api — and so does the final line, making the direct API the default.
  4. The three print(choose_door(...)) calls at the bottom test three situations: an AWS shop, a GCP shop, and someone who just wants the simplest option.

What the output means: Prints bedrock, then vertex, then api (direct) — matching the console output shown below the code, one line per test case.

Try this: Add a call with both on_aws=True and on_gcp=True and no residency rule — which door wins? Trace the rules top to bottom to predict it, then run to confirm. That's how you reason about any decision function.

7 · Tech-lead — abstract the provider tech-lead

Here's the difference between an engineer and a tech lead, in one design decision. An engineer gets Claude working on Vertex and ships it. A tech lead gets it working and then asks the uncomfortable question: "what happens to every call site in our codebase the day we have to move providers?" If the answer is "we grep for AnthropicVertex and pray," the team has quietly signed up for a painful migration later. This section is about spending a little design effort now so that day is a config change instead.

The idea is an old and reliable one: put the thing that varies behind a single door, and make everyone knock on that door instead of reaching around it. Think of the electrical outlets in your walls. Your lamp doesn't know or care whether the electricity comes from a coal plant, a solar farm, or a nuclear station — it just plugs into a standard socket, and the utility handles all the messy provider-specific detail behind that socket. You can swap the entire power source and not one appliance in the house needs rewiring. A provider abstraction is that socket: your application code calls something plain like llm.complete(...), and a single small adapter decides — from config — whether that resolves to the API, Bedrock, or Vertex, applying the right client and the right model-ID prefix along the way.

What makes this cheap to build is precisely the discovery of this whole chapter: only two things actually differ between doors — the client constructor and the model-ID convention. So the abstraction doesn't have to be clever or large; it just has to isolate those two facts in one place. Everything downstream — your prompts, your streaming loops, your tool schemas, your business logic — stays blissfully provider-agnostic. This is the "one client" discipline from DF4 pointed at model providers instead of HTTP endpoints.

The mistake here is over-engineering in the other direction — building a sprawling framework that tries to abstract every conceivable difference. Don't. The whole force of a good abstraction is that it hides only what varies (constructor, prefix) and leaves everything shared exposed and obvious. Wrap the two differences; leave messages.create alone. A lead who draws that line well saves the team a migration; a lead who abstracts too much just builds a second thing to maintain. Here's the principle:

A lead wraps the front door behind one internal client so the app code never hard-codes a provider. Switching API→Bedrock→Vertex (for cost, compliance, or a new region) becomes a config change — the DF4 "one client" lesson applied to model providers.

app code llm.complete(...) one internal client (the socket) isolates the client + model-id prefix Anthropic() AnthropicVertex() Bedrock() config picks one — app code never knows which Wrap only what varies. App code knocks on one plain llm.complete(...) door; a small adapter picks API/Bedrock/Vertex from config, applying the right client and model-id prefix. Prompts, streaming, and tools stay provider-agnostic.
🗺️ How to read this diagram

Read top-down: app code at the top, the abstraction in the middle, the three concrete providers at the bottom.

  • The top box is your application calling one plain method, llm.complete(...). It never names a provider — like a lamp that just plugs into a socket.
  • The green middle box is the "socket": a small internal client that isolates the only two things that differ — the client constructor and the model-id prefix.
  • The three amber boxes are the real providers. Config (not code) picks which one the socket resolves to, so you can swap the power source — for cost, compliance, or a new region — without rewiring a single appliance upstream.

In short: Hide only the constructor and id-prefix behind one client; leave messages.create exposed, and a provider switch becomes a config edit.

Provider-agnostic = future-proofWhen app code calls llm.complete(...) and one adapter picks API/Bedrock/Vertex from config, you can move providers for cost or compliance without touching business logic. A lead who builds that abstraction saves the team a painful migration later.

Exercise AK7.1 — Port a call to Vertex + abstract it

Context: Porting a working call to Vertex and then hiding the provider behind an abstraction is the portability payoff in practice: same Messages code, one config value to switch clouds.

Your task: Port a C2 or W2 call to run on Vertex (ADC auth, AnthropicVertex, the Vertex model id), then write a tiny provider abstraction so the same app code runs against any of the three doors by changing one config value.

Requirements:

  • The ported call authenticates via ADC (no Anthropic API key in code)
  • It uses AnthropicVertex with the bare (unprefixed) Vertex model id
  • The provider abstraction follows the shape of front_doors.py
  • Switching provider is a single config-value change — the app code is unchanged
  • The abstraction handles the model-id prefix difference (Bedrock's anthropic.) internally

💡 Hint: Get the raw Vertex call working first, then wrap only the two things that differ — the client constructor and the id prefix — behind the abstraction.

🪜 Practice ladder beginner → industry

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

Exercise 1 · Three front doors, one ClaudeBeginner

Context: The same Claude model is reachable through the first-party API, Amazon Bedrock, and Google Vertex; the Messages surface is identical through every door, so portability comes down to the client class and the model-ID convention.

Your task: Map each front door — first-party, Vertex, Bedrock — to its client class and model-ID convention.

Requirements:

  • First-party uses Anthropic() with the bare model id
  • Vertex uses AnthropicVertex(...) with the bare id (no prefix)
  • Bedrock uses its Bedrock client with an anthropic.-prefixed id
  • Note the Messages API surface is the same through every door
  • Show all three mapped side by side

💡 Hint: The only real differences are the client you construct and whether the model id carries a prefix.

Show solution

Same model, three access paths that differ mainly in client + ID prefix:

def front_door(provider):
    return {
        "anthropic": ("Anthropic()",              "bare id: claude-opus-4-8"),
        "vertex":    ("AnthropicVertex(...)",     "bare id: claude-opus-4-8 (no prefix)"),
        "bedrock":   ("AnthropicBedrockMantle(...)", "prefixed: anthropic.claude-opus-4-8"),
    }[provider]

for p in ("anthropic", "vertex", "bedrock"):
    print(p, "->", front_door(p))

The Messages API surface is the same through every door — you swap the client class and mind the model-ID convention: Vertex uses the bare id, Bedrock adds an anthropic. prefix. That's what makes provider-portability cheap.

Exercise 2 · Authenticate with ADCIntermediate

Context: On Vertex you don't hold an Anthropic key — the SDK signs requests with Google Application Default Credentials resolved from the environment — which is why there is no API key in the code.

Your task: Show the Vertex authentication setup with ADC and explain why there is no API key in code. Needs the gcloud CLI + a GCP project to run.

Requirements:

  • Do the one-time local auth with gcloud auth application-default login
  • Construct AnthropicVertex naming only the project and region — no key
  • Explain there is no ANTHROPIC_API_KEY: auth comes from ADC
  • Note the production equivalent (service account / workload identity)
  • Verify current constructor args in the docs

💡 Hint: The client names project and region; the credentials come from the environment (ADC), not a string in your code.

Show solution

ADC supplies the credentials; the client just names project + region:

# One-time local auth — populates Application Default Credentials:
#   gcloud auth application-default login

from anthropic import AnthropicVertex
client = AnthropicVertex(project_id="my-gcp-project", region="us-east5")
# No ANTHROPIC_API_KEY — auth comes from ADC (the gcloud login above,
# or a service account / workload identity in production).

On Vertex you don't hold an Anthropic key: the SDK signs requests with Google credentials resolved from the environment (ADC). Locally that's gcloud auth application-default login; in prod it's a service account or workload identity. Verify current constructor args in the docs.

Exercise 3 · Call Claude on VertexAdvanced

Context: Once the Vertex client is built, a Messages call is identical to first-party — only the client construction and the unprefixed model id differ — though region matters on Vertex for data residency.

Your task: Make a basic Messages call through AnthropicVertex using the bare model ID. Needs GCP auth to run.

Requirements:

  • Build an AnthropicVertex client with project and region
  • Call messages.create with the bare model id — no anthropic. prefix
  • Set max_tokens and a user message; extract the text from the response
  • The messages.create surface is unchanged from first-party
  • Note region matters (global recommended, or a specific region for data residency); verify supported regions in the docs

💡 Hint: Reuse your first-party call verbatim — swap only the client and drop the model-id prefix.

Show solution

Once the client is built, the call is identical to first-party:

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

resp = client.messages.create(
    model="claude-opus-4-8",          # bare id on Vertex — NO anthropic. prefix
    max_tokens=512,
    messages=[{"role": "user", "content": "Explain ADC in one sentence."}],
)
print(next(b.text for b in resp.content if b.type == "text"))

The messages.create surface is unchanged — only the client construction and the (unprefixed) model ID differ from the first-party API. Region matters on Vertex: global is recommended, or a specific region for data residency. Verify supported regions in the docs.

Exercise 4 · Map concepts across all three doorsExpert

Context: Core Messages, streaming, tools, and prompt caching work on Vertex, but several server tools and endpoints don't — so a capability map is what stops you promising a feature that isn't there.

Your task: Write a small capability map that decides whether a requested feature is available on Vertex and what changes versus first-party.

Requirements:

  • Encode the availability facts: messages/streaming/tools and prompt caching are available
  • Mark automatic prompt caching as first-party-only
  • Mark web fetch, code execution, Files API, and Batches as unavailable on Vertex
  • Mark web search as the basic variant only
  • A lookup returns availability for a given feature, defaulting to "verify in the docs"
  • State the takeaway: check the availability table before promising a feature on Vertex

💡 Hint: Build a small table of what's on and off on Vertex; the first-party-only and unavailable entries are the ones that bite.

Show solution

Encode the availability differences that actually bite on Vertex:

VERTEX = {                      # from the platform-availability table
    "messages/streaming/tools": True,
    "prompt_caching":           True,
    "automatic_prompt_caching": False,   # 1P only
    "web_search":               "basic only (web_search_20250305)",
    "web_fetch":                False,
    "code_execution":           False,
    "files_api":                False,
    "batches":                  False,
}
def on_vertex(feature):
    return VERTEX.get(feature, "verify in platform-availability docs")

print(on_vertex("prompt_caching"))            # True
print(on_vertex("web_fetch"))                 # False
print(on_vertex("web_search"))                # basic only (web_search_20250305)

Core Messages/streaming/tools and prompt caching work on Vertex, but several server tools and endpoints don't: web fetch, code execution, Files API, and Batches are unavailable, web search is the basic variant only, and automatic prompt caching is first-party-only. Check the availability table before promising a feature on Vertex.

Exercise 5 · Choose a front doorProfessional

Context: Choosing a provider is a routing decision on the team's existing cloud, auth model, and needed features — but a first-party-only feature (Batches, Files, code execution) overrides everything and points back to the first-party API.

Your task: Write a selector that recommends first-party / Vertex / Bedrock from a team's real constraints.

Requirements:

  • The selector takes existing cloud, whether a first-party-only feature is needed, and the auth/billing preference
  • A needed first-party-only feature overrides and returns first-party
  • GCP-native teams → Vertex (ADC auth, GCP billing, data in their project)
  • AWS or wanting IAM/Marketplace billing → Bedrock
  • Otherwise default to the first-party API
  • Demonstrate each branch with an example

💡 Hint: Check the first-party-only override first; only after that does existing cloud pick Vertex vs Bedrock.

Show solution

Route by where they already run and which features they need:

def choose_door(existing_cloud, needs_1p_only_feature, wants_iam_billing):
    if needs_1p_only_feature:
        return "first-party API — Batches / Files / code exec are 1P-only"
    if existing_cloud == "gcp":
        return "Vertex — ADC auth, GCP billing, data stays in your GCP project"
    if existing_cloud == "aws" or wants_iam_billing:
        return "Bedrock — AWS IAM auth + Marketplace billing"
    return "first-party API — simplest, full feature surface"

print(choose_door("gcp", False, False))   # Vertex
print(choose_door("aws", False, True))    # Bedrock
print(choose_door("none", True, False))   # first-party (needs 1P-only feature)

Pick the door that matches the team's existing cloud (Vertex for GCP-native auth/billing, Bedrock for AWS IAM/Marketplace) — but if they need a first-party-only feature (Batches, Files API, code execution), that overrides and points back to the first-party API.

Exercise 6 · Abstract the provider (tech-lead)Industry scenario

Context: Because every door exposes the same messages.create, the only provider-specific parts are the client constructor and the model-ID prefix — isolate both behind a factory and the rest of the codebase is portable.

Your task: As tech lead, write a small factory that returns the right client and normalizes the model ID so the codebase is insulated from the provider choice.

Requirements:

  • A factory selects the client by provider (first-party / Vertex / Bedrock), reading config/env
  • It returns both the client and the correct model-id prefix (empty except Bedrock's anthropic.)
  • A helper normalizes the model id by applying the prefix
  • Call sites use the same messages.create regardless of provider
  • Keep provider-specific feature gates behind the same abstraction
  • Verify client names/args in the docs

💡 Hint: Push the only two differences — constructor and id prefix — into the factory so everything downstream is provider-agnostic.

Show solution

One factory, provider-agnostic call sites — the difference lives in construction:

import os

def make_client(provider):
    if provider == "vertex":
        from anthropic import AnthropicVertex
        return AnthropicVertex(project_id=os.environ["GCP_PROJECT"], region="us-east5"), ""
    if provider == "bedrock":
        from anthropic import AnthropicBedrockMantle
        return AnthropicBedrockMantle(aws_region=os.environ["AWS_REGION"]), "anthropic."
    from anthropic import Anthropic
    return Anthropic(), ""          # first-party

def model_id(base, prefix):         # normalize per provider
    return prefix + base            # bedrock -> "anthropic.claude-opus-4-8"

client, prefix = make_client(os.environ.get("LLM_PROVIDER", "anthropic"))
resp = client.messages.create(model=model_id("claude-opus-4-8", prefix),
                              max_tokens=256,
                              messages=[{"role":"user","content":"ping"}])

Because every door exposes the same messages.create, the only provider-specific parts are the client constructor and the model-ID prefix — isolate both behind a factory and the rest of the codebase is portable. Keep provider-specific feature gates (see the availability map) behind the same abstraction. Verify client names/args in the docs.

✓ Checkpoint — you can move on when you can…

  • Explain the three front doors and why enterprises pick a cloud one.
  • Authenticate to Vertex with ADC.
  • Call Vertex; map client/auth/model across doors.
  • Abstract the provider behind one internal client.

Knowledge check check yourself

✓ Knowledge check

When calling Claude on Vertex, what actually changes versus the direct Anthropic API, and what stays the same?

Show answer
Only the client construction (AnthropicVertex(region=..., project_id=...) instead of Anthropic()) and the model-ID format (e.g. claude-3-5-sonnet-v2@20241022) change. The messages.create() request shape, streaming, and tool use are identical because it's the same underlying model.
✓ Knowledge check

If you skip gcloud services enable aiplatform.googleapis.com, what happens, and why do teams still choose Vertex or Bedrock over the direct API?

Show answer
You get permission errors that look like auth failures but are really the Vertex AI service not being activated on the project. Teams still choose a cloud front door mainly for data residency, IAM-based access control, and consolidated billing, i.e. governance reasons, not because the model is any more capable there.
© 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