Module 1: Models and Providers
Local Models, Caching and Rate Limiting
Capsule overview
So far you've worked exclusively with models in the cloud: every call to invoke() travels across the internet to OpenAI's, Anthropic's or Google's servers. That works, but it comes with three real problems: it costs money per token, you're subject to rate limits that can block your application, and you depend on an internet connection. In this capsule you'll learn to solve all three.
First you'll explore Ollama to run models locally on your machine — no API keys, no costs, no internet connection. Then you'll look at prompt caching to cut costs when you repeat the same prompt prefixes across multiple calls. Next you'll use InMemoryRateLimiter to keep your calls from getting blocked for exceeding API quotas. And you'll learn to monitor exactly how many tokens each call consumes with usage_metadata.
The capsule closes with a practical section you'll reach for often: a mapping from legacy APIs to modern LangChain APIs. When you run into old tutorials that use LLMChain or AgentExecutor, you'll know exactly what to replace them with in LangChain v1.2+.
Local models with Ollama
Why run models locally?
Every call to a cloud model has a cost. If you're prototyping, experimenting with prompts, or processing sensitive data you can't send to external servers, running a model locally eliminates those three problems in one shot.
Ollama is the simplest way to run language models on your machine. It acts as a local server that exposes a compatible API — and LangChain connects to it exactly the same way it connects to OpenAI or Anthropic.
Install and configure Ollama
# Install: macOS
brew install ollama
# Linux: curl -fsSL https://ollama.ai/install.sh | sh
# Windows: download from https://ollama.ai/download
# Download a model
ollama pull llama3.2 # Lightweight model (~2GB), good for development
ollama pull llama3.1 # More capable model (~5GB), better quality
# Check which models are available
ollama list
# Check that the server is running (it starts automatically)
ollama serve
# Output: Listening on 127.0.0.1:11434
Using local models with LangChain
Once Ollama is running, you use init_chat_model with the ollama: prefix — identical to any other provider:
from langchain.chat_models import init_chat_model
model = init_chat_model("ollama:llama3.2")
response = model.invoke("Explain what a REST API is in one sentence")
print(response.content)
# Expected output: A REST API is an interface that lets applications
# communicate with each other over the HTTP protocol using standard
# operations like GET, POST, PUT and DELETE.
You don't need an API key. You don't need .env. You don't need an internet connection.
Cloud vs Local: when to use each
| Criterion | Cloud (OpenAI, Anthropic) | Local (Ollama) |
|---|---|---|
| Cost per token | ✅ Pay per use | ✅ Free |
| Response quality | ✅ State-of-the-art | ⚠️ Lower (smaller models) |
| Speed | ✅ Fast (dedicated GPUs) | ⚠️ Depends on your hardware |
| Privacy | ❌ Data travels to external servers | ✅ Everything stays on your machine |
| Internet connection | ❌ Required | ✅ Not needed |
| Setup | ✅ Just an API key | ⚠️ Install Ollama + download the model |
| Available models | ✅ GPT-4.1, Claude, Gemini | ⚠️ Llama, Mistral, Phi (open-source) |
Rule of thumb:
- ✅ Use cloud for production, complex tasks, or when quality is critical
- ✅ Use local for development, prototyping, sensitive data, or when you want to iterate without cost
Prompt caching
How it works
When you call a model, the provider processes the entire prompt from scratch every time — including the system prompt. If you send the same 2,000-token system prompt in 100 consecutive calls, you pay to process those 2,000 tokens 100 times.
Prompt caching solves this: the provider detects that a prefix of the prompt was already processed recently and reuses the cached result. It only processes (and charges for) the new tokens.
Without caching:
Call 1: [System prompt: 2000 tokens] + [User: 50 tokens] → Charges 2050 tokens
Call 2: [System prompt: 2000 tokens] + [User: 30 tokens] → Charges 2030 tokens
Call 3: [System prompt: 2000 tokens] + [User: 45 tokens] → Charges 2045 tokens
Total processed: 6,125 tokens
With caching:
Call 1: [System prompt: 2000 tokens] + [User: 50 tokens] → Charges 2050 tokens (cache miss)
Call 2: [System prompt: CACHED] + [User: 30 tokens] → Charges ~530 tokens (cache hit)
Call 3: [System prompt: CACHED] + [User: 45 tokens] → Charges ~545 tokens (cache hit)
Total processed: ~3,125 tokens (~50% savings)
Support by provider
| Provider | Status | Notes |
|---|---|---|
| Anthropic | ✅ Beta available | Kicks in automatically with long system prompts (>1024 tokens) |
| OpenAI | ✅ Available | Automatic on calls to the same endpoint with repeated prefixes |
| ⚠️ Partial | Explicit caching support via the Context Caching API | |
| Ollama | ❌ Not applicable | Local models, there's no cost per token |
When it helps
Caching pays off when:
- ✅ You use the same long system prompt across multiple calls
- ✅ You process many documents with the same analysis instructions
- ✅ You run an agent that keeps a constant base prompt between iterations
It's not worth it when:
- ❌ Every call has a completely different prompt
- ❌ Your system prompt is short (< 500 tokens)
- ❌ You make few calls (the cache expires quickly)
Note: Prompt caching is a provider-side optimization that works transparently. You don't need to change your code — it simply happens when your usage pattern allows it. What matters is understanding when it benefits you, so you can design your applications to take advantage of it. We'll go deeper into prompt caching with practical examples in Module 12 (LangSmith and Production).
Rate limiting with InMemoryRateLimiter
The problem: rate limit errors
Every API provider has rate limits. If you send too many requests per minute, you get a 429 Too Many Requests error and your application stops.
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4.1-mini")
# This can fail with a rate limit error if you fire off many calls quickly
for i in range(50):
response = model.invoke(f"Question number {i}")
print(f"{i}: {response.content[:30]}...")
# Possible error:
# openai.RateLimitError: Error code: 429 - Rate limit reached
You can use max_retries to retry (capsule 03), but that doesn't fix the root problem: you're sending requests faster than the API allows.
The solution: InMemoryRateLimiter
InMemoryRateLimiter controls the pace of your calls before they leave your application. Instead of firing 50 instant requests and hoping the API accepts them, it spaces them out automatically.
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
rate_limiter = InMemoryRateLimiter(requests_per_second=1)
model = init_chat_model(
"openai:gpt-4.1-mini",
rate_limiter=rate_limiter
)
response = model.invoke("What is rate limiting?")
print(response.content)
# Expected output: Rate limiting is a technique that controls how many
# requests a client can make to a service within a given period
# of time.
You can tune the parameters: requests_per_second (maximum pace), check_every_n_seconds (how often it checks, default 0.1), and max_bucket_size (allowed burst, default 10).
Practical example: batch processing with rate limiting
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
import time
rate_limiter = InMemoryRateLimiter(requests_per_second=2)
model = init_chat_model(
"openai:gpt-4.1-mini",
rate_limiter=rate_limiter
)
questions = [
"What is Python?",
"What is JavaScript?",
"What is Rust?",
"What is Go?",
"What is TypeScript?",
]
start = time.time()
for i, question in enumerate(questions):
response = model.invoke(question)
elapsed = time.time() - start
print(f"[{elapsed:.1f}s] {question} → {response.content[:50]}...")
# Expected output (notice the spacing in time):
# [0.5s] What is Python? → Python is a programming language...
# [1.1s] What is JavaScript? → JavaScript is a programming langu...
# [1.6s] What is Rust? → Rust is a systems programming language...
# [2.2s] What is Go? → Go is a programming language created by...
# [2.7s] What is TypeScript? → TypeScript is a typed superset...
The calls get spaced out automatically to ~0.5s between each one (2 requests/second).
Token usage tracking
Monitoring token consumption
Every model response includes metadata about how many tokens were consumed. You access it through response.usage_metadata:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4.1-mini")
response = model.invoke("Hi, how are you?")
print(response.content)
print(response.usage_metadata)
# Expected output:
# Hi there! I'm doing well, thanks for asking. How can I help you?
# {'input_tokens': 8, 'output_tokens': 15, 'total_tokens': 23}
Calculating costs
Every provider has per-token pricing. You can compute the real cost of each call:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
PRICING = {
"gpt-4.1-mini": {"input": 0.40 / 1_000_000, "output": 1.60 / 1_000_000},
"gpt-4.1": {"input": 2.00 / 1_000_000, "output": 8.00 / 1_000_000},
"gpt-4.1-nano": {"input": 0.10 / 1_000_000, "output": 0.40 / 1_000_000},
}
def invoke_with_cost(model_name: str, question: str) -> dict:
"""Invokes the model and computes the cost of the call."""
model = init_chat_model(f"openai:{model_name}")
response = model.invoke(question)
tokens = response.usage_metadata
pricing = PRICING[model_name]
cost_input = tokens["input_tokens"] * pricing["input"]
cost_output = tokens["output_tokens"] * pricing["output"]
total_cost = cost_input + cost_output
return {
"content": response.content,
"input_tokens": tokens["input_tokens"],
"output_tokens": tokens["output_tokens"],
"cost_usd": total_cost,
}
result = invoke_with_cost("gpt-4.1-mini", "What is Docker in one sentence?")
print(f"Response: {result['content']}")
print(f"Tokens: {result['input_tokens']} input + {result['output_tokens']} output")
print(f"Cost: ${result['cost_usd']:.6f} USD")
# Expected output:
# Response: Docker is a platform that lets you package applications
# into isolated containers so they run consistently in
# any environment.
# Tokens: 12 input + 25 output
# Cost: $0.000045 USD
In Exercise 3 of this capsule you'll build a CostTracker class that accumulates tokens and costs across multiple calls — an essential pattern for production.
Mapping: Legacy API → Modern API
Why you need this table
LangChain went through a major transformation with version 1.0+ (October 2025). Many of the APIs you'll find in tutorials, on Stack Overflow, and in old documentation are deprecated or replaced. If you copy code from a 2024 tutorial, it probably won't work — or it'll work with deprecation warnings.
This table gives you the modern equivalent of every legacy API. Use it as a reference every time you run into old code.
Equivalence table
| Legacy API (pre-v1.0) | Modern API (v1.2+) | Notes |
|---|---|---|
from langchain.llms import OpenAI | init_chat_model("openai:gpt-4.1-mini") | OpenAI was for completion models. Now everything uses chat models |
LLMChain(llm=llm, prompt=prompt) | model.invoke(prompt) | LLMChain added needless complexity. invoke() does the same thing |
SequentialChain([chain1, chain2]) | chain1 | chain2 (pipe operator) | The pipe operator composes runnables naturally |
AgentExecutor(agent, tools) | create_agent(model, tools) | create_agent is simpler and uses LangGraph under the hood |
ConversationChain(llm, memory) | create_agent(model, tools) + state | Agents with state management replace conversation chains |
OutputParser | model.with_structured_output(Schema) | Structured output is native — you don't need to parse text by hand |
from langchain.chat_models import ChatOpenAI | init_chat_model("openai:...") | init_chat_model is universal, you don't need one class per provider |
CallbackHandler | Still valid, but middleware is preferred | Middleware (@before_model, @after_model) is more powerful (Module 4) |
ChatPromptTemplate.from_messages | Still valid | Prompt templates didn't change |
Example: LLMChain → model.invoke()
# ❌ Legacy (pre-v1.0) — DON'T use this
# from langchain.llms import OpenAI
# from langchain.chains import LLMChain
# from langchain.prompts import PromptTemplate
#
# llm = OpenAI(temperature=0.7)
# prompt = PromptTemplate(
# input_variables=["topic"],
# template="Explain {topic} in one sentence."
# )
# chain = LLMChain(llm=llm, prompt=prompt)
# result = chain.run(topic="Docker")
# ✅ Modern (v1.2+) — Use this
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4.1-mini", temperature=0.7)
response = model.invoke("Explain Docker in one sentence.")
print(response.content)
# Expected output: Docker is a container platform that packages
# applications with all their dependencies so they run
# consistently in any environment.
Example: OutputParser → with_structured_output
# ❌ Legacy — manual OutputParser
# from langchain.output_parsers import PydanticOutputParser
# parser = PydanticOutputParser(pydantic_object=Movie)
# prompt = prompt_template.format(format_instructions=parser.get_format_instructions())
# ✅ Modern — native structured output
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
class Movie(BaseModel):
title: str = Field(description="The movie's title")
year: int = Field(description="Release year")
genre: str = Field(description="Main genre")
model = init_chat_model("openai:gpt-4.1-mini")
structured_model = model.with_structured_output(Movie)
result = structured_model.invoke("Give me information about Inception")
print(f"{result.title} ({result.year}) - {result.genre}")
# Expected output: Inception (2010) - Science Fiction
Connection with the project
In this module's project (Capsule 08), you'll build a multi-provider chat with automatic fallback. The concepts from this capsule apply directly:
- Ollama as the last-resort fallback: if OpenAI and Anthropic fail (API down, rate limit), the system can fall back to a local model with Ollama. No cost, no dependency on the internet.
- Rate limiting: when the system gets a lot of requests,
InMemoryRateLimiterkeeps it from getting blocked for exceeding the cloud providers' quotas. - Token tracking: the project reports metadata for each response, including tokens consumed and the provider used — computed with
usage_metadata. - Modern APIs: the whole project uses
init_chat_model,invoke(), andwith_structured_output— the modern APIs you learned in this module.
Troubleshooting
Problem 1: "Connection refused" when using Ollama
ConnectionError: Connection refused - connect(2) for "127.0.0.1" port 11434
Cause: The Ollama server isn't running.
Fix:
# Start the server
ollama serve
# Check that it responds
curl http://localhost:11434
# It should return: "Ollama is running"
Problem 2: Ollama model not found
Error: model "llama3.1" not found, try pulling it first
Cause: You didn't download the model before using it.
Fix:
# Download the model first
ollama pull llama3.1
# Check which models are available
ollama list
Problem 3: Rate limit error even though you're using InMemoryRateLimiter
Cause: Your requests_per_second is higher than the real limit of your API tier. Fix: Lower the value (e.g. requests_per_second=0.5) until the errors go away.
Problem 4: usage_metadata is None
Cause: Some providers don't return usage metadata (Ollama, local models). Fix: Check before you access it: if response.usage_metadata:.
Problem 5: Slow responses with Ollama
Cause: Not enough hardware for the model. Fix: Use a smaller model (ollama:llama3.2 instead of llama3.1), check the GPU with ollama ps, and close apps that are eating memory.
Exercises
Exercise 1: Your first local model (Basic)
Install Ollama, download the llama3.2 model, and make a call with init_chat_model. Print the response and confirm that no API key was needed.
See solution
from langchain.chat_models import init_chat_model
model = init_chat_model("ollama:llama3.2")
response = model.invoke("What's the difference between a list and a tuple in Python?")
print(response.content)
print(f"\nResponse type: {type(response)}")
print(f"Usage metadata: {response.usage_metadata}")
# Expected output:
# The main difference is that lists are mutable (you can modify
# their elements) while tuples are immutable (once created,
# you can't change their elements). Lists use square brackets [] and
# tuples use parentheses ().
#
# Response type: <class 'langchain_core.messages.ai.AIMessage'>
# Usage metadata: None (or a dict with tokens if the model supports it)
Explanation: init_chat_model("ollama:llama3.2") connects to the local Ollama server. You don't need load_dotenv() or API keys. The interface is identical to any cloud provider.
Exercise 2: Rate limiter for batch processing (Basic)
Create a model with a rate limiter of 1 request/second and process a list of 5 questions. Measure the total time to confirm that the rate limiting is working.
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
import time
rate_limiter = InMemoryRateLimiter(requests_per_second=1)
model = init_chat_model("openai:gpt-4.1-mini", rate_limiter=rate_limiter)
questions = [
"What is HTML?",
"What is CSS?",
"What is JavaScript?",
"What is React?",
"What is Node.js?",
]
start = time.time()
for question in questions:
response = model.invoke(question)
elapsed = time.time() - start
print(f"[{elapsed:.1f}s] {question} → {response.content[:40]}...")
total = time.time() - start
print(f"\nTotal time: {total:.1f}s (expected: ~5s at 1 req/s)")
# Expected output:
# [0.8s] What is HTML? → HTML is the standard markup langua...
# [1.9s] What is CSS? → CSS is a style sheet language...
# [2.9s] What is JavaScript? → JavaScript is a programming lan...
# [4.0s] What is React? → React is a JavaScript library...
# [5.1s] What is Node.js? → Node.js is a runtime environment...
#
# Total time: 5.1s (expected: ~5s at 1 req/s)
Explanation: With requests_per_second=1, the calls get spaced out to ~1 second apart. The total time reflects the rate limiting at work. Without the rate limiter, all 5 calls would go out almost simultaneously.
Exercise 3: Per-session cost tracker (Medium)
Create a CostTracker class that accumulates the tokens and costs of multiple calls. It should have track(response) and summary() methods.
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
class CostTracker:
PRICING = {
"gpt-4.1-mini": {"input": 0.40 / 1_000_000, "output": 1.60 / 1_000_000},
"gpt-4.1": {"input": 2.00 / 1_000_000, "output": 8.00 / 1_000_000},
}
def __init__(self, model_name: str = "gpt-4.1-mini"):
self.model_name = model_name
self.total_input = 0
self.total_output = 0
self.call_count = 0
def track(self, response) -> None:
"""Records the tokens from a response."""
if response.usage_metadata:
self.total_input += response.usage_metadata["input_tokens"]
self.total_output += response.usage_metadata["output_tokens"]
self.call_count += 1
def summary(self) -> dict:
"""Returns a summary of usage and cost."""
pricing = self.PRICING.get(self.model_name, {"input": 0, "output": 0})
cost = (
self.total_input * pricing["input"]
+ self.total_output * pricing["output"]
)
return {
"calls": self.call_count,
"input_tokens": self.total_input,
"output_tokens": self.total_output,
"total_tokens": self.total_input + self.total_output,
"cost_usd": cost,
}
tracker = CostTracker("gpt-4.1-mini")
model = init_chat_model("openai:gpt-4.1-mini")
questions = ["What is Git?", "What is Docker?", "What is Kubernetes?"]
for question in questions:
response = model.invoke(question)
tracker.track(response)
print(f"{question} → {response.usage_metadata['total_tokens']} tokens")
s = tracker.summary()
print(f"\n--- Summary ---")
print(f"Calls: {s['calls']}")
print(f"Total tokens: {s['total_tokens']}")
print(f"Estimated cost: ${s['cost_usd']:.6f} USD")
# Expected output:
# What is Git? → 48 tokens
# What is Docker? → 52 tokens
# What is Kubernetes? → 61 tokens
#
# --- Summary ---
# Calls: 3
# Total tokens: 161
# Estimated cost: $0.000212 USD
Explanation: The class encapsulates the tracking logic. In production, you could persist this data in a database to analyze costs per user, per feature, or per time period.
Exercise 4: Migrate legacy code to the modern API (Medium)
Given the following legacy code, rewrite it using the modern LangChain v1.2+ APIs.
# Legacy code to migrate:
# from langchain.llms import OpenAI
# from langchain.chains import LLMChain
# from langchain.prompts import PromptTemplate
# from langchain.output_parsers import PydanticOutputParser
#
# llm = OpenAI(temperature=0.5)
# parser = PydanticOutputParser(pydantic_object=Recipe)
# prompt = PromptTemplate(
# template="Give me a recipe for {dish}. {format_instructions}",
# input_variables=["dish"],
# partial_variables={"format_instructions": parser.get_format_instructions()}
# )
# chain = LLMChain(llm=llm, prompt=prompt)
# result = chain.run(dish="tacos")
# recipe = parser.parse(result)
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
class Recipe(BaseModel):
name: str = Field(description="Name of the recipe")
ingredients: list[str] = Field(description="List of ingredients")
steps: list[str] = Field(description="Preparation steps")
prep_time_minutes: int = Field(description="Preparation time in minutes")
model = init_chat_model("openai:gpt-4.1-mini", temperature=0.5)
structured_model = model.with_structured_output(Recipe)
recipe = structured_model.invoke("Give me a recipe for tacos")
print(f"Recipe: {recipe.name}")
print(f"Time: {recipe.prep_time_minutes} minutes")
print(f"Ingredients: {', '.join(recipe.ingredients)}")
for i, step in enumerate(recipe.steps, 1):
print(f" {i}. {step}")
# Expected output:
# Recipe: Carne asada tacos
# Time: 30 minutes
# Ingredients: corn tortillas, beef, onion, cilantro, lime, salt
# 1. Marinate the beef with salt and lime for 15 minutes
# 2. Grill the beef over high heat for 3-4 minutes per side
# 3. Finely chop the onion and cilantro
# 4. Cut the beef into small pieces
# 5. Warm the tortillas
# 6. Serve the beef in the tortillas with onion, cilantro and lime
Explanation: The legacy code used 4 imports, a manual parser, format instructions injected into the prompt, and a chain. The modern code uses 2 imports, with_structured_output which handles all the parsing automatically, and a direct invoke(). The result is typed (Pydantic), not text you have to parse.
Exercise 5: Robust system with rate limiting and tracking (Hard)
Create a robust_batch_process function that takes a list of questions and processes them with: rate limiting (2 req/s), token tracking, and error handling. It should return a summary with the responses, total tokens, and any errors it hit.
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
import time
def robust_batch_process(
questions: list[str],
model_name: str = "openai:gpt-4.1-mini",
requests_per_second: float = 2,
) -> dict:
"""Processes questions in batch with rate limiting, tracking and error handling."""
rate_limiter = InMemoryRateLimiter(requests_per_second=requests_per_second)
model = init_chat_model(model_name, rate_limiter=rate_limiter)
results = []
errors = []
total_input = 0
total_output = 0
start = time.time()
for i, question in enumerate(questions):
try:
response = model.invoke(question)
tokens = response.usage_metadata or {}
total_input += tokens.get("input_tokens", 0)
total_output += tokens.get("output_tokens", 0)
results.append({
"question": question,
"answer": response.content[:100],
"tokens": tokens.get("total_tokens", 0),
})
except Exception as e:
errors.append({
"question": question,
"error": str(e),
})
elapsed = time.time() - start
status = "OK" if not errors or errors[-1]["question"] != question else "ERROR"
print(f"[{elapsed:.1f}s] ({i+1}/{len(questions)}) {status}: {question[:40]}")
return {
"results": results,
"errors": errors,
"total_input_tokens": total_input,
"total_output_tokens": total_output,
"total_tokens": total_input + total_output,
"elapsed_seconds": time.time() - start,
"success_rate": len(results) / len(questions) * 100,
}
questions = [
"What is Python?",
"What is FastAPI?",
"What is SQLAlchemy?",
"What is Redis?",
"What is PostgreSQL?",
"What is Docker Compose?",
]
summary = robust_batch_process(questions)
print(f"\n--- Summary ---")
print(f"Successful: {len(summary['results'])}/{len(questions)}")
print(f"Errors: {len(summary['errors'])}")
print(f"Total tokens: {summary['total_tokens']}")
print(f"Time: {summary['elapsed_seconds']:.1f}s")
print(f"Success rate: {summary['success_rate']:.0f}%")
# Expected output:
# [0.6s] (1/6) OK: What is Python?
# [1.2s] (2/6) OK: What is FastAPI?
# [1.7s] (3/6) OK: What is SQLAlchemy?
# [2.3s] (4/6) OK: What is Redis?
# [2.8s] (5/6) OK: What is PostgreSQL?
# [3.4s] (6/6) OK: What is Docker Compose?
#
# --- Summary ---
# Successful: 6/6
# Errors: 0
# Total tokens: 312
# Time: 3.4s
# Success rate: 100%
Explanation: This function combines three production patterns: rate limiting so you don't blow past quotas, tracking to monitor costs, and try/except so an error on one question doesn't stop the whole batch. The final summary gives you full visibility into the operation.
Summary
In this capsule you learned:
- Ollama lets you run models locally — no API keys, no costs, no internet
init_chat_model("ollama:llama3.2")is used exactly like any cloud provider- Prompt caching cuts costs when you repeat the same prompt prefix across multiple calls
- InMemoryRateLimiter controls the pace of your calls to avoid
429errors response.usage_metadatareports the tokens consumed (input, output, total) per call- The legacy APIs (
LLMChain,AgentExecutor,OutputParser) have modern equivalents in LangChain v1.2+ invoke()replaces chains,with_structured_outputreplaces parsers,create_agentreplacesAgentExecutor- In production: always configure rate limiting and token tracking
Next capsule: Project — you'll build a multi-provider chat with automatic fallback that pulls together everything you learned in this module: init_chat_model, streaming, structured output, rate limiting, and fallback to a local Ollama model.
Additional resources
- Ollama Official Site - Installation, available models and documentation
- LangChain + Ollama Integration - Official integration guide for Ollama
- Anthropic Prompt Caching - Caching documentation for Anthropic
- OpenAI Rate Limits - Limits per tier and strategies for handling them
- InMemoryRateLimiter API Reference - Reference for the rate limiter class
- LangChain Migration Guide - Official guide for migrating chains to LCEL
- OpenAI Pricing - Current per-model prices for calculating costs
- Token Usage Tracking - How-to for tracking tokens in LangChain
Module 1 — LangChain & LangGraph: From Chains to Agents