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.
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.
code/ak7-claude-vertex-gcp/. Python runs offline; configs are ready to use.1 · Three front doors, one Claude essential
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.
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
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).
setup.shpip install "anthropic[vertex]"
gcloud auth application-default login
gcloud config set project my-gcp-project
gcloud services enable aiplatform.googleapis.com
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).
pip install "anthropic[vertex]"installs Anthropic's Python library plus the extra Vertex pieces (the[vertex]part). This is what gives you theAnthropicVertexclient used later.gcloud auth application-default loginopens 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.gcloud config set project my-gcp-projecttells Google which project (billing + resources bucket) to use.gcloud services enable aiplatform.googleapis.comswitches 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
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).
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)
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.
from anthropic import AnthropicVerteximports 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.client.messages.create(...)is the exact same request method as the direct API.max_tokenscaps the reply length;systemsets the assistant's role;messagesis the conversation (here one user question).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.resp.content[0].textpulls the text out of the reply. The reply's content is a list of blocks, so[0]grabs the first block and.textits 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.
4 · Map concepts across all three doors intermediate
Everything transfers — only the client construction and model id change. Model the mapping so switching front doors is a config change, not a rewrite.
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
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.
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 bydoor.- Each entry lists that door's client (
Anthropic(), boto3, orAnthropicVertex()), 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. for door in ["api", "bedrock", "vertex"]:loops over all three and prints a lined-up row for each, using f-string alignment (:8,:26pad to fixed widths so the columns line up).- The last
printstates the punchline in words: themessagesshape — 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
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.
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()
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.
- The first two lines build the same
AnthropicVertexclient as before (region + project, no key). with client.messages.stream(...) as stream:opens a live streaming connection. Thewithblock guarantees it closes cleanly when finished. This is the identical method name used on the direct API.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 bareprint()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
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.
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)
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.
- The parameters describe your situation: are you already
on_aws/on_gcp, do you just want thewants_simplestpath, and is there adata_residencyrule (data must stay in a specific cloud). - 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
returnstops the function immediately, so a matched earlier rule wins. - If no cloud constraint applies,
if wants_simplest: returnfalls back to the direct api — and so does the final line, making the direct API the default. - 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
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.
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
AnthropicVertexwith 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.
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.
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
AnthropicVertexnaming 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.
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
AnthropicVertexclient with project and region - Call
messages.createwith the bare model id — noanthropic.prefix - Set
max_tokensand a user message; extract the text from the response - The
messages.createsurface is unchanged from first-party - Note region matters (
globalrecommended, 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.
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.
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.
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.createregardless 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
When calling Claude on Vertex, what actually changes versus the direct Anthropic API, and what stays the same?
Show answer
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.If you skip gcloud services enable aiplatform.googleapis.com, what happens, and why do teams still choose Vertex or Bedrock over the direct API?