APPENDIX MODULE
ℹ Optional supplementary content. This module is not required for the Claude Certified Architect exam. It teaches a practical skill — running agents against open source models — that complements the main curriculum.

M28: Open Source Models — Running Agents Without an API Key

The same agent patterns you learned throughout this course work with Mistral-7B, Llama-3, and 100+ other open source models. This module teaches the provider-swap skill: when to use them, how to run them locally with OllamaOllama is a free, open-source tool that runs LLMs on your local machine. Think of it as Docker for AI models — pull, run, done. No API key, no rate limits, no data leaving your machine., and what breaks when you do.

Learning Objectives

🏠
Install Ollama and run Mistral-7B locally with zero API cost
🔄
Swap a Claude agent to any open source provider with a one-line change
👥
Explain the OpenAI-compatible API and why it's the universal adapter
🚫
Identify which Claude features have no open-source equivalent
🎯
Choose the right provider for each use case using a decision framework

Why Open Source Models Matter

This course has used Claude throughout — and for good reason. The main curriculum builds toward the Claude Certified Architect exam, and Claude-specific features like extended thinking, computer use, and hooks have no open-source equivalents. If you're taking the cert, Claude is the right tool.

But "Claude is the right tool for the cert" isn't the same as "Claude is always the right tool." Three situations push developers toward open source:

✅ Reason 1: Cost at Scale

At low volume, Claude's per-token pricing is barely noticeable. At high volume, the math changes. If you're running 10,000 classification tasks per day on a simple "is this spam or not" prompt, Mistral-7B on a $40/month VPS is cheaper than Claude by an order of magnitude. The quality gap on simple tasks is small; the cost gap is huge.

✅ Reason 2: Data Privacy

Some organizations — hospitals, law firms, defense contractors, banks — cannot send data to a third-party API. It's not a preference, it's a compliance requirement. For these use cases, local inference is the only path: the model runs inside your network, your data never leaves. Mistral-7B running on a laptop in a hospital's secure zone is compliant; calling any external API is not.

✅ Reason 3: Portability Knowledge

Understanding the provider swap teaches you something valuable: which parts of your agent are model-specific and which are model-agnostic. RAG pipelines, ReAct loops, guardrails, tracing — all of these work identically regardless of whether you're calling Claude or Mistral. The agentic patterns you've learned belong to you, not to any specific provider.

The position of this course: Use Claude for production agentic workloads, reasoning-heavy tasks, cert prep, and anywhere you need hooks/sessions. Use open source for batch processing, data-private environments, high-volume simple tasks, and fine-tuning on proprietary data. This module gives you the technical skill to do both.

The Open Source Provider Landscape

There are four main ways to run open source models. They differ on where the model runs, who operates the hardware, and how much they cost.

Ollama
Local · Free
Runs on your laptop or server. No API key, no rate limits, no data leaves your machine. Supports Mistral, Llama, Gemma, Phi, and 100+ models. Recommended starting point.
LM Studio
Local · Free · GUI
Desktop app for Windows/Mac. Good for learners who prefer a GUI. Exposes the same OpenAI-compatible API as Ollama once a model is loaded.
Groq
Cloud · Fast · Cheap
Hosted inference on custom LPU hardware. Very fast (often faster than Claude). Free tier available. Use when local hardware is insufficient.
Together AI
Cloud · Many Models
Hundreds of open source models, pay-per-token. Good for experimenting across model families. Also offers fine-tuning.

Decision Matrix: Which Provider for Which Situation?

Situation Recommended Why
Development on a laptop Ollama Zero cost, instant setup, offline capable
Data privacy requirement Ollama Data never leaves local machine
No GPU, need low latency Groq LPU hardware, ~500 tokens/sec for Mistral
Experimenting with model families Together AI 200+ models, easy model swapping
Windows, prefer GUI LM Studio Desktop app, no CLI needed
Production, agentic tasks Claude Best reasoning, reliability, prompt caching, hooks
Animation: Hardware Requirements by Model Size

How much RAM does each model need? Click a model to see its requirement.

RAM required (4-bit quantized)

The OpenAI-Compatible API: The Universal Adapter

💡 Everyday Analogy

Before USB-C: your iPhone needed Lightning, your Android needed Micro-USB, your laptop needed a barrel jack. You carried three cables everywhere. Plug shapes were a choice each manufacturer made independently, and you paid for that divergence.

The pain: OpenAI published their Chat Completions API in 2023, and it became the de facto standard for how applications talk to LLMs. Not because OpenAI planned it that way, but because so many tools and libraries were already built around it. Every new inference server faced the same choice: invent a new API shape (and lose all existing integrations) or implement the ChatCompletions schema (and work with everything immediately).

The mapping: Ollama, Groq, Together AI, LM Studio, and dozens of other providers chose compatibility. They all speak the same "ChatCompletions dialect." You can point the same Python code at localhost:11434 (Ollama), api.groq.com, or api.together.ai by changing one URL. The OpenAI SDK becomes your universal remote.

📐 Technical Definition

The OpenAI Chat Completions API is a specific JSON schema for LLM requests and responses. A request looks like: {"model": "...", "messages": [{"role": "user", "content": "Hello"}]}. A response includes a choices array where choices[0].message.content holds the reply. Any server that accepts this exact shape and returns this exact shape is "OpenAI-compatible."

The Anthropic Messages API uses a different — and in some ways cleaner — schema: system is a top-level field (not a message), the response lives in content[0].text, and max_tokens is always required. The two schemas are close but not identical, which is why a direct swap breaks without a thin adapter layer.

Animation: Anthropic Messages API vs. OpenAI Chat Completions
ANTHROPIC MESSAGES API
OPENAI CHAT COMPLETIONS API

Two Swap Strategies

There are two clean ways to make your Claude code work with open source providers. Pick the one that fits your situation.

Strategy A: OpenAI SDK
Use the openai Python/JS package pointed at a different base_url. One dependency, works with any OpenAI-compatible server. Best for simple use cases.
Strategy B: LiteLLM
LiteLLMLiteLLM is a Python library that provides a single unified API for 100+ LLM providers. One function call, one response format — regardless of whether you're hitting Claude, GPT-4, Mistral, or Llama. wraps every provider behind one litellm.completion() call. Provider is just a string in the model name: "claude-sonnet-4-6", "ollama/mistral", "groq/llama3-8b-8192". Best for multi-provider routing.

Running Mistral-7B Locally with Ollama

Ollama is the simplest path to local model inference. It handles model downloading, quantization, hardware detection, and serving — all behind a single CLI. You install it once, pull a model like a Docker image, and your laptop becomes an LLM server.

Animation: Ollama Install & Model Pull
$

Installation

macOS / Linux
curl -fsSL https://ollama.ai/install.sh | sh
Windows
# Download the installer from https://ollama.ai/download/windows
# Then in PowerShell or Command Prompt:
winget install Ollama.Ollama   # or use the .exe installer

Pull and Run a Model

bash
# Pull Mistral-7B (about 4.1 GB download, 4-bit quantized)
ollama pull mistral

# Test it interactively (type /bye to exit)
ollama run mistral

# Or start the API server (runs on http://localhost:11434)
ollama serve
ℹ Hardware Note

Mistral-7B at 4-bit quantizationQuantization reduces a model's numeric precision (e.g. from 16-bit floats to 4-bit integers), shrinking file size and RAM usage by ~4×. Quality drops slightly but is barely noticeable on most tasks. Without quantization, Mistral-7B would need ~14 GB of RAM instead of ~5 GB. needs approximately 5 GB of RAM. It runs on a MacBook Air (8 GB), a Windows laptop with 16 GB RAM, or any Linux machine. It will use your GPU if available (NVIDIA, AMD, or Apple Silicon) — and fall back to CPU if not, which is slower but still functional.

If you have less than 8 GB of RAM, try the Phi-3 Mini model instead: ollama pull phi3:mini — it's 2.3 GB and handles most text tasks well.

Making Your First API Call to Ollama

Once Ollama is running (ollama serve), it exposes an HTTP endpoint at localhost:11434. You can call it with curl, the OpenAI SDK, or LiteLLM — all three work identically.

curl (test it works)
curl http://localhost:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "mistral",
    "messages": [{"role": "user", "content": "Explain what an LLM is in one sentence."}]
  }'
⚠ Connection Refused?

If you see curl: (7) Failed to connect to localhost port 11434, Ollama isn't running. Open a terminal and run ollama serve, then retry. On macOS, Ollama can also run as a menu bar app that starts automatically — check your menu bar if you installed via .dmg.

Provider Swap: Claude → Mistral-7B

Here's the same "hello world" call made against Claude and Mistral-7B side by side. Read the differences carefully — the concepts are identical, only three things change in the code.

Strategy A: OpenAI SDK (points to Ollama)

# pip install anthropic openai python-dotenv
import os
import anthropic
import openai
from dotenv import load_dotenv

load_dotenv()

PROMPT = "Explain what an LLM is in one sentence."

# ── CHUNK 1: Claude (Anthropic SDK) ─────────────────────────────────────────
# WHAT: Create the Anthropic client and call claude-sonnet-4-6.
# WHY:  This is the pattern you've used throughout the course.
# GOTCHA: ANTHROPIC_API_KEY must be set — client reads it from the environment.
claude_client = anthropic.Anthropic()

claude_response = claude_client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=256,          # required by Anthropic — no default
    system="You are a helpful assistant.",   # top-level field in Anthropic API
    messages=[{"role": "user", "content": PROMPT}]
)
claude_text = claude_response.content[0].text  # Anthropic response path

# ── CHUNK 2: Mistral via Ollama (OpenAI SDK) ─────────────────────────────────
# WHAT: Create an OpenAI client but point its base_url at Ollama's local server.
# WHY:  Ollama implements the OpenAI Chat Completions schema exactly.
#       The openai package doesn't care if it's talking to OpenAI or Ollama —
#       it just sends HTTP to whatever base_url you give it.
# GOTCHA: api_key="ollama" is a dummy value — Ollama ignores the key,
#         but the SDK requires the field to be non-empty.
ollama_client = openai.OpenAI(
    base_url="http://localhost:11434/v1",
    api_key="ollama"         # dummy — Ollama doesn't authenticate
)

ollama_response = ollama_client.chat.completions.create(
    model="mistral",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},  # system is a message here
        {"role": "user", "content": PROMPT}
    ]
    # max_tokens is optional in OpenAI schema — omitting it uses model's default
)
ollama_text = ollama_response.choices[0].message.content  # OpenAI response path

# ── CHUNK 3: Print results side by side ────────────────────────────────────
print(f"Claude:  {claude_text}")
print(f"Mistral: {ollama_text}")
// npm install @anthropic-ai/sdk openai dotenv
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import 'dotenv/config';

const PROMPT = 'Explain what an LLM is in one sentence.';

// ── CHUNK 1: Claude (Anthropic SDK) ─────────────────────────────────────────
// WHAT: Anthropic client — reads ANTHROPIC_API_KEY from environment.
// GOTCHA: response.content is an array; text is at index [0].text.
const claudeClient = new Anthropic();

const claudeResponse = await claudeClient.messages.create({
  model: 'claude-sonnet-4-6',
  max_tokens: 256,              // required by Anthropic
  system: 'You are a helpful assistant.',
  messages: [{ role: 'user', content: PROMPT }]
});
const claudeText = claudeResponse.content[0].text;

// ── CHUNK 2: Mistral via Ollama (OpenAI SDK) ─────────────────────────────────
// WHAT: OpenAI SDK redirected to Ollama's local endpoint.
// WHY:  Ollama speaks OpenAI Chat Completions, so the same SDK works.
// GOTCHA: apiKey must be a non-empty string — 'ollama' is conventional.
const ollamaClient = new OpenAI({
  baseURL: 'http://localhost:11434/v1',
  apiKey: 'ollama'
});

const ollamaResponse = await ollamaClient.chat.completions.create({
  model: 'mistral',
  messages: [
    { role: 'system', content: 'You are a helpful assistant.' },
    { role: 'user', content: PROMPT }
  ]
  // maxTokens optional — omit to use model default
});
const ollamaText = ollamaResponse.choices[0].message.content;

// ── CHUNK 3: Print results ───────────────────────────────────────────────────
console.log(`Claude:  ${claudeText}`);
console.log(`Mistral: ${ollamaText}`);
What Just Happened? You sent the same prompt to two different models using two different SDKs. The logic — create client, build message, extract text — is identical. Only three things changed: (1) the import and client constructor, (2) where system lives in the request, and (3) how you get text out of the response. Everything else — your prompts, your message history, your business logic — is portable.

Strategy B: LiteLLM (one function, any provider)

# pip install litellm python-dotenv
import litellm
from dotenv import load_dotenv

load_dotenv()

# ── CHUNK 1: The provider-agnostic call ──────────────────────────────────────
# WHAT: litellm.completion() has the same signature regardless of provider.
# WHY:  LiteLLM translates your call to each provider's native format,
#       then translates the response back to a unified OpenAI-shaped response.
# GOTCHA: The model string encodes the provider: "ollama/mistral", not "mistral".
#         For Claude: just "claude-sonnet-4-6" (LiteLLM auto-detects Anthropic).

MESSAGES = [
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Explain what an LLM is in one sentence."}
]

# Call Claude
claude_resp = litellm.completion(model="claude-sonnet-4-6", messages=MESSAGES)

# Call Mistral via Ollama — one string change
mistral_resp = litellm.completion(model="ollama/mistral", messages=MESSAGES)

# Call Mistral via Groq (cloud, faster) — another string change
# groq_resp = litellm.completion(model="groq/mistral-saba-v1", messages=MESSAGES)

# ── CHUNK 2: Response format is unified ─────────────────────────────────────
# WHAT: LiteLLM normalizes every provider's response to OpenAI's shape.
# WHY:  You always extract text the same way — no per-provider conditionals.
for label, resp in [("Claude", claude_resp), ("Mistral (Ollama)", mistral_resp)]:
    text = resp.choices[0].message.content   # same path for every provider
    tokens = resp.usage.total_tokens
    print(f"{label}: {text}")
    print(f"  Tokens used: {tokens}\n")
// npm install litellm dotenv
// Note: LiteLLM's JS package is a thin wrapper; the Python package is more feature-complete.
// For Node.js, the OpenAI SDK strategy (Strategy A) is generally preferred.
// This example shows the concept using fetch for the LiteLLM proxy pattern.

import 'dotenv/config';

// LiteLLM also ships a proxy server. Start it with:
//   litellm --model ollama/mistral --port 4000
// Then call it like any OpenAI-compatible endpoint:

const MESSAGES = [
  { role: 'system', content: 'You are a helpful assistant.' },
  { role: 'user', content: 'Explain what an LLM is in one sentence.' }
];

async function callProvider(baseURL, apiKey, model) {
  const resp = await fetch(`${baseURL}/chat/completions`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${apiKey}`
    },
    body: JSON.stringify({ model, messages: MESSAGES })
  });
  if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
  const data = await resp.json();
  return data.choices[0].message.content;
}

// Claude via LiteLLM proxy (start: litellm --model claude-sonnet-4-6 --port 4000)
// Requires ANTHROPIC_API_KEY set in the proxy server's environment, not here.
// Skip this call if you don't have an Anthropic key — Mistral below works offline.
const claudeText = await callProvider(
  'http://localhost:4000',
  'any-string',   // LiteLLM proxy handles auth internally
  'claude-sonnet-4-6'
);

// Mistral via LiteLLM proxy (start: litellm --model ollama/mistral --port 4000)
const mistralText = await callProvider(
  'http://localhost:4000',
  'any-string',   // LiteLLM proxy doesn't require auth by default
  'ollama/mistral'
);

console.log('Claude:', claudeText);
console.log('Mistral:', mistralText);

What Doesn't Port Over

The provider swap covers a lot: basic completions, multi-turn chat, system prompts, temperature, streaming. But several Claude capabilities have no open-source equivalent. This isn't a criticism of open source — it's just the reality of the feature landscape.

⚠ Common Misconceptions

"I can just swap to Mistral-7B and get the same quality." — For simple tasks (extract this JSON, classify this sentence), quality is close. For complex agentic tasks — multi-step reasoning, tool chaining across 5+ steps, following nuanced instructions — the quality gap between a 7B model and a frontier model is significant. Test your specific task before committing to a provider swap in production.

"Open source models support tool use the same way." — Ollama's tool use support depends on the model. Mistral-7B-Instruct supports function calling, but reliability is lower than Claude's. LLama-3-70B is much better. Test before deploying agentic tool use with open source models.

Prompt caching Anthropic-only. Dramatically reduces cost and latency for long system prompts. No equivalent in Ollama, Groq, or Together. See M22.
Extended thinking Claude-specific reasoning mode where the model works through problems step-by-step before responding. Open source models have chain-of-thought prompting, but not this integrated mode.
Computer use Claude-specific capability to see and interact with desktop GUIs. No open source equivalent at this reliability level.
Claude Code / hooks The Agent SDK, hooks system, and session management (M25-M26) are Anthropic platform features. They don't exist for Ollama-backed models.
Context window Mistral-7B: 32K tokens. Claude Sonnet: 200K tokens. For long documents, multi-session agents, and large codebases, this gap matters.
Native citations Claude can attribute claims to source documents at the API level. With open source, you'd implement this manually using RAG patterns from M09.
✅ What IS Portable (the good news)

Most of what you learned in this course transfers directly: RAG pipelines (M09-M10), ReAct loops (M12), multi-agent patterns (M14), guardrails (M16-M17), tracing and logging (M19), cost optimization patterns except prompt caching (M22), and all deployment patterns (M21-M22B). The agentic architecture is model-agnostic. Only the model-specific features are Claude-specific.

When to Use Open Source in Production

The honest answer is "it depends on your task." Here's a framework for thinking through the decision:

Task Type Use Claude Consider Open Source
Complex multi-step reasoning ★ Claude Quality gap is significant for 7B models
Simple classification / extraction Either works ★ Open source — cheaper at scale
Agentic tool chaining (3+ steps) ★ Claude Open source reliability drops on tool selection
Batch offline processing Either works ★ Open source — no API latency, free
Data privacy requirement Cannot use ★ Ollama — only option
Fine-tuning on proprietary data Cannot self-host ★ Open source — fine-tunable
Cert prep (M25-M27) ★ Claude Exam tests Claude-specific features
💡 Hybrid Routing Pattern (Preview)

Before: Every single request in your system goes through the same model, paying the same per-token price, regardless of complexity.

The pain: A simple "classify this as spam or not" prompt costs the same per token as a complex "analyze this medical record and identify risk factors" prompt. That's like paying a consultant's rate to have them press an elevator button.

The mapping: Route cheap tasks to cheap models. Use an open source model (or even a regex) for intent classification — "is this a greeting, a question, or a complaint?" — and reserve Claude for the complex reasoning tasks. M22 covers this pattern in depth. The idea: use Mistral-7B as a traffic cop, Claude as the specialist it routes to.

Code Walkthrough: Provider-Agnostic CLI Chat

This is the same multi-turn CLI chat from M01 Step 5, refactored to accept a --provider flag. Run it with claude, mistral, or groq — the conversation logic is identical, only the model call changes.

This is the pattern to internalize: isolate the model call behind a thin function. Everything above that function — message history, user input loop, error handling — never needs to change when you swap providers.

# provider_agnostic_chat.py
# Usage:  python provider_agnostic_chat.py --provider claude
#         python provider_agnostic_chat.py --provider mistral
#         python provider_agnostic_chat.py --provider groq
# pip install anthropic openai litellm python-dotenv

import argparse
import anthropic
import openai
from dotenv import load_dotenv

load_dotenv()

# ── CHUNK 1: Provider configuration ─────────────────────────────────────────
# WHAT: Map provider name to (client, model_id, response_extractor).
# WHY:  All provider-specific logic lives here — the rest of the code
#       never branches on provider.
# GOTCHA: Groq requires GROQ_API_KEY in your environment.
PROVIDERS = {
    "claude": {
        "sdk": "anthropic",
        "model": "claude-sonnet-4-6",
    },
    "mistral": {
        "sdk": "openai_compat",
        "base_url": "http://localhost:11434/v1",
        "api_key": "ollama",
        "model": "mistral",
    },
    "groq": {
        "sdk": "openai_compat",
        "base_url": "https://api.groq.com/openai/v1",
        "api_key_env": "GROQ_API_KEY",
        "model": "llama3-8b-8192",
    }
}

def build_client(provider_name: str):
    import os
    cfg = PROVIDERS[provider_name]
    if cfg["sdk"] == "anthropic":
        return ("anthropic", anthropic.Anthropic(), cfg["model"])
    else:
        key = cfg.get("api_key") or os.environ[cfg["api_key_env"]]
        client = openai.OpenAI(base_url=cfg["base_url"], api_key=key)
        return ("openai_compat", client, cfg["model"])

# ── CHUNK 2: The unified chat function ──────────────────────────────────────
# WHAT: Single function that calls whichever provider was selected.
# WHY:  Branching once here means zero branching in the main loop.
# GOTCHA: Anthropic requires system as top-level; OpenAI compat uses a message.
def chat(sdk_type, client, model, messages, system="You are a helpful assistant."):
    try:
        if sdk_type == "anthropic":
            resp = client.messages.create(
                model=model, max_tokens=1024,
                system=system, messages=messages
            )
            return resp.content[0].text
        else:
            full_messages = [{"role": "system", "content": system}] + messages
            resp = client.chat.completions.create(model=model, messages=full_messages)
            return resp.choices[0].message.content
    except openai.APIConnectionError:
        return "[Error: Cannot reach Ollama. Is 'ollama serve' running?]"
    except Exception as e:
        return f"[Error: {e}]"

# ── CHUNK 3: The conversation loop ──────────────────────────────────────────
# WHAT: Standard multi-turn loop — accumulate messages, call, append reply.
# WHY:  This is identical to M01 Step 5. Nothing changes when you swap providers.
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--provider", choices=["claude","mistral","groq"], default="claude")
    args = parser.parse_args()

    sdk_type, client, model = build_client(args.provider)
    print(f"\nChatting with {args.provider} ({model}). Type 'quit' to exit.\n")

    conversation = []
    while True:
        user_input = input("You: ").strip()
        if user_input.lower() in ("quit", "exit", "q"):
            break
        if not user_input:
            continue
        conversation.append({"role": "user", "content": user_input})
        reply = chat(sdk_type, client, model, conversation)
        conversation.append({"role": "assistant", "content": reply})
        print(f"\n{args.provider.capitalize()}: {reply}\n")

if __name__ == "__main__":
    main()
// provider_agnostic_chat.mjs
// Usage:  node provider_agnostic_chat.mjs --provider claude
//         node provider_agnostic_chat.mjs --provider mistral
//         node provider_agnostic_chat.mjs --provider groq
// npm install @anthropic-ai/sdk openai dotenv

import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import readline from 'readline';
import 'dotenv/config';

// ── CHUNK 1: Provider configuration ─────────────────────────────────────────
const PROVIDERS = {
  claude: { sdk: 'anthropic', model: 'claude-sonnet-4-6' },
  mistral: { sdk: 'openai_compat', baseURL: 'http://localhost:11434/v1', apiKey: 'ollama', model: 'mistral' },
  groq: { sdk: 'openai_compat', baseURL: 'https://api.groq.com/openai/v1', apiKey: process.env.GROQ_API_KEY, model: 'llama3-8b-8192' }
};

function buildClient(providerName) {
  const cfg = PROVIDERS[providerName];
  if (cfg.sdk === 'anthropic') return { type: 'anthropic', client: new Anthropic(), model: cfg.model };
  const client = new OpenAI({ baseURL: cfg.baseURL, apiKey: cfg.apiKey });
  return { type: 'openai_compat', client, model: cfg.model };
}

// ── CHUNK 2: Unified chat function ───────────────────────────────────────────
async function chat({ type, client, model }, messages, system = 'You are a helpful assistant.') {
  try {
    if (type === 'anthropic') {
      const resp = await client.messages.create({ model, max_tokens: 1024, system, messages });
      return resp.content[0].text;
    }
    const resp = await client.chat.completions.create({
      model,
      messages: [{ role: 'system', content: system }, ...messages]
    });
    return resp.choices[0].message.content;
  } catch (err) {
    if (err.code === 'ECONNREFUSED') return '[Error: Cannot reach Ollama. Is "ollama serve" running?]';
    return `[Error: ${err.message}]`;
  }
}

// ── CHUNK 3: Conversation loop ───────────────────────────────────────────────
const providerArg = process.argv.indexOf('--provider');
const providerName = providerArg >= 0 ? process.argv[providerArg + 1] : 'claude';
const provider = buildClient(providerName);
const conversation = [];

console.log(`\nChatting with ${providerName} (${provider.model}). Type 'quit' to exit.\n`);

const rl = readline.createInterface({ input: process.stdin, output: process.stdout });

function ask() {
  rl.question('You: ', async (input) => {
    const text = input.trim();
    if (['quit','exit','q'].includes(text.toLowerCase())) { rl.close(); return; }
    if (!text) { ask(); return; }
    conversation.push({ role: 'user', content: text });
    const reply = await chat(provider, conversation);
    conversation.push({ role: 'assistant', content: reply });
    console.log(`\n${providerName}: ${reply}\n`);
    ask();
  });
}
ask();
What Just Happened? The conversation loop — accumulate messages, call model, append reply, repeat — is identical to M01 Step 5. The only new piece is buildClient(), which isolates all provider-specific initialization into one place. Swap the provider name, change the PROVIDERS dict, and the entire chat app works with a new backend. This pattern scales: your RAG pipeline, your ReAct agent, your multi-tool orchestration — all of them become provider-agnostic if you isolate the model call behind a function like chat().

Hands-On Lab: Your First Open Source Agent

What You'll BuildA CLI chat app that runs against Mistral-7B locally, then compare outputs to Claude on the same prompt
Time Estimate45-60 min (includes model download ~4 GB)
PrerequisitesM01 complete, ~5 GB free disk space. ANTHROPIC_API_KEY optional — only needed in Step 2 to run the Claude side-by-side comparison. Steps 1 and 3 run fully offline.
Files You'll Createollama_hello.py, litellm_swap.py, provider_agnostic_chat.py

Environment Setup

bash — run once
mkdir m28-open-source && cd m28-open-source
python -m venv venv
# macOS/Linux:
source venv/bin/activate
# Windows:
# venv\Scripts\activate

pip install anthropic openai litellm python-dotenv

# Optional: set your Anthropic key only if you want to run the Claude comparison in Step 2.
# Steps 1 and 3 work entirely offline without this key.
# echo "ANTHROPIC_API_KEY=your-key-here" > .env
# Windows (PowerShell): # Set-Content .env "ANTHROPIC_API_KEY=your-key-here"

# Install Ollama (macOS/Linux):
curl -fsSL https://ollama.ai/install.sh | sh
# Windows: download installer from https://ollama.ai then run in terminal:
# ollama pull mistral

# Pull Mistral-7B (~4.1 GB):
ollama pull mistral

# Start Ollama's API server in a separate terminal:
ollama serve

Step 1: Your First Ollama API Call

Create a file called ollama_hello.py. This sends a single message to Mistral-7B and prints the response. It's the equivalent of M01 Step 1, but using a local model instead of Claude.

# ollama_hello.py
import openai

# Ollama runs on localhost:11434 and speaks the OpenAI Chat Completions schema.
# api_key="ollama" is a dummy value — Ollama doesn't authenticate, but the SDK requires the field.
client = openai.OpenAI(
    base_url="http://localhost:11434/v1",
    api_key="ollama"
)

try:
    response = client.chat.completions.create(
        model="mistral",
        messages=[
            {"role": "user", "content": "Explain what a language model is in one sentence."}
        ]
    )
    print("Mistral says:", response.choices[0].message.content)
    print("Tokens used:", response.usage.total_tokens)
except openai.APIConnectionError:
    print("ERROR: Cannot reach Ollama. Make sure 'ollama serve' is running in another terminal.")
except Exception as e:
    print(f"ERROR: {e}")
// ollama_hello.mjs
// npm install openai
import OpenAI from 'openai';

const client = new OpenAI({
  baseURL: 'http://localhost:11434/v1',
  apiKey: 'ollama'
});

try {
  const response = await client.chat.completions.create({
    model: 'mistral',
    messages: [{ role: 'user', content: 'Explain what a language model is in one sentence.' }]
  });
  console.log('Mistral says:', response.choices[0].message.content);
  console.log('Tokens used:', response.usage.total_tokens);
} catch (err) {
  if (err.code === 'ECONNREFUSED') {
    console.error('ERROR: Cannot reach Ollama. Run "ollama serve" in another terminal.');
  } else {
    console.error('ERROR:', err.message);
  }
}
Run it
python ollama_hello.py
Expected output
Mistral says: A language model is a statistical system trained on large amounts of text data to predict and generate human-like text by learning patterns in language.
Tokens used: 42
✅ Checkpoint: You should see a one-sentence explanation from Mistral. The token count will vary slightly each run. If you see ERROR: Cannot reach Ollama, run ollama serve in a separate terminal window and try again.
Troubleshooting Step 1
  • ModuleNotFoundError: No module named 'openai' → run pip install openai
  • APIConnectionError / Connection refused → Ollama isn't serving. Run ollama serve in another terminal.
  • model "mistral" not found → you need to pull it first: ollama pull mistral (wait for download to complete)
  • Very slow first response → Ollama is loading the model into RAM. Subsequent calls will be faster. On CPU it can take 10-30 seconds for the first token.

Step 2: Side-by-Side Comparison with LiteLLM

This step sends the same prompt to both Claude and Mistral-7B and prints both responses with token counts. It uses LiteLLM as the unified interface so the call is identical for both providers.

# litellm_swap.py
import litellm
from dotenv import load_dotenv

load_dotenv()

PROMPT = "What are three real-world use cases for AI agents in healthcare?"

PROVIDERS = [
    ("Claude Sonnet", "claude-sonnet-4-6"),
    ("Mistral 7B (Ollama)", "ollama/mistral"),
]

for label, model in PROVIDERS:
    print(f"\n{'='*50}")
    print(f"  {label}")
    print(f"{'='*50}")
    try:
        resp = litellm.completion(
            model=model,
            messages=[
                {"role": "system", "content": "You are a helpful assistant. Be concise."},
                {"role": "user", "content": PROMPT}
            ],
            max_tokens=300
        )
        print(resp.choices[0].message.content)
        usage = resp.usage
        print(f"\n[Tokens: {usage.total_tokens} total ({usage.prompt_tokens} in / {usage.completion_tokens} out)]")
    except Exception as e:
        print(f"[Error calling {label}: {e}]")
// litellm_swap.mjs
// LiteLLM's Python package is the primary interface; Node.js uses the OpenAI SDK
// pointed at each provider directly — same result, same comparison.
// npm install @anthropic-ai/sdk openai dotenv
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import 'dotenv/config';

const PROMPT = 'What are three real-world use cases for AI agents in healthcare?';
const SYSTEM  = 'You are a helpful assistant. Be concise.';

// ── Claude via Anthropic SDK ─────────────────────────────────────────────────
const claude = new Anthropic();
try {
  const r = await claude.messages.create({
    model: 'claude-sonnet-4-6', max_tokens: 300,
    system: SYSTEM,
    messages: [{ role: 'user', content: PROMPT }]
  });
  console.log('\n==================================================');
  console.log('  Claude Sonnet');
  console.log('==================================================');
  console.log(r.content[0].text);
  console.log(`\n[Tokens: ${r.usage.input_tokens + r.usage.output_tokens} total]`);
} catch (e) { console.error(`[Error calling Claude: ${e.message}]`); }

// ── Mistral via Ollama (OpenAI-compatible) ───────────────────────────────────
const ollama = new OpenAI({ baseURL: 'http://localhost:11434/v1', apiKey: 'ollama' });
try {
  const r = await ollama.chat.completions.create({
    model: 'mistral', max_tokens: 300,
    messages: [
      { role: 'system', content: SYSTEM },
      { role: 'user', content: PROMPT }
    ]
  });
  console.log('\n==================================================');
  console.log('  Mistral 7B (Ollama)');
  console.log('==================================================');
  console.log(r.choices[0].message.content);
  console.log(`\n[Tokens: ${r.usage?.total_tokens ?? 'N/A'} total]`);
} catch (e) {
  if (e.code === 'ECONNREFUSED') console.error('[Error: Ollama not running. Run "ollama serve".]');
  else console.error(`[Error calling Mistral: ${e.message}]`);
}
Run it
python litellm_swap.py        # Python
node litellm_swap.mjs         # Node.js
✅ Checkpoint: You should see two responses — one from Claude, one from Mistral — for the same prompt, with token counts below each. Compare quality: for a factual listing task like this, both should be reasonable. Try a more complex reasoning prompt to see where the gap widens.
Troubleshooting Step 2
  • Claude call fails with AuthenticationError → the ANTHROPIC_API_KEY is missing or invalid. Add it to your .env file, or skip the Claude call — the Mistral (Ollama) call works without it.
  • Mistral call fails with ConnectionError → Ollama needs to be running. In another terminal: ollama serve.
  • litellm.exceptions.APIError: ollama/mistral not found → Make sure you pulled the model: ollama pull mistral.

Step 3: Provider-Agnostic CLI Chat

Save the provider_agnostic_chat.py code from the Code Walkthrough section above as a file, then run it with different providers. This step uses the code you already reviewed — no new code to write.

Run with Mistral (local)
python provider_agnostic_chat.py --provider mistral
Run with Claude (compare)
python provider_agnostic_chat.py --provider claude
✅ Checkpoint: Have a 3-turn conversation with each provider on the same topic. Ask something that requires remembering earlier messages (e.g., "My name is Alex. What is my name?" in the second turn). Both should remember context — the multi-turn logic is provider-agnostic.

Stretch Goal: Try Groq (Cloud-Hosted, Faster)

Groq runs open source models on custom LPULanguage Processing Unit — Groq's custom silicon designed specifically for LLM inference. Unlike a GPU (built for parallel matrix math across many tasks), an LPU is optimized for the sequential token-by-token generation loop, delivering dramatically lower latency per token. hardware. It's often faster than Ollama (which runs on your CPU) and completely free to start. Sign up at console.groq.com, get a free API key, and add it to your .env:

.env (add this line)
GROQ_API_KEY=gsk_your_key_here
bash
python provider_agnostic_chat.py --provider groq

Notice the latency difference vs. Ollama on CPU. Groq's LPU hardware typically delivers 200-500 tokens/second on Llama-3-8B, compared to 5-30 tokens/second on a laptop CPU with Ollama.

Verify Everything Works

bash — run all three
python ollama_hello.py
python litellm_swap.py
python provider_agnostic_chat.py --provider mistral
🎉 Congratulations! All three scripts working means you can call Mistral-7B locally, compare providers side-by-side with LiteLLM, and run a multi-turn chat against any provider with one flag change. The patterns you've learned in this course are not locked to Claude — they work anywhere.

Knowledge Check

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

Q1: Ollama, Groq, Together AI, and LM Studio all implement the same API schema. Which one is it?

AThe Anthropic Messages API
BThe Hugging Face Inference API
CThe OpenAI Chat Completions API
DThe LiteLLM unified API
Correct! The OpenAI Chat Completions API became the de-facto standard for LLM inference. Ollama, Groq, Together AI, and LM Studio all implement this schema — which is why the same OpenAI SDK code can be redirected to any of them by changing just the base_url.
Not quite. The shared standard is the OpenAI Chat Completions API schema. LiteLLM is a wrapper on top of this; Anthropic uses a different schema; Hugging Face has its own endpoint format.

Q2: You switch from Claude to Ollama/Mistral. Your code has response.content[0].text to extract the reply. What needs to change?

ANothing — the response format is identical
BThe response path: use response.choices[0].message.content instead
CYou must switch from Python to JavaScript
DYou need to add max_tokens — it's required by Ollama
Correct! Anthropic uses content[0].text; the OpenAI schema (used by Ollama, Groq, etc.) uses choices[0].message.content. This is the most common bug when first doing a provider swap.
The response schemas are different. Anthropic: response.content[0].text. OpenAI/Ollama: response.choices[0].message.content. Also, max_tokens is required by Anthropic, not Ollama — so it's the opposite of option D.

Q3: Name two valid reasons to prefer an open source model over Claude in a production system.

ABetter reasoning quality and larger context window
BBuilt-in prompt caching and extended thinking
CHooks, sessions, and the Claude Agent SDK
DData privacy (no data leaves your network) and cost reduction on high-volume simple tasks
Correct! Data privacy (running fully offline behind your firewall) and cost reduction on high-volume simple tasks are the two strongest production cases for open source. Reasoning quality, context window, and all the features in options A, B, and C favor Claude.
Those are advantages of Claude, not open source models. The open source advantages are: data privacy (local inference, no third-party API), cost at scale (free to self-host), and fine-tuning on proprietary data.

Q4: Which Claude feature has absolutely no equivalent in any open source inference server today?

AClaude Code hooks and sessions (the Agent SDK platform features)
BMulti-turn conversation history
CFunction calling / tool use
DTemperature and top-p sampling
Correct! Claude Code hooks, sessions, and the Agent SDK are Anthropic platform features — they don't exist for Ollama-backed models. Multi-turn history, function calling, and temperature/top-p are all standard features available in open source models too.
Multi-turn history (B), function calling (C), and temperature/top-p (D) are all available in open source models including Mistral-7B via Ollama. The Claude-specific platform features — hooks, sessions, the Agent SDK — have no open-source equivalent.

Q5: You're building an agent that classifies customer support tickets (simple task, high volume) and then routes complex ones to a human-written response generator (hard task, low volume). What's the best provider strategy?

AUse Claude for everything — consistency is more important than cost
BUse an open source model (or Groq) for classification, Claude for the complex response generation
CUse an open source model for everything to minimize cost
DUse LiteLLM with random provider rotation
Correct! This is the hybrid routing pattern from M22: use a cheap model for the high-volume simple task (classification), and Claude for the low-volume complex task (response generation). You get the cost savings where quality difference is small, and the quality premium where it matters.
The hybrid routing pattern is the right call here. Route cheap, high-volume, simple tasks to open source; reserve Claude for the complex, quality-critical tasks. Using Claude for everything wastes money; using only open source sacrifices quality where it matters. Random rotation (D) is never correct.

Summary

Ollama Local inference, free, zero API key. Install once, ollama pull <model>, then ollama serve.
OpenAI-compat API Most open source servers speak this schema. Point the OpenAI SDK at a different base_url to use any of them.
Response path diff Anthropic: resp.content[0].text · OpenAI schema: resp.choices[0].message.content
System prompt diff Anthropic: top-level system="" field · OpenAI schema: {"role":"system","content":"..."} message
LiteLLM One call, any provider. Model string encodes provider: "claude-sonnet-4-6", "ollama/mistral", "groq/llama3-8b".
What doesn't port Prompt caching, extended thinking, computer use, Claude Code hooks/sessions, Agent SDK.
Hybrid routing Open source for cheap/simple tasks → Claude for complex/quality-critical tasks. Best of both worlds.
📚 What's Next

This is the last module in the curriculum. You've covered 28 modules spanning the full agent lifecycle — from first API call to cert prep to open source deployment. If you haven't already, the next step is M25-M27 for the Claude Certified Architect exam, or pick a capstone project and build something real.

The skills you've built — RAG, ReAct, guardrails, tracing, multi-agent patterns — work with Claude, with Mistral, and with whatever model ships next year. The architecture is yours.