skills/dspy-adapters/SKILL.md
Use when you need to customize how DSPy formats prompts for a specific provider — switching from chat to completion format, forcing JSON output, or debugging prompt rendering issues. Common scenarios - debugging why your prompt looks wrong when sent to the model, switching from OpenAI to Anthropic and the formatting breaks, forcing the model to return valid JSON instead of markdown, working with completion-style models that do not support chat format, customizing system messages, or handling models that choke on structured output instructions. Related - ai-switching-models, ai-following-rules, ai-parsing-data. Also used for prompt template rendering, how DSPy builds the prompt, custom system message in DSPy, JSON mode not working, model ignores format instructions, switch from chat to completion API, dspy.ChatAdapter, dspy.JSONAdapter, prompt formatting issues, debug what DSPy sends to the model, dspy.XMLAdapter, XML output format.
npx skillsauth add lebsral/dspy-programming-not-prompting-lms-skills dspy-adaptersInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Adapters sit between your DSPy modules and the language model. They control how signatures get turned into prompts and how LM responses get parsed back into typed Python objects. Most of the time the default adapter just works -- but when you need tighter control over structured output, or you are working with reasoning models that struggle with formatting, adapters give you that control.
Every time a DSPy module calls an LM, an adapter handles two jobs:
You never call adapters directly. You configure one globally or per-module, and DSPy uses it behind the scenes.
| Adapter | How it formats | How it parses | Best for |
|---------|---------------|---------------|----------|
| ChatAdapter | Field markers like [[ ## field_name ## ]] | Splits on field headers | General use (default) |
| JSONAdapter | Requests JSON output with field schema | json_repair + type casting | Reliable structured output |
| XMLAdapter | XML tags like <field_name>value</field_name> | Regex on XML tags | Models that handle XML well |
| TwoStepAdapter | Natural language prompt (no formatting constraints) | Sends raw response to a second LM for extraction | Reasoning models (o1, o3) |
ChatAdapter is what DSPy uses unless you say otherwise. It formats prompts with field delimiters and parses responses by looking for those same delimiters in the output.
import dspy
# ChatAdapter is used automatically -- no configuration needed
lm = dspy.LM("openai/gpt-4o-mini") # or "anthropic/claude-sonnet-4-5-20250929", etc.
dspy.configure(lm=lm)
classify = dspy.ChainOfThought("text -> label")
result = classify(text="Great product!")
print(result.label)
dspy.ChatAdapter(
callbacks=None, # Optional list of BaseCallback objects
use_native_function_calling=False, # Use native function calling features
native_response_types=None, # Output field types handled natively by the LM
use_json_adapter_fallback=True, # Fall back to JSONAdapter on parse failure
)
[[ ## field_name ## ]] delimiters in the prompt.JSONAdapter (unless you set use_json_adapter_fallback=False).You rarely need to instantiate ChatAdapter yourself. Do it when you want to:
dspy.ChatAdapter(use_json_adapter_fallback=False)dspy.ChatAdapter(use_native_function_calling=True)adapter = dspy.ChatAdapter(use_json_adapter_fallback=False)
dspy.configure(lm=lm, adapter=adapter)
JSONAdapter extends ChatAdapter and instructs the LM to respond with a JSON object matching your output fields. It uses the provider's native structured output mode when available, falling back to response_format: {"type": "json_object"}.
import dspy
lm = dspy.LM("openai/gpt-4o-mini") # or "anthropic/claude-sonnet-4-5-20250929", etc.
adapter = dspy.JSONAdapter()
dspy.configure(lm=lm, adapter=adapter)
# Now all modules request JSON output from the LM
classify = dspy.Predict("text -> label: str, confidence: float")
result = classify(text="Great product!")
print(result.label) # positive
print(result.confidence) # 0.95
dspy.JSONAdapter(
callbacks=None, # Optional list of BaseCallback objects
use_native_function_calling=True, # Enabled by default (unlike ChatAdapter)
)
"(must be formatted as a valid Python str)").json_repair for resilience against minor formatting errors.AdapterParseError if the response cannot be parsed as a JSON object.Use JSONAdapter when you need more reliable structured output, especially with:
[[ ## field ## ]] formatfrom pydantic import BaseModel
class Invoice(BaseModel):
vendor: str
total: float
line_items: list[dict]
class ExtractInvoice(dspy.Signature):
"""Extract invoice details from the document text."""
document: str = dspy.InputField()
invoice: Invoice = dspy.OutputField()
lm = dspy.LM("openai/gpt-4o") # or "anthropic/claude-sonnet-4-5-20250929", etc.
adapter = dspy.JSONAdapter()
dspy.configure(lm=lm, adapter=adapter)
extractor = dspy.Predict(ExtractInvoice)
result = extractor(document="Invoice #1234 from Acme Corp. Total: $1,250.00 ...")
print(result.invoice.vendor) # Acme Corp
print(result.invoice.total) # 1250.0
print(result.invoice.line_items) # [...]
XMLAdapter extends ChatAdapter and wraps input/output fields in XML tags like <field_name>value</field_name> instead of [[ ## field ## ]] delimiters. Some models respond more reliably to XML-structured prompts.
import dspy
lm = dspy.LM("openai/gpt-4o-mini") # or "anthropic/claude-sonnet-4-5-20250929", etc.
adapter = dspy.XMLAdapter()
dspy.configure(lm=lm, adapter=adapter)
classify = dspy.Predict("text -> label: str, confidence: float")
result = classify(text="Great product!")
dspy.XMLAdapter(
callbacks=None, # Optional list of BaseCallback objects
)
<field_name>value</field_name>.JSONAdapter on parse failure (like ChatAdapter).callbacks.Consider XMLAdapter when:
TwoStepAdapter is designed for reasoning models (like OpenAI's o1 and o3 series) that produce better answers when they are not forced into a rigid output format. It splits the work into two steps:
import dspy
# Main LM: a reasoning model that struggles with structured output
main_lm = dspy.LM("openai/o3-mini", max_tokens=16000, temperature=1.0) # or another reasoning model
# Extraction model: a fast, cheap model for parsing
extraction_lm = dspy.LM("openai/gpt-4o-mini") # or "anthropic/claude-haiku-3-5-20241022", etc.
adapter = dspy.TwoStepAdapter(extraction_model=extraction_lm)
dspy.configure(lm=main_lm, adapter=adapter)
# The reasoning model thinks freely; gpt-4o-mini extracts the answer
solver = dspy.ChainOfThought("question -> answer")
result = solver(question="What is the sum of the first 100 prime numbers?")
print(result.answer)
dspy.TwoStepAdapter(
extraction_model=dspy.LM("openai/gpt-4o-mini"), # Required: cheap LM for extraction
)
ChatAdapter internally to parse the main LM's freeform response into structured fields.Use TwoStepAdapter when:
| Situation | Adapter | Why |
|-----------|---------|-----|
| General use, most models | ChatAdapter (default) | Works out of the box, no config needed |
| Need reliable JSON/Pydantic output | JSONAdapter | Stricter parsing, native structured output support |
| Complex nested output types | JSONAdapter | Better at complex schemas than field-delimiter parsing |
| Model responds better to XML structure | XMLAdapter | XML tags instead of field delimiters |
| Using reasoning models (o1, o3) | TwoStepAdapter | Reasoning models perform worse with format constraints |
| Parse failures in production | JSONAdapter | More resilient parsing with json_repair |
| Fastest iteration, prototyping | ChatAdapter (default) | Zero config, good enough for most tasks |
Set the adapter for all modules at once:
import dspy
lm = dspy.LM("openai/gpt-4o-mini") # or "anthropic/claude-sonnet-4-5-20250929", etc.
adapter = dspy.JSONAdapter()
dspy.configure(lm=lm, adapter=adapter)
Switch adapters for a block of code without changing the global config:
json_adapter = dspy.JSONAdapter()
# Use JSONAdapter just for this block
with dspy.context(adapter=json_adapter):
result = extractor(document=text)
# Back to the default ChatAdapter outside the block
Assign different adapters to different modules using set_adapter():
class Pipeline(dspy.Module):
def __init__(self):
self.classify = dspy.Predict("text -> label")
self.extract = dspy.Predict(ExtractDetails)
def forward(self, text):
# Classification is simple -- default ChatAdapter is fine
label = self.classify(text=text)
return self.extract(text=text)
pipeline = Pipeline()
# Use JSONAdapter only for the extraction step
pipeline.extract.set_adapter(dspy.JSONAdapter())
dspy.adapters.baml_adapter) — exists in the DSPy source but is undocumented and likely experimental. It integrates with the BAML structured output framework. Do not use in production until it has official documentation.You can build your own adapter by subclassing the base Adapter class. This is advanced -- only needed if the built-in adapters do not fit your use case.
import dspy
from dspy.adapters import Adapter
class MyAdapter(Adapter):
def format(self, signature, demos, inputs, messages=None):
"""Convert signature + inputs into a list of messages for the LM."""
system_msg = {"role": "system", "content": f"Task: {signature.instructions}"}
user_msg = {"role": "user", "content": str(inputs)}
return [system_msg, user_msg]
def parse(self, signature, completion):
"""Extract output fields from the LM's raw response text."""
# Your custom parsing logic here
fields = {}
for field_name in signature.output_fields:
fields[field_name] = completion.strip()
return fields
adapter = MyAdapter()
dspy.configure(lm=lm, adapter=adapter)
Override format() to control how prompts are built and parse() to control how responses are read. The return from parse() should be a dict mapping output field names to their values.
By default, ChatAdapter already falls back to JSONAdapter on parse failure. This gives you the best of both worlds -- fast field-delimiter parsing most of the time, with JSON as a safety net.
# This is the default behavior -- you get it for free
lm = dspy.LM("openai/gpt-4o-mini") # or "anthropic/claude-sonnet-4-5-20250929", etc.
dspy.configure(lm=lm)
# ChatAdapter is used, with automatic JSONAdapter fallback on failure
To disable this fallback:
adapter = dspy.ChatAdapter(use_json_adapter_fallback=False)
dspy.configure(lm=lm, adapter=adapter)
# Full setup for reasoning models
reasoning_lm = dspy.LM("openai/o3-mini", max_tokens=16000, temperature=1.0) # or another reasoning model
extraction_lm = dspy.LM("openai/gpt-4o-mini") # or "anthropic/claude-haiku-3-5-20241022", etc.
adapter = dspy.TwoStepAdapter(extraction_model=extraction_lm)
dspy.configure(lm=reasoning_lm, adapter=adapter)
import dspy
lm = dspy.LM("openai/gpt-4o") # or "anthropic/claude-sonnet-4-5-20250929", etc.
dspy.configure(lm=lm) # default ChatAdapter
class MixedPipeline(dspy.Module):
def __init__(self):
self.summarize = dspy.ChainOfThought("document -> summary")
self.extract = dspy.Predict(ExtractInvoice)
def forward(self, document):
summary = self.summarize(document=document)
return self.extract(document=document)
pipeline = MixedPipeline()
# ChatAdapter for summarization (freeform text is fine)
# JSONAdapter for extraction (need reliable structured output)
pipeline.extract.set_adapter(dspy.JSONAdapter())
ChatAdapter automatically retries with JSONAdapter when parsing fails. If you are only switching to JSONAdapter to fix occasional parse errors, the default fallback may already handle it. Only switch globally when you need JSON for every call.set_adapter() for per-module overrides. When a pipeline has steps that need different adapters (e.g., freeform summarization + structured extraction), Claude sets the adapter globally instead of per-module. Use pipeline.extract.set_adapter(dspy.JSONAdapter()) to override only the modules that need it.Install any skill:
npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill <name>
/dspy-signatures -- Define the input/output fields that adapters format and parse/dspy-lm -- Configure the language model that adapters communicate with/dspy-modules -- Modules that use adapters under the hood (Predict, ChainOfThought, etc.)/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-dotools
See what is happening during optimizer.compile() instead of waiting blind. Use when you want to watch optimization progress, see scores as they come in, know if your optimizer is working, check if optimization is stuck, understand why optimization is taking too long, get live progress during compile, monitor convergence, detect overfitting during optimization, interpret optimization results, or pick the right tool for watching optimization. Also used for optimizer progress bar, is my optimizer doing anything, optimization seems stuck, how long will optimization take, watch GEPA run, watch MIPROv2 run, live optimization dashboard, optimizer not improving, scores not going up, optimization taking forever, see what optimizer is doing, debug slow optimization, optimization visibility, optimizer metrics, track compile progress, optimization observability.
testing
Use when you want the highest-quality prompt optimization DSPy offers — jointly optimizes instructions and few-shot demos, with auto=light/medium/heavy presets. Common scenarios - you want the best possible accuracy from prompt optimization, jointly tuning instructions and few-shot demonstrations, using auto presets for different compute budgets, or when COPRO or BootstrapFewShot alone are not reaching your accuracy target. Related - ai-improving-accuracy, dspy-copro, dspy-bootstrap-few-shot. Also used for dspy.MIPROv2, best DSPy optimizer, highest quality optimization, auto=light medium heavy, joint instruction and demo optimization, most powerful prompt optimizer, MIPROv2 vs COPRO vs BootstrapFewShot, which optimizer should I use, state of the art prompt optimization, when to use MIPROv2, optimize both instructions and examples, heavy optimization for production, best optimizer for accuracy.
testing
Use LangWatch for DSPy auto-tracing and real-time optimizer progress. Use when you want to set up LangWatch, langwatch.dspy.init, auto-tracing DSPy, real-time optimization dashboard, optimizer progress tracking, app.langwatch.ai, or DSPy optimizer dashboard. Also used for langwatch setup, pip install langwatch, langwatch trace, optimizer progress, real-time optimization, watch optimizer run, LangWatch self-hosted, langwatch docker, langwatch vs langtrace, langwatch autotrack_dspy.
data-ai
Use when you want to optimize instructions without few-shot examples — a lightweight alternative to COPRO when you do not have or do not want to use demonstrations. Common scenarios - optimizing instructions when you do not have or do not want to use few-shot demonstrations, lightweight instruction search as a first step, tasks where examples in the prompt confuse the model, or when you want fast instruction optimization without the cost of COPRO. Related - ai-improving-accuracy, dspy-copro, dspy-miprov2. Also used for dspy.GEPA, instruction optimization without demos, lightweight prompt optimization, optimize instructions only, no few-shot examples needed, GEPA vs COPRO, quick instruction search, when demonstrations hurt performance, zero-shot optimization, instruction-only optimizer, simplest instruction tuner, fast prompt optimization, skip few-shot and just tune instructions, optimize Pydantic field descriptions, GEPA structured output, GEPA does not optimize field desc.