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
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:
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.
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.
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.
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 |
How much RAM does each model need? Click a model to see its requirement.
The OpenAI-Compatible API: The Universal Adapter
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.
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.
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.
openai Python/JS package pointed at a different base_url. One dependency, works with any OpenAI-compatible server. Best for simple use cases.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.
Installation
curl -fsSL https://ollama.ai/install.sh | sh
# 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
# 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
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 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."}]
}'
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}`);
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.
"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.
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 |
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();
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
ollama_hello.py, litellm_swap.py, provider_agnostic_chat.pyEnvironment Setup
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);
}
}
python ollama_hello.py
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
ERROR: Cannot reach Ollama, run ollama serve in a separate terminal window and try again.
Troubleshooting Step 1
ModuleNotFoundError: No module named 'openai'→ runpip install openaiAPIConnectionError / Connection refused→ Ollama isn't serving. Runollama servein 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}]`);
}
python litellm_swap.py # Python
node litellm_swap.mjs # Node.js
Troubleshooting Step 2
- Claude call fails with AuthenticationError → the
ANTHROPIC_API_KEYis missing or invalid. Add it to your.envfile, 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.
python provider_agnostic_chat.py --provider mistral
python provider_agnostic_chat.py --provider claude
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:
GROQ_API_KEY=gsk_your_key_here
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
python ollama_hello.py
python litellm_swap.py
python provider_agnostic_chat.py --provider mistral
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?
base_url.Q2: You switch from Claude to Ollama/Mistral. Your code has response.content[0].text to extract the reply. What needs to change?
response.choices[0].message.content insteadmax_tokens — it's required by Ollamacontent[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.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.
Q4: Which Claude feature has absolutely no equivalent in any open source inference server today?
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?
Summary
ollama pull <model>, then ollama serve.
base_url to use any of them.
resp.content[0].text · OpenAI schema: resp.choices[0].message.content
system="" field · OpenAI schema: {"role":"system","content":"..."} message
"claude-sonnet-4-6", "ollama/mistral", "groq/llama3-8b".
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.