Track 7 — Production Deployment
Building AI Agents with Claude
M21B: Cloud Provider Deep Dive — Bedrock, Vertex AI & Azure AI
Module 21B of 30+ • 75–90 min

Cloud Provider Deep Dive

Bedrock, Vertex AI & Azure AI — one Claude model, three enterprise on-ramps.

Learning Objectives

  • Set up and authenticate with AWS Bedrock, Google Vertex AI, and Microsoft Azure AI Foundry using provider-native credential patterns
  • Switch between all four Claude access paths (direct API, Bedrock, Vertex, Azure) with a constructor-level change and no other code modifications
  • Use Bedrock Guardrails, Bedrock Knowledge Bases, Vertex Agent Engine, and Azure AI Foundry Agent Service for managed safety and RAG
  • Build a provider-agnostic wrapper class driven by an environment variable so your agent code never hard-codes a cloud
  • Apply the decision framework to choose the right cloud for a given team, compliance requirement, and cost profile
Level
Intermediate → Advanced
Prerequisites
M21 (API Design & Deployment)
Cloud accounts needed
AWS, GCP, or Azure (one is enough for the lab)

The Four Ways to Call Claude

💡 Everyday Analogy — The Hospital Network

BEFORE: Imagine a specialist doctor — let's call her Dr. Claude — who can be seen at four different hospital networks: her own private clinic, General Hospital (AWS), City Medical (GCP), and Metro Health (Azure). The same doctor. The same expertise. The same consultation quality.

PAIN: Before this existed, your company's insurance (procurement contract) only covered City Medical. Every time you needed Dr. Claude, you had to file out-of-network paperwork, get a special exception, and pay the premium yourself. Legal reviewed every appointment. That's what it felt like before Claude was available on GCP's Vertex AI.

MAPPING: Now Dr. Claude operates in all four networks. If your company's existing GCP contract covers City Medical, you book through City Medical — no new vendor, no new legal review, bills go to the existing account. The consultation is identical either way. Picking your cloud provider is picking which hospital network handles the paperwork, not which doctor you see.

📐 Technical Definition

Claude runs on Anthropic's own infrastructure. When you call Claude through AWS Bedrock, GCP Vertex AI, or Azure AI, Anthropic is still running the actual model — what changes is the API gateway, authentication, billing integration, and data plane through which your request reaches Claude.

Each cloud provider signs a reseller agreement with Anthropic, provisions capacity in specific regions, and exposes Claude through their own SDKs and identity systems. Your requests hit the cloud provider's endpoint first, credentials are validated against their IAM, then the request is forwarded to Anthropic's inference fleet. The response comes back through the same path.

From a code perspective, you swap one constructor for another. The messages.create() call shape, tool-use format, and response structure are identical on Bedrock and Vertex (they use the Anthropic SDK natively). Azure AI uses the chat completions formatA JSON request structure popularized by OpenAI and adopted by many cloud providers. Fields like messages, model, and max_tokens map directly to the Anthropic format, but the SDK and endpoint URL differ. via the azure-ai-inference SDK.

The table below summarises what actually changes between the four access paths. All four reach the same Claude model; only the plumbing differs.

Dimension Direct API Bedrock Vertex AI Azure AI
SDK classAnthropic()AnthropicBedrock()AnthropicVertex()ChatCompletionsClient()
AuthAPI keyAWS IAM / SigV4Google ADCAPI key / Entra ID
BillingAnthropic invoiceAWS billGCP billAzure bill
Model ID formatclaude-sonnet-4-6anthropic.claude-*-v1:0claude-*@YYYYMMDDDeployment name
New model lagDay 0Days–weeksDays–weeksDays–weeks
Data never leavesAnthropic infraYour AWS accountYour GCP projectYour Azure subscription
Why This Matters in the Real World

At a mid-size healthcare company, procurement has already signed an AWS Enterprise Agreement and negotiated a Business Associate Agreement (BAA) covering all AWS services. Adding a new vendor — even Anthropic — would require a 3–6 month legal review plus a new BAA. Using Claude on Bedrock costs zero additional vendor reviews: it's just another AWS service under the existing contract. The savings aren't in token pricing (which is the same). The savings are in organizational velocity.

AWS Bedrock Deep Dive

AWS BedrockAmazon Bedrock is a fully managed service that provides access to foundation models (including Claude) via the AWS API. You do not run any servers — AWS handles all infrastructure. You pay per token. is not a hosting layer for your agent code. It is a managed inference gateway that sits in front of Claude. Your application code runs wherever you put it — EC2, Lambda, your laptop — and sends API calls to Bedrock's regional endpoints. Bedrock validates your IAM credentials, logs the request to CloudTrail, applies any Guardrails you've configured, forwards the payload to Claude, and streams the response back to you.

Think of Bedrock as AWS's "API gateway for AI." Just as AWS API Gateway manages HTTP traffic to your Lambda functions, Bedrock manages LLM traffic to foundation models. You benefit from AWS's global infrastructure, compliance certifications (SOC 2, HIPAA BAA, FedRAMP), and the ability to keep data in a specific AWS region — all without running any new servers.

One-Time Setup

Bedrock requires two one-time steps before you can make your first call:

  1. Request model access — In the AWS Console, go to Amazon Bedrock → Model access, find Claude under Anthropic, and click "Request access." Approval is usually instant for Claude Haiku and Sonnet; enterprise tiers may take a few hours.
  2. Create an IAM policy — Your code's IAM identity (the role attached to your Lambda, EC2 instance, or dev user) needs permission to call bedrock:InvokeModel. The minimal policy:
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": [
      "bedrock:InvokeModel",
      "bedrock:InvokeModelWithResponseStream"
    ],
    "Resource": "arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-*"
  }]
}

For cross-region inference profiles (which give higher throughput by routing across AWS regions), also add the arn:aws:bedrock:us-*::foundation-model/* resource pattern. In development, the easiest setup is to configure your AWS CLI with aws configure and the SDK will automatically find credentials.

⚠️ Common Misconception: Bedrock Model IDs

The model ID format on Bedrock is not the same as the direct API. claude-sonnet-4-6 on the direct API becomes anthropic.claude-sonnet-4-6-v1:0 on Bedrock (single region) or us.anthropic.claude-sonnet-4-6-20250514-v1:0 for cross-region inference profiles. Using the direct API model ID against Bedrock's endpoint will return a ValidationException: Unknown model ID — one of the most common beginner errors.

Code Walkthrough

Install the Bedrock-enabled Anthropic SDK — it adds boto3 and SigV4 request signing transparently:

pip install "anthropic[bedrock]"   # adds boto3 automatically
npm install @anthropic-ai/bedrock-sdk

Here is a complete production-ready Bedrock client with cross-region inference and error handling:

WHAT — Import and configure the Bedrock client. The only new line vs. the direct API is AnthropicBedrock instead of Anthropic and an aws_region argument. Everything else is identical.
from anthropic import AnthropicBedrock
import anthropic

# ── Chunk 1: Build the client ─────────────────────────────────────
# AnthropicBedrock reads AWS credentials from the standard chain:
#   1. AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY env vars (dev)
#   2. ~/.aws/credentials file (CLI / SSO)
#   3. EC2/ECS/Lambda instance role (production — no keys in code)
client = AnthropicBedrock(
    aws_region="us-west-2",   # must match the region where you requested model access
)

# ── Chunk 2: Cross-region inference profile model ID ─────────────
# The "us." prefix means Bedrock may route to any US region for
# higher throughput. Without it, you're locked to one region and
# may hit tighter rate limits.
BEDROCK_MODEL = "us.anthropic.claude-sonnet-4-6-20250514-v1:0"

# ── Chunk 3: Make the call ────────────────────────────────────────
# GOTCHA: max_tokens is REQUIRED on Bedrock — the default (None)
# that works on the direct API raises a ValidationException here.
try:
    response = client.messages.create(
        model=BEDROCK_MODEL,
        max_tokens=1024,
        messages=[{"role": "user", "content": "Explain AWS Bedrock in one sentence."}],
    )
    print(response.content[0].text)
    print(f"Tokens: {response.usage.input_tokens} in / {response.usage.output_tokens} out")

except anthropic.APIStatusError as e:
    # Common errors:
    # 403 AccessDeniedException  → IAM policy missing bedrock:InvokeModel
    # 404 ResourceNotFoundException → wrong model ID format or model not enabled
    # 429 ThrottlingException    → rate limit hit; add exponential backoff
    print(f"Bedrock error {e.status_code}: {e.message}")
import { AnthropicBedrock } from "@anthropic-ai/bedrock-sdk";

// ── Chunk 1: Build the client ─────────────────────────────────────
// Reads credentials from AWS SDK credential chain (env vars,
// ~/.aws/credentials, or instance profile — same as boto3).
const client = new AnthropicBedrock({ awsRegion: "us-west-2" });

const BEDROCK_MODEL = "us.anthropic.claude-sonnet-4-6-20250514-v1:0";

// ── Chunk 2: Make the call ────────────────────────────────────────
try {
  const response = await client.messages.create({
    model: BEDROCK_MODEL,
    max_tokens: 1024,
    messages: [{ role: "user", content: "Explain AWS Bedrock in one sentence." }],
  });
  console.log(response.content[0].text);
  console.log(`Tokens: ${response.usage.input_tokens} in / ${response.usage.output_tokens} out`);

} catch (err: any) {
  // status 403 → IAM permission missing
  // status 404 → wrong model ID or model not enabled in console
  console.error(`Bedrock error ${err.status}: ${err.message}`);
}
✅ What Just Happened? The AnthropicBedrock client intercepted your messages.create() call, signed the request with your AWS credentials using SigV4 (AWS's request-signing protocol), sent it to bedrock-runtime.us-west-2.amazonaws.com, and returned a response in exactly the same format as the direct API. From your application code's perspective, almost nothing changed.

Bedrock Guardrails & Knowledge Bases

Bedrock adds two managed safety and retrieval features that sit outside your agent code — which means they apply even if someone bypasses your application layer and calls the Bedrock endpoint directly.

WHAT — Bedrock Guardrails attach a content policy to every request. You configure the guardrail once in the console (blocked topics, PII detection, word filters), then pass its ID with every call. GOTCHA: Guardrails add ~50–200ms latency. Don't enable them on every call if only a subset of calls need safety filtering.
import boto3, os

# ── Bedrock Guardrails via boto3 (raw Bedrock client) ─────────────
# Use when you need guardrails but prefer boto3 over the Anthropic SDK
bedrock_rt = boto3.client("bedrock-runtime", region_name="us-west-2")

body = {
    "anthropic_version": "bedrock-2023-05-31",
    "max_tokens": 512,
    "messages": [{"role": "user", "content": "List medications for diabetes."}],
}
response = bedrock_rt.invoke_model(
    modelId="us.anthropic.claude-sonnet-4-6-20250514-v1:0",
    body=__import__("json").dumps(body),
    # Attach guardrail — created in Bedrock console
    guardrailIdentifier=os.environ["BEDROCK_GUARDRAIL_ID"],
    guardrailVersion="DRAFT",   # or a published version number
)
result = __import__("json").loads(response["body"].read())
print(result["content"][0]["text"])

# ── Bedrock Knowledge Bases — built-in RAG ────────────────────────
# retrieve_and_generate does: embed query → search KB → augment prompt
# → generate response.  No vector DB code on your side.
agent_rt = boto3.client("bedrock-agent-runtime", region_name="us-west-2")

kb_response = agent_rt.retrieve_and_generate(
    input={"text": "What are the Q4 revenue figures?"},
    retrieveAndGenerateConfiguration={
        "type": "KNOWLEDGE_BASE",
        "knowledgeBaseConfiguration": {
            "knowledgeBaseId": os.environ["BEDROCK_KB_ID"],
            "modelArn": "arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-sonnet-4-6-v1:0",
        },
    },
)
print(kb_response["output"]["text"])
print("Sources:", [r["location"] for r in kb_response.get("citations", [])])
import { BedrockAgentRuntimeClient, RetrieveAndGenerateCommand }
  from "@aws-sdk/client-bedrock-agent-runtime";

// ── Bedrock Knowledge Bases — built-in RAG ────────────────────────
const agentRt = new BedrockAgentRuntimeClient({ region: "us-west-2" });

const cmd = new RetrieveAndGenerateCommand({
  input: { text: "What are the Q4 revenue figures?" },
  retrieveAndGenerateConfiguration: {
    type: "KNOWLEDGE_BASE",
    knowledgeBaseConfiguration: {
      knowledgeBaseId: process.env.BEDROCK_KB_ID!,
      modelArn: "arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-sonnet-4-6-v1:0",
    },
  },
});
const res = await agentRt.send(cmd);
console.log(res.output?.text);
console.log("Sources:", res.citations?.map(c => c.retrievedReferences?.map(r => r.location)));

Google Vertex AI Deep Dive

Google's Vertex AI Model GardenA catalog of foundation models available on Google Cloud's Vertex AI platform. Models from Anthropic, Meta, Mistral, and others are listed here alongside Google's own Gemini family. You browse, accept terms, and deploy from the Garden. is the GCP marketplace for foundation models including Claude. When you deploy Claude from Model Garden, Google provisions a managed endpointA serverless HTTPS endpoint that accepts inference requests. You don't configure servers — Google auto-scales the underlying compute. You pay per token, not per hour. — a serverless URL that accepts Anthropic-format API calls. Your code uses the AnthropicVertex client from the Anthropic SDK, which handles GCP credential signing transparently using Application Default CredentialsGCP's standard mechanism for discovering credentials. The SDK checks: (1) GOOGLE_APPLICATION_CREDENTIALS env var pointing to a service account JSON file, (2) gcloud CLI credentials from `gcloud auth application-default login`, (3) compute metadata server (for GKE/Cloud Run/GCE). Your code doesn't change between environments. (ADC).

One-Time Setup

  1. Enable the Vertex AI API in your GCP project: gcloud services enable aiplatform.googleapis.com
  2. Accept Claude's terms — go to Vertex AI → Model Garden → find "Claude" → click "Enable" and accept Anthropic's terms of service. You must do this once per GCP project, not per user.
  3. Set up credentials — for local development: gcloud auth application-default login. For production (Cloud Run, GKE), attach a service account with roles/aiplatform.user to the compute resource — no JSON file needed in the container.
⚠️ Common Misconceptions

"Claude on Vertex uses Gemini pricing." — No. Claude on Vertex is priced at the same per-token rates as the direct Anthropic API. Vertex just rolls the bill into your GCP invoice. GCP committed-use discounts and credits do apply, which can make it cheaper if your organization has a GCP commitment.

"Any GCP region works." — Claude is only available in specific Vertex regions. As of mid-2025, the primary regions are us-east5 (Ohio) and europe-west1 (Belgium). Specifying an unsupported region returns a model-not-found error.

"I can omit the date in the Vertex model ID." — Unlike the direct API where claude-sonnet-4-6 resolves to the latest compatible version, on Vertex you should pin a date-stamped ID like claude-sonnet-4-6@20250514. This prevents unexpected behavior when Anthropic updates the model.

Code Walkthrough

from anthropic import AnthropicVertex
import anthropic, os

# ── Chunk 1: Install ──────────────────────────────────────────────
# pip install "anthropic[vertex]"
# This adds the google-auth library for ADC credential resolution.

# ── Chunk 2: Build the client ─────────────────────────────────────
# project_id: your GCP project ID (not number, not name — the slug)
# region: must be a region where Claude is available on Vertex
# Credentials are resolved automatically via ADC — no key in code.
client = AnthropicVertex(
    project_id=os.environ["GCP_PROJECT"],   # e.g. "my-project-123"
    region=os.environ.get("GCP_REGION", "us-east5"),
)

# ── Chunk 3: Vertex model ID format ──────────────────────────────
# The date stamp (@20250514) pins to a specific model checkpoint.
# Vertex exposes both "claude-sonnet-4-6" (resolves to latest) and
# "claude-sonnet-4-6@20250514" (pinned). Use pinned in production.
VERTEX_MODEL = "claude-sonnet-4-6@20250514"

# ── Chunk 4: Make the call ────────────────────────────────────────
# GOTCHA: system prompts on Vertex are passed the same way as the
# direct API — as a separate `system` parameter, NOT as a message
# with role "system". This is identical to the direct API behavior.
try:
    response = client.messages.create(
        model=VERTEX_MODEL,
        max_tokens=1024,
        system="You are a concise assistant.",
        messages=[{"role": "user", "content": "What is Vertex AI?"}],
    )
    print(response.content[0].text)

except anthropic.APIStatusError as e:
    # 403 → service account missing aiplatform.user role
    # 404 → model not enabled in Model Garden for this project/region
    print(f"Vertex error {e.status_code}: {e.message}")
// npm install @anthropic-ai/vertex-sdk
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk";

const client = new AnthropicVertex({
  projectId: process.env.GCP_PROJECT!,
  region: process.env.GCP_REGION ?? "us-east5",
});

const VERTEX_MODEL = "claude-sonnet-4-6@20250514";

try {
  const response = await client.messages.create({
    model: VERTEX_MODEL,
    max_tokens: 1024,
    system: "You are a concise assistant.",
    messages: [{ role: "user", content: "What is Vertex AI?" }],
  });
  console.log(response.content[0].text);
} catch (err: any) {
  console.error(`Vertex error ${err.status}: ${err.message}`);
}
✅ What Just Happened? The AnthropicVertex client fetched a short-lived OAuth token from GCP's metadata server (or your gcloud CLI cache), signed the request with it, and sent it to aiplatform.googleapis.com. GCP verified the token, checked that your project has Claude enabled in Model Garden, and forwarded the payload to Anthropic's inference fleet. You got back the same Message object format as the direct API.

For production deployments on Cloud Run or GKE, skip gcloud auth application-default login. Instead, attach a service account to your Cloud Run service with the roles/aiplatform.user IAM role, and ADC will discover credentials from the GCP metadata server automatically — no key file, no environment variable.

Microsoft Azure AI Deep Dive

Azure AI FoundryMicrosoft's unified AI development platform (formerly Azure AI Studio). It provides a model catalog, deployment management, evaluation tools, and a hosted agent runtime — all accessible from the Azure portal or the Azure AI Python SDK. is Microsoft's unified AI platform. Claude models from Anthropic are available in the Azure AI Foundry model catalog as serverless deployments — you create a deployment, get a unique endpoint URL and API key, and call it from your application. Unlike Bedrock and Vertex (which use the Anthropic SDK natively), Azure AI uses the azure-ai-inference package, which implements the Azure AI inference protocolA standardized chat completions API format maintained by Microsoft and used across all models in Azure AI Foundry. Compatible with OpenAI's Chat Completions API shape, but accessed through Azure's credential and endpoint system rather than OpenAI's. — a format compatible with the OpenAI Chat Completions API.

The key difference from Bedrock and Vertex: you don't use the Anthropic SDK for Azure. You use Microsoft's azure-ai-inference SDK. The request fields (messages, model, max_tokens) are similar but the client, endpoint, and credential classes are all Azure-specific.

One-Time Setup

  1. Create an Azure AI resource — In the Azure portal, create a new "Azure AI Foundry" resource (search "Azure AI Foundry" in the marketplace).
  2. Deploy Claude — Inside your Azure AI Foundry resource, go to Model catalog → search "Claude" → select a Claude model → click "Deploy" → choose "Serverless API." Azure provisions the endpoint in minutes.
  3. Copy your endpoint and key — After deployment, the Azure portal shows your AZURE_AI_ENDPOINT (e.g., https://your-resource.services.ai.azure.com/models) and a primary key. Store these in environment variables — never in code.
  4. Install the SDK: pip install azure-ai-inference azure-core

Code Walkthrough

WHAT — The Azure AI client uses the azure-ai-inference package. Instead of messages.create(), you call complete(). The response structure differs: choices are in response.choices[0].message.content rather than response.content[0].text. WHY — Azure AI Foundry standardized on the OpenAI Chat Completions response shape across all models. GOTCHA — Anthropic SDK features like tool_use in the native format are NOT available through azure-ai-inference. You must use the Azure AI tool calling format (similar to OpenAI's function calling) instead.
from azure.ai.inference import ChatCompletionsClient
from azure.ai.inference.models import SystemMessage, UserMessage, AssistantMessage
from azure.core.credentials import AzureKeyCredential
from azure.core.exceptions import HttpResponseError
import os

# ── Chunk 1: Build the client (API key auth — dev/testing) ────────
# endpoint: the URL from your Azure AI Foundry deployment
# credential: wraps the API key from the Azure portal
# For production, replace AzureKeyCredential with ManagedIdentityCredential
# (see the "Managed Identity" section below)
client = ChatCompletionsClient(
    endpoint=os.environ["AZURE_AI_ENDPOINT"],
    credential=AzureKeyCredential(os.environ["AZURE_AI_KEY"]),
)

# ── Chunk 2: Make a call ──────────────────────────────────────────
# model: the DEPLOYMENT NAME you chose in Azure AI Foundry
# (not the Anthropic model ID — you named it during deployment)
# Messages use Azure's typed message classes or plain dicts — both work.
try:
    response = client.complete(
        model=os.environ.get("AZURE_DEPLOYMENT_NAME", "claude-sonnet-4-6"),
        messages=[
            SystemMessage(content="You are a concise assistant."),
            UserMessage(content="What is Azure AI Foundry?"),
        ],
        max_tokens=512,
    )
    # GOTCHA: response structure differs from Anthropic SDK
    # Not response.content[0].text — it's response.choices[0].message.content
    print(response.choices[0].message.content)
    print(f"Finish reason: {response.choices[0].finish_reason}")
    print(f"Tokens: {response.usage.prompt_tokens} in / {response.usage.completion_tokens} out")

except HttpResponseError as e:
    # 401 → wrong API key
    # 404 → deployment name doesn't match what you created
    # 429 → rate limit; Azure returns Retry-After header
    print(f"Azure error {e.status_code}: {e.message}")
// npm install @azure-rest/ai-inference @azure/core-auth
import ModelClient, { isUnexpected } from "@azure-rest/ai-inference";
import { AzureKeyCredential } from "@azure/core-auth";

// ── Chunk 1: Build the client ─────────────────────────────────────
const client = ModelClient(
  process.env.AZURE_AI_ENDPOINT!,
  new AzureKeyCredential(process.env.AZURE_AI_KEY!)
);

// ── Chunk 2: Make a call ──────────────────────────────────────────
const response = await client.path("/chat/completions").post({
  body: {
    model: process.env.AZURE_DEPLOYMENT_NAME ?? "claude-sonnet-4-6",
    messages: [
      { role: "system", content: "You are a concise assistant." },
      { role: "user", content: "What is Azure AI Foundry?" },
    ],
    max_tokens: 512,
  },
});

// isUnexpected() is Azure's way of checking for HTTP errors in the REST client
if (isUnexpected(response)) {
  throw new Error(`Azure error ${response.status}: ${response.body.error.message}`);
}

// Response is in OpenAI Chat Completions format
console.log(response.body.choices[0].message.content);
console.log(`Tokens: ${response.body.usage?.prompt_tokens} in / ${response.body.usage?.completion_tokens} out`);

Production Pattern: Entra ID Managed Identity

API keys in environment variables are fine for development. In production on Azure (App Service, Container Apps, AKS), the recommended pattern is Entra ID Managed IdentityA feature of Azure Active Directory (now called Microsoft Entra ID) that automatically provisions a machine identity for Azure compute resources. Your App Service or Container App gets an identity without any credentials — Azure rotates the underlying tokens automatically. Your code just calls DefaultAzureCredential() and the runtime handles the rest. — no keys in code, no rotation required.

# pip install azure-identity
from azure.ai.inference import ChatCompletionsClient
from azure.identity import DefaultAzureCredential
import os

# DefaultAzureCredential tries (in order):
#   1. AZURE_CLIENT_ID / SECRET / TENANT env vars (CI/CD)
#   2. Azure CLI (local dev: `az login`)
#   3. Managed Identity (Azure App Service / Container Apps / AKS)
# No key in code — zero secrets to rotate or accidentally commit.
credential = DefaultAzureCredential()

client = ChatCompletionsClient(
    endpoint=os.environ["AZURE_AI_ENDPOINT"],
    credential=credential,
)
# The rest of the code is identical to the API-key example above
// npm install @azure/identity
import { DefaultAzureCredential } from "@azure/identity";
import ModelClient from "@azure-rest/ai-inference";

const credential = new DefaultAzureCredential();
const client = ModelClient(process.env.AZURE_AI_ENDPOINT!, credential);
// Use exactly as before — credential resolution is transparent
Azure AI Foundry Agent Service

Beyond calling Claude directly, Azure AI Foundry offers a managed Agent Service — analogous to Bedrock's AgentCore. You configure your agent in the Foundry portal (system prompt, tools, knowledge connections), and Azure runs the agent loop. Built-in tools include code interpreter, file search via Azure AI Search, and custom tools backed by Azure Functions. Sessions are persisted on Azure's side, and billing rolls into your subscription.

The trade-off vs. self-hosted is the same as with Bedrock: you give up the ability to intercept individual tool calls or inject custom middleware between reasoning steps, but you gain a fully managed runtime with Azure Monitor integration and zero server management.

Building a Provider-Agnostic Wrapper

You now know the constructor and response-format differences between all four providers. Before writing application code that ties itself to one, let's build a wrapper that hides those differences — so your agent logic never knows or cares which cloud it's running on.

The adapter patternA software design pattern that converts the interface of one class into an interface another class expects. The adapter sits between the caller and the provider, translating calls from the caller's format into each provider's specific format. The caller never sees the translation. is perfect here: create a single class whose chat() method always returns a string. Internally, the class holds whichever cloud client is configured by environment variable. Swap the environment variable, and your entire agent switches clouds with zero code changes.

WHAT — A ClaudeClient class that reads CLAUDE_PROVIDER from the environment, builds the right backend client, and exposes a uniform chat() method. WHY — This is the only place in your codebase that knows which cloud you're on. Every other file just calls client.chat(message). GOTCHA — The Azure response path is different (.choices[0].message.content vs. .content[0].text), so the adapter must normalize it.
"""
claude_client.py — provider-agnostic Claude wrapper
Switch providers by setting CLAUDE_PROVIDER=direct|bedrock|vertex|azure
"""
from __future__ import annotations
import os
from enum import Enum
from typing import Any


class Provider(str, Enum):
    DIRECT  = "direct"
    BEDROCK = "bedrock"
    VERTEX  = "vertex"
    AZURE   = "azure"


class ClaudeClient:
    """
    Uniform interface to Claude regardless of which cloud hosts it.
    Usage:
        client = ClaudeClient()              # reads CLAUDE_PROVIDER from env
        client = ClaudeClient(Provider.BEDROCK)  # explicit
        answer = client.chat("Hello!")
    """

    def __init__(self, provider: Provider | None = None):
        if provider is None:
            raw = os.environ.get("CLAUDE_PROVIDER", "direct").lower()
            provider = Provider(raw)
        self.provider = provider
        self._backend: Any = self._build_backend()

    # ── Private: build the right backend client ───────────────────
    def _build_backend(self) -> Any:
        if self.provider == Provider.DIRECT:
            from anthropic import Anthropic
            return Anthropic()

        elif self.provider == Provider.BEDROCK:
            from anthropic import AnthropicBedrock
            return AnthropicBedrock(
                aws_region=os.environ.get("AWS_REGION", "us-west-2")
            )

        elif self.provider == Provider.VERTEX:
            from anthropic import AnthropicVertex
            return AnthropicVertex(
                project_id=os.environ["GCP_PROJECT"],
                region=os.environ.get("GCP_REGION", "us-east5"),
            )

        elif self.provider == Provider.AZURE:
            from azure.ai.inference import ChatCompletionsClient
            from azure.core.credentials import AzureKeyCredential
            return ChatCompletionsClient(
                endpoint=os.environ["AZURE_AI_ENDPOINT"],
                credential=AzureKeyCredential(os.environ["AZURE_AI_KEY"]),
            )

        raise ValueError(f"Unknown provider: {self.provider}")

    # ── Private: resolve the right model ID for this provider ─────
    def _model_id(self) -> str:
        if self.provider == Provider.DIRECT:
            return os.environ.get("CLAUDE_MODEL", "claude-sonnet-4-6")
        elif self.provider == Provider.BEDROCK:
            return os.environ.get(
                "BEDROCK_MODEL_ID",
                "us.anthropic.claude-sonnet-4-6-20250514-v1:0"
            )
        elif self.provider == Provider.VERTEX:
            return os.environ.get("VERTEX_MODEL_ID", "claude-sonnet-4-6@20250514")
        elif self.provider == Provider.AZURE:
            return os.environ.get("AZURE_DEPLOYMENT_NAME", "claude-sonnet-4-6")
        return "claude-sonnet-4-6"

    # ── Public: the only method your application code calls ───────
    def chat(self, message: str, system: str | None = None, max_tokens: int = 1024) -> str:
        model = self._model_id()

        if self.provider == Provider.AZURE:
            from azure.ai.inference.models import SystemMessage, UserMessage
            msgs = []
            if system:
                msgs.append(SystemMessage(content=system))
            msgs.append(UserMessage(content=message))
            resp = self._backend.complete(model=model, messages=msgs, max_tokens=max_tokens)
            return resp.choices[0].message.content

        else:  # direct, bedrock, vertex — all use Anthropic SDK format
            kwargs: dict = dict(model=model, max_tokens=max_tokens,
                                messages=[{"role": "user", "content": message}])
            if system:
                kwargs["system"] = system
            resp = self._backend.messages.create(**kwargs)
            return resp.content[0].text


# ── Usage ─────────────────────────────────────────────────────────
if __name__ == "__main__":
    import sys
    provider_name = sys.argv[1] if len(sys.argv) > 1 else None
    p = Provider(provider_name) if provider_name else None
    client = ClaudeClient(p)
    print(f"Provider: {client.provider.value}")
    answer = client.chat(
        "In one sentence, what is the main benefit of using Claude through a cloud provider?",
        system="You are concise."
    )
    print(f"Answer: {answer}")
/**
 * claude-client.ts — provider-agnostic Claude wrapper
 * Set CLAUDE_PROVIDER=direct|bedrock|vertex|azure
 */

type Provider = "direct" | "bedrock" | "vertex" | "azure";

interface ChatOptions {
  system?: string;
  maxTokens?: number;
}

export class ClaudeClient {
  private provider: Provider;
  private backend: any;

  constructor(provider?: Provider) {
    this.provider = provider ?? (process.env.CLAUDE_PROVIDER as Provider) ?? "direct";
    this.backend = this.buildBackend();
  }

  private buildBackend() {
    switch (this.provider) {
      case "direct": {
        const { default: Anthropic } = await import("@anthropic-ai/sdk");
        // Note: top-level await needs ESM; use dynamic import pattern in CJS
        return new (require("@anthropic-ai/sdk"))();
      }
      case "bedrock":
        return new (require("@anthropic-ai/bedrock-sdk").AnthropicBedrock)({
          awsRegion: process.env.AWS_REGION ?? "us-west-2",
        });
      case "vertex":
        return new (require("@anthropic-ai/vertex-sdk").AnthropicVertex)({
          projectId: process.env.GCP_PROJECT!,
          region: process.env.GCP_REGION ?? "us-east5",
        });
      case "azure": {
        const ModelClient = require("@azure-rest/ai-inference").default;
        const { AzureKeyCredential } = require("@azure/core-auth");
        return ModelClient(
          process.env.AZURE_AI_ENDPOINT!,
          new AzureKeyCredential(process.env.AZURE_AI_KEY!)
        );
      }
    }
  }

  private modelId(): string {
    const envMap: Record = {
      direct:  ["CLAUDE_MODEL",          "claude-sonnet-4-6"],
      bedrock: ["BEDROCK_MODEL_ID",      "us.anthropic.claude-sonnet-4-6-20250514-v1:0"],
      vertex:  ["VERTEX_MODEL_ID",       "claude-sonnet-4-6@20250514"],
      azure:   ["AZURE_DEPLOYMENT_NAME", "claude-sonnet-4-6"],
    };
    const [envKey, fallback] = envMap[this.provider];
    return process.env[envKey] ?? fallback;
  }

  async chat(message: string, opts: ChatOptions = {}): Promise {
    const model = this.modelId();
    const maxTokens = opts.maxTokens ?? 1024;

    if (this.provider === "azure") {
      const { isUnexpected } = require("@azure-rest/ai-inference");
      const msgs: any[] = [];
      if (opts.system) msgs.push({ role: "system", content: opts.system });
      msgs.push({ role: "user", content: message });
      const res = await this.backend.path("/chat/completions").post({
        body: { model, messages: msgs, max_tokens: maxTokens },
      });
      if (isUnexpected(res)) throw new Error(res.body.error.message);
      return res.body.choices[0].message.content;
    }

    // direct / bedrock / vertex — Anthropic SDK format
    const kwargs: any = {
      model, max_tokens: maxTokens,
      messages: [{ role: "user", content: message }],
    };
    if (opts.system) kwargs.system = opts.system;
    const resp = await this.backend.messages.create(kwargs);
    return resp.content[0].text;
  }
}

// ── Usage ─────────────────────────────────────────────────────────
const client = new ClaudeClient();
const answer = await client.chat(
  "In one sentence, what is the benefit of using a provider-agnostic wrapper?"
);
console.log(answer);
✅ What Just Happened? You built a single-file adapter that lets your entire agent codebase remain cloud-agnostic. Switching from the direct API to Bedrock is now one environment variable change: CLAUDE_PROVIDER=bedrock. No application code changes. This is the pattern used by teams that start on the direct API for speed and migrate to Bedrock or Vertex when procurement requires it.

Decision Framework: Which Provider to Use?

Click each question to reveal the recommendation

▶ Do you need the newest Claude model on day of release?
Direct Anthropic API. New models land on the direct API first. Bedrock, Vertex, and Azure receive them days to weeks later. For startups racing to ship new capabilities, this matters.
▶ Is your team fully AWS-native (existing EA/BAA, data in S3, compute in EC2/ECS)?
AWS Bedrock. No new vendor contract. Data stays in your VPC. Compliance teams don't need to review a new data processor. Bedrock Guardrails and Knowledge Bases add enterprise safety and RAG without new infrastructure.
▶ Are you on GCP (BigQuery, Cloud Run, Vertex Pipelines, GKE)?
Google Vertex AI. ADC makes auth invisible across local dev and GKE workloads. GCP committed-use credits and discounts apply to Claude calls. Vertex Agent Engine deploys your Python agent without containerizing.
▶ Is your organization Microsoft-heavy (Azure AD/Entra, M365, existing Azure commitment)?
Azure AI Foundry. Managed Identity means no API keys in any environment. Bills go to existing Azure credits. Azure AI Foundry Agent Service integrates with Azure AI Search, Azure Functions, and the broader Microsoft ecosystem.
▶ None of the above — what's the simplest place to start?
Direct Anthropic API. One API key, one SDK, no cloud setup. Graduate to a cloud provider when procurement requires it. Use the provider-agnostic wrapper from this module so the migration is a one-line environment variable change.

Migration Path

The typical journey for a growing team looks like this:

  1. Stage 1 (prototype): Direct API → fastest to ship, zero cloud setup.
  2. Stage 2 (scaling startup): Direct API with the provider-agnostic wrapper → same speed, but the migration is one environment variable away.
  3. Stage 3 (enterprise adoption): Bedrock, Vertex, or Azure → procurement unlocks, data residency requirements are met, cloud-native tooling (Guardrails, Knowledge Bases, Agent Engine) replaces custom code.

The provider-agnostic wrapper you built in the previous section means Stage 3 is a configuration change, not a rewrite.

Hands-On Lab: Call Claude on All Four Providers

What You'll Build

A provider-agnostic CLI tool that calls Claude through whichever cloud provider you configure, using the wrapper from this module. By the end you'll have connected to at least one cloud provider beyond the direct API and seen that your application code requires zero changes.

Time: 30–60 min (depending on how many providers you set up) • Minimum: direct API + one cloud provider

Prerequisites

  • Python 3.9+ or Node.js 18+
  • ANTHROPIC_API_KEY set (for Step 1)
  • For Step 2: AWS account with Claude access enabled in Bedrock
  • For Step 3: GCP project with Vertex AI API enabled and Claude in Model Garden
  • For Step 4: Azure subscription with Claude deployed in Azure AI Foundry

Files You'll Create

  • claude_client.py — provider-agnostic wrapper (from Section 5)
  • chat_cli.py — simple CLI that uses the wrapper
  • .env — your credentials (never commit this)

Environment Setup

mkdir cloud-provider-lab && cd cloud-provider-lab
python -m venv venv
# macOS/Linux:
source venv/bin/activate
# Windows:
# venv\Scripts\activate

# Install all providers — only the ones you configure will be used
pip install "anthropic[bedrock,vertex]" azure-ai-inference azure-core azure-identity python-dotenv
mkdir cloud-provider-lab && cd cloud-provider-lab
npm init -y
npm install @anthropic-ai/sdk @anthropic-ai/bedrock-sdk @anthropic-ai/vertex-sdk \
  @azure-rest/ai-inference @azure/core-auth @azure/identity dotenv

Step 1: Call Claude via the Direct API (Baseline)

This step verifies your base setup. You're using the wrapper from Section 5 here, so even the "direct" mode goes through the same code path that will later switch to Bedrock/Vertex/Azure.

Create claude_client.py by copying the full solution from Section 5. Then create chat_cli.py:

"""chat_cli.py — run with: python chat_cli.py"""
from claude_client import ClaudeClient, Provider
import sys

provider_arg = sys.argv[1] if len(sys.argv) > 1 else None
p = Provider(provider_arg) if provider_arg else None

client = ClaudeClient(p)
print(f"[Provider: {client.provider.value}]")
answer = client.chat(
    "Name one advantage of using Claude through a managed cloud provider. One sentence.",
    system="Be concise."
)
print(answer)

Run it:

ANTHROPIC_API_KEY=your-key python chat_cli.py direct

Expected output:

[Provider: direct]
One advantage is that managed cloud providers consolidate billing, IAM, and compliance under your existing cloud contract, removing the need for a separate vendor relationship with Anthropic.
✅ Step 1 Checkpoint: You should see [Provider: direct] followed by a one-sentence answer. If you see AuthenticationError, check that ANTHROPIC_API_KEY is exported in your shell.

Step 2: Call Claude via AWS Bedrock

This step uses the same chat_cli.py from Step 1 — only the provider flag and AWS credentials change. Skip to Step 3 if you don't have an AWS account.

Configure AWS credentials (one of these three methods):

# Method A: environment variables (fastest for dev)
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=your-secret
export AWS_REGION=us-west-2

# Method B: AWS CLI (recommended for dev)
aws configure   # enter key, secret, region=us-west-2, output=json

# Method C: AWS SSO (enterprise)
aws sso login --profile my-profile
export AWS_PROFILE=my-profile
python chat_cli.py bedrock

Expected output:

[Provider: bedrock]
One advantage is that AWS Bedrock integrates Claude billing into your existing AWS account, enabling enterprises to use their existing contract and compliance approvals.
✅ Step 2 Checkpoint: Same code, same chat_cli.py, new provider. If you see AccessDeniedException, your IAM user/role is missing bedrock:InvokeModel. If you see ResourceNotFoundException, the model ID in the wrapper doesn't match what's enabled in your Bedrock console region — check that you requested model access for Claude Sonnet in us-west-2.
Troubleshooting — Bedrock
  • ValidationException: Unknown model ID → Wrong model ID format. Bedrock needs anthropic.claude-* not claude-*.
  • AccessDeniedException → Add bedrock:InvokeModel to your IAM policy.
  • botocore.exceptions.NoCredentialsError → Run aws configure or set AWS_ACCESS_KEY_ID env var.

Step 3: Call Claude via Google Vertex AI

This step requires a GCP project with Vertex AI enabled and Claude deployed from Model Garden. Free-tier GCP accounts get $300 in credits — enough to complete this lab.

# Set up ADC — opens a browser window to authenticate
gcloud auth application-default login

# Set env vars
export GCP_PROJECT=your-project-id
export GCP_REGION=us-east5

python chat_cli.py vertex
✅ Step 3 Checkpoint: [Provider: vertex] followed by a response. If you see 403 PermissionDenied, your GCP user account (or service account) is missing roles/aiplatform.user on the project. If you see 404 Not Found, Claude has not been enabled in Model Garden for your project — go to Vertex AI → Model Garden → Claude → Enable.

Step 4: Call Claude via Azure AI

This step requires an Azure AI Foundry resource with Claude deployed as a serverless endpoint.

export AZURE_AI_ENDPOINT=https://your-resource.services.ai.azure.com/models
export AZURE_AI_KEY=your-azure-key
export AZURE_DEPLOYMENT_NAME=claude-sonnet-4-6   # your deployment name

python chat_cli.py azure
✅ Step 4 Checkpoint: [Provider: azure] followed by a response. If you see 401 Unauthorized, double-check the API key. If you see 404 Not Found, the deployment name in AZURE_DEPLOYMENT_NAME doesn't match the name you gave it in Azure AI Foundry.

Step 5: Run All Four with One Script

This final step confirms that the same application logic runs identically on all four providers — exactly what the wrapper was designed to achieve.

"""all_providers.py — test all configured providers"""
from claude_client import ClaudeClient, Provider

QUESTION = "Name the cloud you're running on. One sentence."

for p in Provider:
    try:
        client = ClaudeClient(p)
        answer = client.chat(QUESTION)
        print(f"[{p.value:8}] {answer}")
    except Exception as e:
        print(f"[{p.value:8}] SKIPPED — {type(e).__name__}: {e}")
python all_providers.py

Expected output (with all four configured):

[direct  ] I'm running on Anthropic's direct API, not a managed cloud provider.
[bedrock ] I'm running on AWS Bedrock, Amazon's managed AI inference service.
[vertex  ] I'm running on Google Cloud Vertex AI, Google's managed AI platform.
[azure   ] I'm running on Microsoft Azure AI Foundry, Microsoft's managed AI platform.
🎉 Congratulations! You've completed M21B. Your ClaudeClient wrapper lets you switch between all four Claude access paths with one environment variable. The same agent logic, the same code, four different enterprise on-ramps. Onward to M22: Cost Optimization.

Knowledge Check

Test your understanding. Select the best answer for each question.

Q1: You want to call Claude on AWS Bedrock using the cross-region inference profile for Claude Sonnet 4.6. Which model ID format is correct?

a) claude-sonnet-4-6
b) us.anthropic.claude-sonnet-4-6-20250514-v1:0
c) anthropic/claude-sonnet-4-6@latest
d) claude-sonnet-4-6-v1:0
✅ Correct! Bedrock cross-region inference profiles use the us. (or eu.) regional prefix followed by the full date-stamped model identifier. The simple claude-sonnet-4-6 format works on the direct API and Vertex, not Bedrock.
❌ Not quite. Cross-region inference profile IDs start with a regional prefix (us.) and include a date stamp. us.anthropic.claude-sonnet-4-6-20250514-v1:0 is the correct format.

Q2: What does ADC stand for in the Google Cloud context, and how do you initialize it for local development?

a) Azure Directory Credentials — az login
b) Anthropic Default Client — automatically set by anthropic[vertex]
c) Application Default Credentials — gcloud auth application-default login
d) Adaptive Data Compression — configured in the Vertex AI console
✅ Correct! ADC (Application Default Credentials) is GCP's standard credential discovery chain. On local dev you run gcloud auth application-default login. On GKE/Cloud Run, the runtime metadata server provides credentials automatically — no CLI command needed.
❌ ADC stands for Application Default Credentials — GCP's mechanism for discovering credentials automatically across dev, CI, and production. Initialize it locally with gcloud auth application-default login.

Q3: When calling Claude through Azure AI Foundry, which Python package do you use instead of the Anthropic SDK?

a) anthropic[azure]
b) azure-ai-inference
c) openai with an Azure endpoint
d) msrest / azure-mgmt-cognitiveservices
✅ Correct! Unlike Bedrock and Vertex (which have native Anthropic SDK clients), Azure AI Foundry uses Microsoft's own azure-ai-inference package. The request format is similar to OpenAI's Chat Completions API, and the response uses choices[0].message.content rather than Anthropic's content[0].text.
❌ Azure AI Foundry uses Microsoft's azure-ai-inference package — not the Anthropic SDK. There is no anthropic[azure] extra. The Anthropic SDK only has native clients for Bedrock (AnthropicBedrock) and Vertex (AnthropicVertex).

Q4: Your startup wants to ship a feature using the newest Claude model the day it is released. Which access method gives you Day-0 access?

a) Direct Anthropic API (api.anthropic.com)
b) AWS Bedrock — it mirrors Anthropic's release schedule
c) Google Vertex AI — Google has first-party access to all models
d) Azure AI Foundry — Microsoft's partnership with Anthropic ensures simultaneous launch
✅ Correct! New Claude models are always available on the direct Anthropic API first. Bedrock, Vertex, and Azure typically receive them days to weeks later, after the cloud providers complete their own validation and compliance processes. For startups shipping on the cutting edge, this matters.
❌ Only the direct Anthropic API provides Day-0 access. Cloud providers (Bedrock, Vertex, Azure) receive new Claude models days to weeks after the direct API launch, because each provider runs its own validation process before making models available to customers.

Q5: What is the main purpose of a cross-region inference profile in AWS Bedrock?

a) To encrypt inference traffic between AWS regions for HIPAA compliance
b) To replicate your fine-tuned model across multiple AWS regions for disaster recovery
c) To route inference requests across multiple AWS regions to achieve higher throughput and availability
d) To synchronize rate limits between your Anthropic account and your Bedrock account
✅ Correct! Cross-region inference profiles (identified by the us. or eu. prefix) allow Bedrock to route your request to whichever US (or EU) region has available capacity at that moment. This gives higher effective throughput than a single-region endpoint, especially at scale.
❌ Cross-region inference profiles route requests across multiple AWS regions to achieve higher throughput by load-balancing across available capacity. The us. prefix in the model ID signals to Bedrock that it may use any US region for this request.

Q6: A healthcare company already has an AWS Enterprise Agreement and BAA. They want to use Claude for clinical decision support. What is the PRIMARY reason to use Bedrock over the direct Anthropic API?

a) Bedrock tokens are cheaper than the direct API
b) Bedrock provides better model quality for medical use cases
c) Bedrock automatically anonymizes PHI before sending it to Claude
d) Bedrock is covered under their existing AWS BAA — no new vendor legal review required
✅ Correct! The primary driver for enterprise cloud-provider adoption is organizational procurement, not pricing or model quality. Bedrock, as an AWS service, is covered by the company's existing Business Associate Agreement. Adding the direct Anthropic API would require legal to review a new data processing agreement — a process that can take months.
❌ Bedrock token pricing is the same as the direct API, and the model quality is identical (it's the same Claude). The primary driver is that Bedrock is already covered by the company's existing AWS BAA, eliminating the need for a new vendor legal review.

Answer all questions to see your score

Module Summary

Key Takeaways

  • One Claude, four on-ramps: The model quality and API protocol are identical across direct API, Bedrock, Vertex, and Azure AI. What changes is authentication, billing, and the SDK client constructor.
  • Bedrock = AWS-native: AnthropicBedrock(aws_region=...) — uses IAM credentials. Model IDs use anthropic.claude-*-v1:0 or us.anthropic.claude-* for cross-region. Adds Guardrails and Knowledge Bases.
  • Vertex AI = GCP-native: AnthropicVertex(project_id=..., region=...) — uses ADC. Model IDs use date stamps: claude-*@YYYYMMDD. Use us-east5 or europe-west1.
  • Azure AI = Microsoft-native: Uses azure-ai-inference SDK, not the Anthropic SDK. Response shape is OpenAI-compatible (choices[0].message.content). Managed Identity removes API keys from production.
  • Provider-agnostic wrapper: One class, one CLAUDE_PROVIDER env var, all four providers. Migrate clouds with zero application code changes.
  • Decision rule: Start direct for speed. Graduate to the cloud that already holds your data and your procurement contract.

Next: M22 — Cost Optimization

Now that you can call Claude from any cloud, M22 explores how to make those calls cheaper: prompt caching (up to 90% cost reduction on repeated context), model routing (use Claude Haiku for simple tasks, Sonnet for complex ones), the Message Batches API (50% off for async workloads), and token optimization techniques. The provider-agnostic wrapper you built here will be extended with a cost-aware routing layer.