Use Reasoning Models with dspy.TwoStepAdapter
Guide the user through configuring DSPy to work with reasoning models (o1, o3, o3-mini, DeepSeek-R1, Claude with extended thinking) that need special handling for structured output.
Why reasoning models need TwoStepAdapter
Reasoning models (o1, o3, DeepSeek-R1, Claude extended thinking) behave differently from standard chat models:
- They reject or ignore system prompts (o1/o3 strip them)
- They ignore formatting instructions (the model "thinks" and produces free-form output)
- They cannot follow ChatAdapter's field delimiters (
[[ ## field_name ## ]])
TwoStepAdapter solves this with a two-phase approach:
- Phase 1 (main LM): The reasoning model generates freely -- no formatting constraints
- Phase 2 (extraction LM): A fast, cheap model parses the reasoning output into structured fields
When to use TwoStepAdapter
| Use TwoStepAdapter when... |
Use ChatAdapter (default) when... |
| Using o1, o3, o3-mini |
Using GPT-4o, Claude, Gemini |
| Using DeepSeek-R1 |
Using any instruction-following model |
| Using Claude with extended thinking |
The model follows formatting reliably |
| Model ignores your output format |
Structured output works out of the box |
| Getting raw reasoning dumps instead of fields |
You do not need reasoning-heavy processing |
Step 1: Basic TwoStepAdapter setup
import dspy
# The reasoning model (generates freely)
main_lm = dspy.LM("openai/o3-mini")
# The extraction model (parses into structured fields)
extraction_lm = dspy.LM("openai/gpt-4o-mini")
# Configure the adapter (only the extraction model is passed in)
adapter = dspy.TwoStepAdapter(extraction_model=extraction_lm)
# The reasoning model is the configured lm, not an adapter argument
dspy.configure(lm=main_lm, adapter=adapter)
Now use DSPy normally -- the adapter handles the two-phase flow transparently:
qa = dspy.ChainOfThought("question -> answer")
result = qa(question="What is 127 * 389?")
print(result.answer) # Structured output, extracted by gpt-4o-mini
Step 2: Model pairing recommendations
Reasoning model (dspy.configure(lm=...)) |
Extraction model (extraction_model=...) |
Notes |
openai/o1 |
openai/gpt-4o-mini |
Best reasoning + cheap extraction |
openai/o3 |
openai/gpt-4o-mini |
Highest capability |
openai/o3-mini |
openai/gpt-4o-mini |
Cost-effective reasoning |
deepseek/deepseek-r1 |
openai/gpt-4o-mini |
Open-weight reasoning |
anthropic/claude-sonnet-4-5-20250929 (extended thinking) |
anthropic/claude-haiku-3-5-20241022 |
All-Anthropic stack |
Pairing principles:
- The extraction LM should be fast and cheap -- it just parses structured fields from text
- The extraction LM must follow formatting instructions well (ChatAdapter-compatible)
- Cross-provider pairing is fine (o3 + Claude Haiku works)
Step 3: Per-module adapter assignment
You can use TwoStepAdapter for specific modules while using ChatAdapter elsewhere:
import dspy
# Default: fast model with ChatAdapter
fast_lm = dspy.LM("openai/gpt-4o-mini")
dspy.configure(lm=fast_lm)
# Reasoning model for hard problems
reasoning_lm = dspy.LM("openai/o3-mini")
extraction_lm = dspy.LM("openai/gpt-4o-mini")
reasoning_adapter = dspy.TwoStepAdapter(extraction_model=extraction_lm)
class Pipeline(dspy.Module):
def __init__(self):
# Simple classification -- uses default ChatAdapter
self.classify = dspy.Predict("text -> category")
# Hard reasoning -- uses TwoStepAdapter
self.analyze = dspy.ChainOfThought("text, category -> analysis")
self.analyze.adapter = reasoning_adapter
self.analyze.lm = reasoning_lm
def forward(self, text):
category = self.classify(text=text).category
analysis = self.analyze(text=text, category=category).analysis
return dspy.Prediction(category=category, analysis=analysis)
Step 4: Handling extended thinking (Claude)
Claude with extended thinking uses a budget parameter instead of a separate model:
import dspy
# Claude with extended thinking enabled
thinking_lm = dspy.LM(
"anthropic/claude-sonnet-4-5-20250929",
thinking={"type": "enabled", "budget_tokens": 10000},
)
# Extraction model
extraction_lm = dspy.LM("anthropic/claude-haiku-3-5-20241022")
adapter = dspy.TwoStepAdapter(extraction_model=extraction_lm)
# The thinking model is the configured lm, not an adapter argument
dspy.configure(lm=thinking_lm, adapter=adapter)
# Works transparently -- thinking model reasons, haiku extracts structure
solver = dspy.ChainOfThought("problem -> solution")
result = solver(problem="Prove that sqrt(2) is irrational")
Step 5: When NOT to use TwoStepAdapter
Do not use TwoStepAdapter when:
- The model already follows formatting -- standard models (GPT-4o, Claude, Gemini) work fine with ChatAdapter
- You need minimum latency -- two LM calls instead of one adds overhead
- The task is simple -- reasoning models are overkill for classification or extraction
- Cost is critical -- you pay for both the reasoning call and the extraction call
If the model follows ChatAdapter format reliably, TwoStepAdapter adds cost and latency for no benefit.
Gotchas
- Claude uses ChatAdapter for o1/o3 models. Reasoning models reject ChatAdapter formatting. If you see raw reasoning dumps without structured fields, switch to TwoStepAdapter.
- Claude sets extraction_lm to the same reasoning model. The extraction model should be fast and cheap (gpt-4o-mini, Claude Haiku). Using o3 for extraction wastes money and is slower.
- Claude forgets to set both
lm and adapter on per-module assignment. When assigning TwoStepAdapter to a specific module, set both module.lm = reasoning_lm and module.adapter = dspy.TwoStepAdapter(extraction_model=...). There is no main_lm constructor arg -- the reasoning model is the module's lm, and only the extraction model goes to the adapter. Missing either causes the wrong model or wrong adapter to be used.
- TwoStepAdapter is not needed for DeepSeek-V3. Only DeepSeek-R1 (the reasoning variant) needs it. DeepSeek-V3 follows formatting like a standard chat model.
- Claude wraps extended thinking in TwoStepAdapter incorrectly. For Claude extended thinking, pass
thinking={"type": "enabled", "budget_tokens": N} to the LM constructor, not as a separate parameter.
Additional resources
Cross-references
Install any skill: npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill <name>
- All adapters overview -- see
/dspy-adapters
- ChatAdapter deep dive -- see
/dspy-chatadapter
- LM configuration and provider setup -- see
/dspy-lm
- Switching models without breaking prompts -- see
/ai-switching-models
- Install
/ai-do if you do not have it -- it routes any AI problem to the right skill and is the fastest way to work: npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill ai-do
1---2name: dspy-two-step-adapter3description: Use when working with reasoning models (o1, o3, o3-mini, DeepSeek-R1, Claude extended thinking) that reject system prompts or ignore formatting instructions. Common scenarios - using o1 or o3 with DSPy, getting structured output from reasoning models, two-phase prompting where a reasoning model generates freely then an extraction model parses the output, or fixing format errors from thinking models. Related - dspy-adapters, dspy-lm, ai-switching-models. Also used for dspy.TwoStepAdapter, o1 with DSPy, o3-mini DSPy, reasoning model in DSPy, DeepSeek-R1 DSPy, extended thinking DSPy, thinking model ignores format, o1 ignores my DSPy format, TwoStepAdapter setup, two-phase prompting, extraction LM, reasoning model structured output, extraction_model TwoStepAdapter, model pairing for reasoning models.4---56# Use Reasoning Models with dspy.TwoStepAdapter78Guide the user through configuring DSPy to work with reasoning models (o1, o3, o3-mini, DeepSeek-R1, Claude with extended thinking) that need special handling for structured output.910## Why reasoning models need TwoStepAdapter1112Reasoning models (o1, o3, DeepSeek-R1, Claude extended thinking) behave differently from standard chat models:1314- They **reject or ignore system prompts** (o1/o3 strip them)15- They **ignore formatting instructions** (the model "thinks" and produces free-form output)16- They **cannot follow ChatAdapter's field delimiters** (`[[ ## field_name ## ]]`)1718`TwoStepAdapter` solves this with a two-phase approach:191. **Phase 1 (main LM):** The reasoning model generates freely -- no formatting constraints202. **Phase 2 (extraction LM):** A fast, cheap model parses the reasoning output into structured fields2122## When to use TwoStepAdapter2324| Use TwoStepAdapter when... | Use ChatAdapter (default) when... |25|---------------------------|----------------------------------|26| Using o1, o3, o3-mini | Using GPT-4o, Claude, Gemini |27| Using DeepSeek-R1 | Using any instruction-following model |28| Using Claude with extended thinking | The model follows formatting reliably |29| Model ignores your output format | Structured output works out of the box |30| Getting raw reasoning dumps instead of fields | You do not need reasoning-heavy processing |3132## Step 1: Basic TwoStepAdapter setup3334```python35import dspy3637# The reasoning model (generates freely)38main_lm = dspy.LM("openai/o3-mini")3940# The extraction model (parses into structured fields)41extraction_lm = dspy.LM("openai/gpt-4o-mini")4243# Configure the adapter (only the extraction model is passed in)44adapter = dspy.TwoStepAdapter(extraction_model=extraction_lm)4546# The reasoning model is the configured lm, not an adapter argument47dspy.configure(lm=main_lm, adapter=adapter)48```4950Now use DSPy normally -- the adapter handles the two-phase flow transparently:5152```python53qa = dspy.ChainOfThought("question -> answer")54result = qa(question="What is 127 * 389?")55print(result.answer) # Structured output, extracted by gpt-4o-mini56```5758## Step 2: Model pairing recommendations5960| Reasoning model (`dspy.configure(lm=...)`) | Extraction model (`extraction_model=...`) | Notes |61|---------------------------|----------------------------------|-------|62| `openai/o1` | `openai/gpt-4o-mini` | Best reasoning + cheap extraction |63| `openai/o3` | `openai/gpt-4o-mini` | Highest capability |64| `openai/o3-mini` | `openai/gpt-4o-mini` | Cost-effective reasoning |65| `deepseek/deepseek-r1` | `openai/gpt-4o-mini` | Open-weight reasoning |66| `anthropic/claude-sonnet-4-5-20250929` (extended thinking) | `anthropic/claude-haiku-3-5-20241022` | All-Anthropic stack |6768**Pairing principles:**69- The extraction LM should be **fast and cheap** -- it just parses structured fields from text70- The extraction LM must **follow formatting instructions well** (ChatAdapter-compatible)71- Cross-provider pairing is fine (o3 + Claude Haiku works)7273## Step 3: Per-module adapter assignment7475You can use TwoStepAdapter for specific modules while using ChatAdapter elsewhere:7677```python78import dspy7980# Default: fast model with ChatAdapter81fast_lm = dspy.LM("openai/gpt-4o-mini")82dspy.configure(lm=fast_lm)8384# Reasoning model for hard problems85reasoning_lm = dspy.LM("openai/o3-mini")86extraction_lm = dspy.LM("openai/gpt-4o-mini")87reasoning_adapter = dspy.TwoStepAdapter(extraction_model=extraction_lm)888990class Pipeline(dspy.Module):91 def __init__(self):92 # Simple classification -- uses default ChatAdapter93 self.classify = dspy.Predict("text -> category")9495 # Hard reasoning -- uses TwoStepAdapter96 self.analyze = dspy.ChainOfThought("text, category -> analysis")97 self.analyze.adapter = reasoning_adapter98 self.analyze.lm = reasoning_lm99100 def forward(self, text):101 category = self.classify(text=text).category102 analysis = self.analyze(text=text, category=category).analysis103 return dspy.Prediction(category=category, analysis=analysis)104```105106## Step 4: Handling extended thinking (Claude)107108Claude with extended thinking uses a budget parameter instead of a separate model:109110```python111import dspy112113# Claude with extended thinking enabled114thinking_lm = dspy.LM(115 "anthropic/claude-sonnet-4-5-20250929",116 thinking={"type": "enabled", "budget_tokens": 10000},117)118119# Extraction model120extraction_lm = dspy.LM("anthropic/claude-haiku-3-5-20241022")121122adapter = dspy.TwoStepAdapter(extraction_model=extraction_lm)123124# The thinking model is the configured lm, not an adapter argument125dspy.configure(lm=thinking_lm, adapter=adapter)126127# Works transparently -- thinking model reasons, haiku extracts structure128solver = dspy.ChainOfThought("problem -> solution")129result = solver(problem="Prove that sqrt(2) is irrational")130```131132## Step 5: When NOT to use TwoStepAdapter133134Do not use TwoStepAdapter when:135136- **The model already follows formatting** -- standard models (GPT-4o, Claude, Gemini) work fine with ChatAdapter137- **You need minimum latency** -- two LM calls instead of one adds overhead138- **The task is simple** -- reasoning models are overkill for classification or extraction139- **Cost is critical** -- you pay for both the reasoning call and the extraction call140141If the model follows ChatAdapter format reliably, TwoStepAdapter adds cost and latency for no benefit.142143## Gotchas1441451. **Claude uses ChatAdapter for o1/o3 models.** Reasoning models reject ChatAdapter formatting. If you see raw reasoning dumps without structured fields, switch to TwoStepAdapter.1462. **Claude sets extraction_lm to the same reasoning model.** The extraction model should be fast and cheap (gpt-4o-mini, Claude Haiku). Using o3 for extraction wastes money and is slower.1473. **Claude forgets to set both `lm` and `adapter` on per-module assignment.** When assigning TwoStepAdapter to a specific module, set both `module.lm = reasoning_lm` and `module.adapter = dspy.TwoStepAdapter(extraction_model=...)`. There is no `main_lm` constructor arg -- the reasoning model is the module's `lm`, and only the extraction model goes to the adapter. Missing either causes the wrong model or wrong adapter to be used.1484. **TwoStepAdapter is not needed for DeepSeek-V3.** Only DeepSeek-R1 (the reasoning variant) needs it. DeepSeek-V3 follows formatting like a standard chat model.1495. **Claude wraps extended thinking in TwoStepAdapter incorrectly.** For Claude extended thinking, pass `thinking={"type": "enabled", "budget_tokens": N}` to the LM constructor, not as a separate parameter.150151## Additional resources152153- [dspy.ai/api/adapters/TwoStepAdapter](https://dspy.ai/api/adapters/TwoStepAdapter/)154- For API details, see [reference.md](reference.md)155- For worked examples, see [examples.md](examples.md)156157## Cross-references158159> Install any skill: `npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill <name>`160161- **All adapters** overview -- see `/dspy-adapters`162- **ChatAdapter** deep dive -- see `/dspy-chatadapter`163- **LM configuration** and provider setup -- see `/dspy-lm`164- **Switching models** without breaking prompts -- see `/ai-switching-models`165- **Install `/ai-do` if you do not have it** -- it routes any AI problem to the right skill and is the fastest way to work: `npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill ai-do`