Module 1: Models and Providers
Multimodal and Reasoning
Capsule overview
Up to now, every example you've seen sends text to the model and gets text back. But modern models can do much more: process images, interpret diagrams, analyze screenshots, and in some cases work with audio and video. All of it without switching frameworks — LangChain uses the same message system you already know, just with a richer content format.
The second half of this capsule covers reasoning models: models that "think" internally before giving a final answer. Instead of generating text token by token from left to right, these models spend tokens reasoning about the problem, weighing alternatives and checking their own logic. The result is more accurate answers on tasks that require math, multi-step analysis or complex logic.
Both capabilities — multimodal and reasoning — plug straight into the flow you already know with init_chat_model and invoke. In the module's project, you'll be able to use multimodal to analyze images inside the chat, and reasoning for questions that need more rigorous answers.
Multimodal content: beyond text
Multimodal models accept images (and other media) as part of the input. In LangChain, this is done through content blocks: instead of passing a plain string as the content, you pass a list of blocks where each block can be text or an image.
Sending images to the model
Up to now you've used HumanMessage with a string:
from langchain_core.messages import HumanMessage
message = HumanMessage(content="What is Python?")
To send images, content goes from being a string to being a list of dictionaries:
message = HumanMessage(
content=[
{"type": "text", "text": "What do you see in this image?"},
{"type": "image_url", "image_url": {"url": "https://..."}},
]
)
Each block has a type: "text" for regular text, "image_url" for images (by URL or base64). You can combine as many blocks as you need in a single message.
Image from a URL
The most direct way to send an image is with a public URL:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
model = init_chat_model("openai:gpt-4.1")
message = HumanMessage(
content=[
{"type": "text", "text": "Describe this image in 2-3 sentences."},
{
"type": "image_url",
"image_url": {
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"
},
},
]
)
response = model.invoke([message])
print(response.content)
# Expected output: "The image shows a domestic tabby cat..."
Key points:
invoketakes a list of messages, not a single message- The URL has to be publicly reachable — the provider downloads it internally
- Use a model with vision support (GPT-4.1, Claude Sonnet 4, Gemini 2.0 Flash)
Image from base64
For local or private images with no public URL, convert them to base64:
from dotenv import load_dotenv
load_dotenv()
import base64
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
model = init_chat_model("openai:gpt-4.1")
def encode_image(path: str) -> str:
"""Reads a local image and returns its base64 representation."""
with open(path, "rb") as f:
return base64.standard_b64encode(f.read()).decode("utf-8")
image_data = encode_image("photo.jpg")
message = HumanMessage(
content=[
{"type": "text", "text": "What object appears in this photo?"},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{image_data}"
},
},
]
)
response = model.invoke([message])
print(response.content)
# Expected output: a description of the local image's contents
The base64 URL follows the format data:{mime_type};base64,{data}. The common MIME types:
| Extension | MIME type | Notes |
|---|---|---|
.jpg / .jpeg | image/jpeg | Photos, the most common |
.png | image/png | Images with transparency |
.webp | image/webp | Modern format, good compression |
.gif | image/gif | Only the first frame gets processed |
Multiple images in one message
You can send several images in a single message by adding more image_url blocks:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
model = init_chat_model("openai:gpt-4.1")
message = HumanMessage(
content=[
{"type": "text", "text": "Compare these two images. What differences do you see?"},
{
"type": "image_url",
"image_url": {
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"
},
},
{
"type": "image_url",
"image_url": {
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/26/YellowLabradorLooking_new.jpg/1200px-YellowLabradorLooking_new.jpg"
},
},
]
)
response = model.invoke([message])
print(response.content)
# Expected output: "The first image shows a cat, while the second one
# shows a Labrador dog. The main differences are..."
There's no hard limit on how many images you can send, but each one eats tokens from the context window. Bigger images = more tokens = more cost.
Multimodal with Structured Output
You can combine multimodal with with_structured_output from the previous capsule to extract structured data from images:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, Field
class ImageAnalysis(BaseModel):
description: str = Field(description="A brief one-sentence description of the image")
objects: list[str] = Field(description="List of the main objects detected")
dominant_colors: list[str] = Field(description="Dominant colors in the image")
mood: str = Field(description="Overall mood or atmosphere")
model = init_chat_model("openai:gpt-4.1")
structured_model = model.with_structured_output(ImageAnalysis)
message = HumanMessage(
content=[
{"type": "text", "text": "Analyze this image."},
{
"type": "image_url",
"image_url": {
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"
},
},
]
)
result = structured_model.invoke([message])
print(f"Description: {result.description}")
print(f"Objects: {result.objects}")
print(f"Colors: {result.dominant_colors}")
print(f"Mood: {result.mood}")
# Expected output:
# Description: A tabby cat resting in an outdoor setting
# Objects: ['cat', 'grass', 'plants']
# Colors: ['green', 'brown', 'gray']
# Mood: calm
This pattern is very useful for image-processing pipelines. It works with any Pydantic schema from Capsule 05.
Multimodal support by provider
| Provider | Model | Images | Audio | Video |
|---|---|---|---|---|
| OpenAI | gpt-4.1 | ✅ | ✅ | ❌ |
| OpenAI | gpt-4.1-mini | ✅ | ✅ | ❌ |
| OpenAI | o3-mini | ❌ | ❌ | ❌ |
| Anthropic | claude-sonnet-4-20250514 | ✅ | ❌ | ❌ |
| Anthropic | claude-haiku-4-20250514 | ✅ | ❌ | ❌ |
| gemini-2.0-flash | ✅ | ✅ | ✅ | |
| Ollama | llava | ✅ | ❌ | ❌ |
- ✅ GPT-4.1 and Claude Sonnet 4 are the main options for vision
- ✅ Gemini 2.0 Flash is the most versatile: it supports images, audio and video
- ❌ Reasoning models (o3-mini) generally don't support multimodal
- ⚠️ Audio and video support changes often — check the provider's documentation
Audio and video (overview)
Some models accept audio and video as input in addition to images. The pattern is similar — content blocks with specific types:
import base64
from langchain_core.messages import HumanMessage
with open("recording.mp3", "rb") as f:
audio_data = base64.standard_b64encode(f.read()).decode("utf-8")
# Audio input (GPT-4.1, Gemini 2.0 Flash)
message = HumanMessage(
content=[
{"type": "text", "text": "Transcribe this audio."},
{
"type": "input_audio",
"input_audio": {"data": audio_data, "format": "mp3"},
},
]
)
For video, Gemini 2.0 Flash is currently the main model with direct support:
# Video input (Gemini 2.0 Flash)
message = HumanMessage(
content=[
{"type": "text", "text": "Describe what happens in this video."},
{"type": "media", "mime_type": "video/mp4", "data": video_base64},
]
)
⚠️ Audio and video are newer capabilities whose APIs are still evolving. The exact content-block format can vary between providers and package versions. Always check the corresponding provider's up-to-date documentation.
Reasoning: models that think step by step
What are reasoning models?
Standard models (GPT-4.1, Claude Sonnet 4) generate text token by token without "thinking" about the problem globally. They work well for most tasks, but they can stumble on problems that require multi-step logic or complex mathematical reasoning.
Reasoning models spend internal tokens reasoning about the problem before generating the final answer: they break the task down, weigh alternatives, verify intermediate steps. More tokens = more time and cost, but higher quality on complex tasks.
There are two main approaches:
| Approach | Provider | Models | Visible reasoning |
|---|---|---|---|
| Dedicated models | OpenAI | o3-mini | No (internal tokens) |
| Extended thinking | Anthropic | Claude Sonnet 4, Claude Opus 4 | Yes (blocks in the response) |
The key difference: with OpenAI, the reasoning is internal. With Anthropic, you can see exactly what the model thought.
Reasoning with OpenAI (o3-mini)
o3-mini is a model designed for reasoning. You use it just like any other model:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:o3-mini")
response = model.invoke(
"A train leaves Madrid at 8:00 doing 120 km/h. "
"Another leaves Barcelona (620 km away) at 9:00 doing 150 km/h toward Madrid. "
"At what time do they cross?"
)
print(response.content)
# Expected output: the working, plus the correct answer (~10:20)
The model reasons internally but doesn't show the steps in content. The reasoning tokens show up in the metadata:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
model = init_chat_model("openai:o3-mini")
response = model.invoke("How many prime numbers are there between 1 and 50?")
print(response.content)
# Expected output: There are 15 prime numbers between 1 and 50: 2, 3, 5, 7, 11, 13, ...
print(f"Input tokens: {response.usage_metadata['input_tokens']}")
print(f"Output tokens: {response.usage_metadata['output_tokens']}")
print(f"Total: {response.usage_metadata['total_tokens']}")
# Expected output:
# Input tokens: 18
# Output tokens: 245
# Total: 263
The output tokens include both the (internal) reasoning tokens and the visible answer. You pay for both.
Effort levels (OpenAI)
You can control how much effort o3-mini spends reasoning with the reasoning_effort parameter:
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
model_low = init_chat_model("openai:o3-mini", reasoning_effort="low")
model_medium = init_chat_model("openai:o3-mini", reasoning_effort="medium")
model_high = init_chat_model("openai:o3-mini", reasoning_effort="high")
question = "Explain why 0.1 + 0.2 != 0.3 in most programming languages."
for label, m in [("low", model_low), ("medium", model_medium), ("high", model_high)]:
response = m.invoke(question)
tokens = response.usage_metadata["output_tokens"]
print(f"[{label:6s}] Output tokens: {tokens}")
print(f"[{label:6s}] Response: {response.content[:120]}...")
print()
# Expected output: each level spends more tokens and gives more detailed answers
# [low ] ~80 tokens → a concise answer
# [medium] ~200 tokens → an explanation with context
# [high ] ~450 tokens → a detailed analysis of the IEEE 754 standard
| Effort level | Speed | Cost | Quality | When to use it |
|---|---|---|---|---|
"low" | Fast | Low | Good for simple tasks | Classification, direct factual questions |
"medium" | Medium | Medium | Good in general | The recommended default for most cases |
"high" | Slow | High | Maximum | Complex math, formal logic, code debugging |
Important: o3-mini ignores the temperature parameter. Its behavior is controlled exclusively through reasoning_effort. If you need to control randomness, use a standard model.
Extended thinking with Anthropic
Anthropic offers a different approach: Claude models can turn on extended thinking, where the reasoning steps are visible in the response. It requires direct initialization with ChatAnthropic:
from dotenv import load_dotenv
load_dotenv()
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(
model="claude-sonnet-4-20250514",
max_tokens=16000,
thinking={
"type": "enabled",
"budget_tokens": 10000,
},
)
response = model.invoke(
"If I have 3 boxes, each box has 2 bags, and each bag has 5 coins, "
"how many coins do I have in total? Show your reasoning."
)
# With extended thinking, response.content is a LIST of blocks
for block in response.content:
if block["type"] == "thinking":
print(f"[The model's reasoning]")
print(block["thinking"][:500])
print()
elif block["type"] == "text":
print(f"[Final answer]")
print(block["text"])
# Expected output:
# [The model's reasoning]
# I need to work out the total number of coins.
# 3 boxes × 2 bags = 6 bags. 6 bags × 5 coins = 30. Check: 3×2×5 = 30.
#
# [Final answer]
# You have 30 coins in total.
Requirements for extended thinking:
- ✅
max_tokensis mandatory and has to be greater thanbudget_tokens - ✅
budget_tokenscontrols how many tokens the model can spend thinking - ⚠️
temperaturehas to be1(or left unset) while extended thinking is on - ⚠️ Not every Claude model supports it — check Anthropic's documentation
When to use reasoning vs standard models
Reasoning isn't always the better option. More reasoning means more tokens, more latency and more cost. Use this guide to decide:
| Task | Recommended model | Why |
|---|---|---|
| Simple Q&A, summaries | GPT-4.1-mini, Claude Haiku | Fast and cheap, no reasoning needed |
| Creative writing | GPT-4.1, Claude Sonnet 4 | Needs quality, not formal logic |
| Data extraction (structured output) | GPT-4.1-mini | A mechanical task, reasoning is wasted |
| Math problems | o3-mini (high) | Needs step verification |
| Complex logical analysis | o3-mini (high), Claude + thinking | Multi-step decomposition |
| Code debugging | o3-mini (medium), Claude + thinking | Tracing the program's flow |
| Text classification | GPT-4.1-mini | A simple task, no reasoning needed |
| Comparing options with trade-offs | Claude + thinking | Visible reasoning helps you audit it |
Rule of thumb: If you can solve the task without thinking for more than 5 seconds, use a standard model. If you need pen and paper to check the answer, use reasoning.
Connection to the project
In the Multi-Provider Chat with Fallback:
- You can add multimodal support so the user can send images into the chat
- Content blocks work the same across every provider with vision support — the fallback can go from GPT-4.1 to Claude Sonnet 4 without changing the message format
- For complex questions, the chat can route to a reasoning model instead of the standard one
- The structured metadata (Capsule 05) can include a field indicating whether reasoning was used
Troubleshooting
Problem 1: "Could not process image" or an error when sending an image
Cause: The model doesn't support multimodal input, or the image's URL isn't reachable. Fix: Check that you're using a model with vision support (see the support table). If you're using a URL, make sure it's public and reachable:
import requests
url = "https://your-image.com/photo.jpg"
r = requests.head(url)
print(f"Status: {r.status_code}")
print(f"Content-Type: {r.headers.get('content-type')}")
# It should be 200 and an image content-type
Problem 2: content as a list doesn't work with the model
Cause: The model doesn't support content blocks (e.g. o3-mini, or local models without vision).
Fix: Check the support in the provider table. If you need a model without multimodal support, send text only:
# This works with any model
message = HumanMessage(content="Plain text")
# This requires a model with multimodal support
message = HumanMessage(content=[{"type": "text", "text": "..."}, {"type": "image_url", ...}])
Problem 3: A base64 image causes a size error
Cause: The image is too large. Providers have limits (OpenAI: ~20MB, Anthropic: ~5MB per image). Fix: Resize the image before encoding it:
from PIL import Image
import io
import base64
def encode_image_resized(path: str, max_size: int = 1024) -> str:
"""Resizes the image if it exceeds max_size and returns base64."""
img = Image.open(path)
if max(img.size) > max_size:
img.thumbnail((max_size, max_size))
buffer = io.BytesIO()
img.save(buffer, format="JPEG", quality=85)
return base64.standard_b64encode(buffer.getvalue()).decode("utf-8")
Problem 4: reasoning_effort has no effect
Cause: You're using reasoning_effort with a model that isn't a reasoning model (e.g. gpt-4.1).
Fix: That parameter only works with reasoning models like o3-mini. For standard models, use temperature to control behavior.
Problem 5: Anthropic's extended thinking returns a string instead of a list
Cause: The thinking parameter wasn't passed correctly, or max_tokens is missing.
Fix: max_tokens is mandatory when you use thinking, and budget_tokens has to be lower than max_tokens:
model = ChatAnthropic(
model="claude-sonnet-4-20250514",
max_tokens=16000,
thinking={"type": "enabled", "budget_tokens": 10000},
)
Exercises
Exercise 1: Describe an image (Easy)
Send an image from a URL to a model with multimodal support and ask it to describe the image. Use gpt-4.1 or claude-sonnet-4-20250514. Print the description.
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
model = init_chat_model("openai:gpt-4.1")
message = HumanMessage(
content=[
{"type": "text", "text": "Describe this image in detail."},
{
"type": "image_url",
"image_url": {
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/300px-PNG_transparency_demonstration_1.png"
},
},
]
)
response = model.invoke([message])
print(response.content)
# Expected output: a detailed description of the image
# (two red dice over a checkered background demonstrating PNG transparency)
Explanation: The image_url content block tells the model to download and process the image. The model combines the instruction text with the visual analysis to produce the description.
Exercise 2: Extract data from an image (Medium)
Combine multimodal with Structured Output. Define a Pydantic schema ProductInfo with fields name, price (float), and category (str). Send the URL of a product image and extract the structured data.
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, Field
class ProductInfo(BaseModel):
name: str = Field(description="Name of the product visible in the image")
price: float = Field(description="The product's price if visible, 0.0 if not")
category: str = Field(description="General product category: electronics, clothing, food, other")
model = init_chat_model("openai:gpt-4.1")
structured_model = model.with_structured_output(ProductInfo)
message = HumanMessage(
content=[
{"type": "text", "text": "Extract the product information from this image."},
{
"type": "image_url",
"image_url": {
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/16/AirPods_Pro_%282nd_generation%29.jpg/440px-AirPods_Pro_%282nd_generation%29.jpg"
},
},
]
)
result = structured_model.invoke([message])
print(f"Name: {result.name}")
print(f"Price: ${result.price}")
print(f"Category: {result.category}")
# Expected output:
# Name: AirPods Pro (2nd generation)
# Price: $0.0 (not visible in the image)
# Category: electronics
Explanation: with_structured_output works the same with multimodal messages. The model analyzes the image and maps what it sees onto the Pydantic schema's fields. Field(description=...) guides the model on what to pull out of the image.
Exercise 3: Reasoning with o3-mini (Easy)
Use o3-mini to solve a logic problem. Compare the answer with gpt-4.1-mini's for the same problem. Print both responses and the tokens used.
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.chat_models import init_chat_model
question = (
"I have 5 shirts and 3 pairs of pants. "
"How many different outfits can I put together if I always wear one shirt and one pair of pants?"
)
model_standard = init_chat_model("openai:gpt-4.1-mini")
model_reasoning = init_chat_model("openai:o3-mini")
print("=== GPT-4.1-mini (standard) ===")
r1 = model_standard.invoke(question)
print(f"Response: {r1.content}")
print(f"Tokens: {r1.usage_metadata['total_tokens']}")
print("\n=== o3-mini (reasoning) ===")
r2 = model_reasoning.invoke(question)
print(f"Response: {r2.content}")
print(f"Tokens: {r2.usage_metadata['total_tokens']}")
# Expected output: both answer 15, but o3-mini burns more tokens (internal reasoning).
Explanation: Both land on the right answer, but o3-mini spends more tokens (and more money) because it reasons internally. For simple tasks, the standard model is more efficient.
Exercise 4: Compare effort levels (Medium)
Ask the same complex question to o3-mini at all 3 effort levels (low, medium, high). Print the response, the output tokens, and time each one.
See solution
from dotenv import load_dotenv
load_dotenv()
import time
from langchain.chat_models import init_chat_model
question = (
"A snail climbs 3 meters every day but slides back 2 meters every night. "
"If the well is 10 meters deep, how many days does it take to get out?"
)
for effort in ["low", "medium", "high"]:
model = init_chat_model("openai:o3-mini", reasoning_effort=effort)
start = time.time()
response = model.invoke(question)
elapsed = time.time() - start
print(f"=== Effort: {effort} ===")
print(f"Response: {response.content[:200]}")
print(f"Output tokens: {response.usage_metadata['output_tokens']}")
print(f"Time: {elapsed:.1f}s")
print()
# Expected output: low is fast with few tokens, high is slow but detailed.
# All of them should answer 8 days (on day 8 it climbs to 10m and gets out).
Explanation: Notice how high spends more tokens (and time) reasoning. In this case, the right answer is 8 days (on day 8 it climbs to 10m and gets out before nightfall). With low, the model may still give the right answer but with less detail. With high, you'll probably see a day-by-day verification.
Exercise 5: Extended thinking with Anthropic (Hard)
Use ChatAnthropic with extended thinking enabled to solve a logic problem. Pull out and display the reasoning and the final answer separately. Work out what percentage of the tokens went into reasoning.
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(
model="claude-sonnet-4-20250514",
max_tokens=8000,
thinking={"type": "enabled", "budget_tokens": 5000},
)
question = (
"In a race, you overtake the runner in second place. "
"What position are you in now?"
)
response = model.invoke(question)
thinking_text = ""
answer_text = ""
for block in response.content:
if block["type"] == "thinking":
thinking_text = block["thinking"]
elif block["type"] == "text":
answer_text = block["text"]
print("[The model's reasoning]")
print(thinking_text)
print()
print("[Final answer]")
print(answer_text)
print()
input_tokens = response.usage_metadata["input_tokens"]
output_tokens = response.usage_metadata["output_tokens"]
print(f"Input tokens: {input_tokens}")
print(f"Output tokens: {output_tokens}")
# Expected output:
# [The model's reasoning]
# The question is designed to pull an intuitive wrong answer out of you.
# If you overtake the runner in second, you take THEIR position...
#
# [Final answer]
# You're in second place. By overtaking the second-place runner, you take their spot.
Explanation: Extended thinking exposes the full reasoning process. You can audit how the model reached its conclusion, which is impossible with o3-mini.
Summary
In this capsule you learned:
- Content blocks let you send images (and other media) alongside text in a single message
- For images from a URL, you use
{"type": "image_url", "image_url": {"url": "https://..."}} - For local images, you encode them in base64 with the format
data:image/jpeg;base64,... - Multimodal + Structured Output combine directly — you can extract typed data from images
- Reasoning models (o3-mini) spend internal tokens thinking before answering
reasoning_effortcontrols how much o3-mini thinks:"low","medium","high"- Anthropic's extended thinking makes the reasoning visible in blocks separate from the answer
- Not everything needs reasoning — for simple tasks, standard models are faster and cheaper
Next capsule: Local models, caching and rate limiting — how to run models with Ollama, cut costs with prompt caching, and handle rate limits in production.
Additional resources
- Multimodal — LangChain Docs — The conceptual guide to multimodal in LangChain
- How to pass multimodal data to models — A step-by-step tutorial with content blocks
- OpenAI Vision Guide — OpenAI's vision documentation
- Anthropic Vision Documentation — Anthropic's vision guide with limits and formats
- OpenAI Reasoning Models — Guide to o3-mini and reasoning effort
- Anthropic Extended Thinking — Documentation for extended thinking with Claude
- Gemini Multimodal Capabilities — Images, audio and video with Gemini
- LangChain ChatAnthropic API Reference — Reference for the
thinkingparameter
Module 1 — LangChain & LangGraph: From Chains to Agents