Setup guide · Models
OpenRouter or a local model?
Choose between any model through your own OpenRouter key and a free local Ollama model, and configure exercise code so the same file runs on either.
Exercises tagged API run code that calls a model directly: the Claude Agent SDK, the Anthropic or OpenAI SDK, or an embeddings endpoint. Your Claude subscription doesn't cover direct API calls, so you choose one of two backends: any model through your own OpenRouter key, or a free local model through Ollama. Every answer reads its configuration from environment variables, so you switch by editing .env, never the code.
Exercises that only use Claude Code or claude -p aren't tagged API. They run on your subscription and don't need anything on this page.
Before you start
You need Python with uv or Node.js and TypeScript, plus one of the two backends set up.
1. Compare the options
| OpenRouter | Local model (Ollama) | |
|---|---|---|
| Quality | Any hosted model, including Claude: reliable tool use, long context, strict formats | Much weaker, especially at multi-step tool use, long inputs and exact formats |
| Cost | Per token from prepaid credits. Haiku 5.5 $0.10 and Sonnet 5.5 $2 per million input tokens, open-weight models far less; most exercises cost cents. Cap it with a credit limit on the key | Free after the download; you pay in electricity and fan noise |
| Privacy | Prompts go to OpenRouter and the chosen provider. OpenRouter stores none unless you opt in; ZDR routing limits providers to ones that don't store them | Nothing leaves your machine |
| Setup | 5 minutes: account, credits, key | 15 to 30 minutes plus multi-GB downloads; needs 16 GB+ RAM to be useful |
| Limits | Your balance and key limit; :free models are capped at 20 requests a minute and 50 a day (1,000 after buying 10 credits). Agent SDK compatibility is guaranteed only with Anthropic models | No tool_choice, token counting, caching, batches, citations or PDF blocks; context is 4K by default; speed depends on your hardware |
A good default: build and debug on a local model or a cheap OpenRouter model (anthropic/claude-haiku-5.5, deepseek/deepseek-v4-flash), then do the final run whose numbers go in your deliverable on a strong model such as anthropic/claude-sonnet-5.5. If an exercise needs tool_choice or prompt caching, use an Anthropic model through OpenRouter.
2. Use the shared configuration pattern
Every answer project reads the same variables from a .env file in its folder, and ships a .env.example with both blocks: OpenRouter first, Ollama commented out. Copy it to .env, keep one block uncommented, and make sure .env is in .gitignore.
Claude Agent SDK and Anthropic SDK code
OpenRouter:
ANTHROPIC_BASE_URL=https://openrouter.ai/api
ANTHROPIC_AUTH_TOKEN=sk-or-...
ANTHROPIC_API_KEY=
MODEL=anthropic/claude-sonnet-5.5Local model:
ANTHROPIC_BASE_URL=http://localhost:11434
ANTHROPIC_AUTH_TOKEN=ollama
ANTHROPIC_API_KEY=
MODEL=qwen3.5ANTHROPIC_API_KEY stays empty in both, so nothing falls back to Anthropic directly. MODEL is any OpenRouter slug; tool use is most reliable on anthropic/… models.
Everything else: OpenAI SDK code
Plain model calls, structured output and embeddings use the OpenAI SDK, which every provider speaks.
OpenRouter:
OPENAI_BASE_URL=https://openrouter.ai/api/v1
OPENAI_API_KEY=sk-or-...
MODEL=anthropic/claude-haiku-5.5
EMBEDDINGS_MODEL=openai/text-embedding-3-smallLocal model:
OPENAI_BASE_URL=http://localhost:11434/v1
OPENAI_API_KEY=ollama
MODEL=qwen3.5
EMBEDDINGS_MODEL=embeddinggemmaEmbeddings go to the same endpoint as chat: OpenRouter's /api/v1/embeddings, or Ollama's OpenAI-compatible /v1/embeddings.
| Variable | Read by |
|---|---|
ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_API_KEY | Anthropic SDKs and the Claude Agent SDK, on their own |
OPENAI_BASE_URL, OPENAI_API_KEY | The OpenAI SDK, on its own |
MODEL, EMBEDDINGS_MODEL | Answer code, passed as the model option |
3. Write code that works either way
The clients are created with no arguments: the SDKs read the base URL and key from the environment, so the same file runs on OpenRouter or Ollama.
Python
uv add openaiimport os
from openai import OpenAI
client = OpenAI() # reads OPENAI_BASE_URL and OPENAI_API_KEY
MODEL = os.environ["MODEL"]
EMBEDDINGS_MODEL = os.environ["EMBEDDINGS_MODEL"]
def ask(prompt: str) -> str:
response = client.chat.completions.create(
model=MODEL,
max_tokens=1000,
messages=[{"role": "user", "content": prompt}],
)
return response.choices[0].message.content or ""
def embed(texts: list[str]) -> list[list[float]]:
response = client.embeddings.create(model=EMBEDDINGS_MODEL, input=texts)
return [item.embedding for item in response.data]
if __name__ == "__main__":
print(MODEL, "->", ask("Reply with the single word: ready"))
print("embedding dimensions:", len(embed(["hello"])[0]))uv run --env-file .env model.pyTypeScript
pnpm add openaiimport OpenAI from "openai";
const client = new OpenAI(); // reads OPENAI_BASE_URL and OPENAI_API_KEY
const MODEL = process.env.MODEL!;
const EMBEDDINGS_MODEL = process.env.EMBEDDINGS_MODEL!;
export async function ask(prompt: string): Promise<string> {
const response = await client.chat.completions.create({
model: MODEL,
max_tokens: 1000,
messages: [{ role: "user", content: prompt }],
});
return response.choices[0].message.content ?? "";
}
export async function embed(texts: string[]): Promise<number[][]> {
const response = await client.embeddings.create({ model: EMBEDDINGS_MODEL, input: texts });
return response.data.map((item) => item.embedding);
}
console.log(MODEL, "->", await ask("Reply with the single word: ready"));
console.log("embedding dimensions:", (await embed(["hello"]))[0].length);node --env-file-if-exists=.env model.tsClaude Agent SDK
Agent SDK code follows the same rule: no credentials in code, and MODEL passed as the model option.
import { query } from "@anthropic-ai/claude-agent-sdk";
// ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY come from .env
for await (const message of query({
prompt: "List the files in this folder.",
options: { model: process.env.MODEL, allowedTools: ["Bash"] },
})) {
if (message.type === "result") console.log(message.subtype);
}The Python SDK (claude-agent-sdk) takes ClaudeAgentOptions(model=os.environ["MODEL"]) the same way.
4. Know what breaks on small local models
Answers are written to keep working on a local model, and each answer's Run it section notes any feature that's missing. These are the usual problems and how answers handle them:
- Tool use is unreliable. Small models skip a needed tool, call one with malformed arguments, or loop. Without
tool_choice, the model can't be forced to call a tool. Answers validate tool inputs against the schema, return the error to the model as the tool result, and cap the loop at a fixed number of turns. - Structured output drifts. Ollama's Anthropic endpoint has no JSON-schema output setting, and while its OpenAI endpoint accepts
response_format, small models still produce invalid output. Answers parse and validate the JSON (pydantic or zod), and retry once with the validation error before failing loudly. - Long context is short. Ollama often allocates only 4K tokens of context, and anything beyond that is silently dropped. Raise it with
OLLAMA_CONTEXT_LENGTH=64000(details), or use a smaller input. - No token counting. Neither Ollama nor OpenRouter documents Anthropic's
count_tokensendpoint, so answers use theusagenumbers in responses (approximate on Ollama), or estimate characters ÷ 4. - Results differ. Accuracy numbers from a local or cheap model aren't comparable with Claude's. Say which model you used in your deliverable.
Check it works
With either OpenAI-SDK .env block in place, run the Python or TypeScript example. Expected output with a local model:
qwen3.5 -> ready
embedding dimensions: <a number such as 768 or 1024>With OpenRouter, the first line starts with your slug, such as anthropic/claude-haiku-5.5. The dimension count depends on the embedding model; any positive number means embeddings work.
Troubleshooting
An error that OPENAI_API_KEY is missing, or Could not resolve authentication method. No variables reached the process. Check that you passed --env-file, and that .env sits in the folder you ran the command from.
Connection error to localhost:11434. Ollama isn't running, or the base URL still points at Ollama while you meant to use OpenRouter. Start Ollama, or switch the block in .env.
The model isn't found. MODEL names an OpenRouter slug while the base URL points at Ollama, or the other way round. Ollama names must match ollama list; OpenRouter slugs look like anthropic/claude-sonnet-5.5.
claude -p started spending OpenRouter credits, or answering from a local model. The variables leaked into your shell. Run unset ANTHROPIC_API_KEY ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN and remove them from your shell profile.
Going further
Sources
- Anthropic Agent SDKDocs · OpenRouterThe environment variables that point the Claude Agent SDK at OpenRouter.
- OpenAI SDKDocs · OpenRouterUsing the OpenAI SDK against `https://openrouter.ai/api/v1`.
- Anthropic compatibilityDocs · OllamaWhat the local Messages API supports, and the unsupported features listed below.
- OpenAI compatibilityDocs · OllamaThe `/v1` chat and embeddings endpoints that OpenAI-SDK code uses locally.
- Context lengthDocs · OllamaWhy a local model's default context is often only 4K tokens, and how to raise it.
Exercises that use this
- Run the same task through the Agent SDKAgent Foundations · What is an agent?
- Same report through the Agent SDKAgent Foundations · Structured outputs and grounding
- Build and test a session store adapterInfrastructure for Agents · State, sessions and checkpoints
- Run the title-review agent on the SDKBuilding Agents with Claude and eve · The Claude Agent SDK
- Build and break a contract-pack reader in eveBuilding Agents with Claude and eve · Agents as files with eve
- Add an open-weight model to the bake-offLLMs for Engineers · Choosing and comparing models
- Log rate-limit headers and cache usageScaling AI Systems · Rate limits and quotas
- A deferrable-work batch pipeline against a mockScaling AI Systems · Backpressure, load shedding and batching