skills/dspy-chatadapter/SKILL.md
Deep dive into dspy.ChatAdapter -- the default adapter that formats DSPy signatures into multi-turn chat messages with field delimiters, parses LM responses back into typed Python objects, and falls back to JSONAdapter on failure. Use when you need to understand how DSPy builds prompts, debug why a model ignores output format, customize prompt rendering, enable native function calling, use callbacks, generate fine-tuning data, or control the JSON fallback. Also used for how DSPy formats prompts, field delimiters, prompt template rendering, parse error debugging, ChatAdapter vs JSONAdapter vs TwoStepAdapter, format_finetune_data, dspy prompt inspection, why model output is wrong format, adapter callbacks, native function calling in DSPy.
npx skillsauth add lebsral/dspy-programming-not-prompting-lms-skills dspy-chatadapterInstall 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.
Before diving into adapter internals, clarify:
If you just need to pick the right adapter, start with /dspy-adapters instead -- it covers the decision between ChatAdapter, JSONAdapter, TwoStepAdapter, and XMLAdapter.
ChatAdapter is the default adapter in DSPy. Every time a module calls an LM, ChatAdapter handles two jobs:
[[ ## field_name ## ]] delimitersYou never call it directly -- DSPy uses it behind the scenes. But understanding its internals helps you debug formatting issues and customize behavior.
dspy.ChatAdapter(
callbacks=None, # list[BaseCallback] | None
use_native_function_calling=False, # bool
native_response_types=None, # list[type] | None
use_json_adapter_fallback=True, # bool
)
| Parameter | Type | Default | What it controls |
|-----------|------|---------|-----------------|
| callbacks | list[BaseCallback] \| None | None | Callback hooks executed during format/parse |
| use_native_function_calling | bool | False | Use provider-native function calling for structured output |
| native_response_types | list[type] \| None | None | Output field types handled by native LM features instead of text parsing |
| use_json_adapter_fallback | bool | True | Automatically retry with JSONAdapter when parsing fails |
ChatAdapter converts a DSPy call into a multi-turn message list:
System message: Task instructions from the signature docstring
+ field structure showing expected input/output format
+ output type hints and constraints
Demo messages: For each few-shot demo:
User message: input fields with [[ ## field ## ]] headers
Assistant message: output fields with headers + [[ ## completed ## ]]
History messages: If dspy.History is used, prior conversation turns
User message: Current input fields with headers
+ output format reminder (for long conversations)
ChatAdapter marks each field with header delimiters:
[[ ## question ## ]]
What is the capital of France?
[[ ## answer ## ]]
Paris
[[ ## completed ## ]]
The [[ ## completed ## ]] marker signals that the LM has finished all output fields. This is how parse() knows where output ends.
Use dspy.inspect_history() to see the exact messages ChatAdapter builds:
import dspy
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini")) # or "anthropic/claude-sonnet-4-5-20250929", etc.
program = dspy.ChainOfThought("question -> answer")
result = program(question="What is DSPy?")
# See the full prompt and response
dspy.inspect_history(n=1)
When the LM responds, parse():
[[ ## field_name ## ]] headersparse_value() to cast each value to its declared Python typeIf any step fails, the adapter raises AdapterParseError -- which triggers the JSON fallback (if enabled).
By default, ChatAdapter automatically retries with JSONAdapter when parsing fails:
ChatAdapter.parse() succeeds? -> Return result
fails? -> Is it a ContextWindowExceededError?
Yes -> Re-raise (cannot fix by reformatting)
No -> Retry entire call with JSONAdapter
This means most parse failures self-heal without intervention. To observe when fallback triggers, enable debug logging or check dspy.inspect_history() for duplicate calls.
To disable the fallback:
adapter = dspy.ChatAdapter(use_json_adapter_fallback=False)
dspy.configure(lm=lm, adapter=adapter)
# Now parse failures raise AdapterParseError immediately
Some providers (OpenAI, Anthropic) support native structured output via function calling. ChatAdapter can use this instead of text-based field delimiters:
adapter = dspy.ChatAdapter(use_native_function_calling=True)
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"), adapter=adapter)
# Output fields are now enforced via the provider's function calling API
# rather than text delimiters in the prompt
Use native_response_types to limit which output types use native features:
from pydantic import BaseModel
class StructuredResult(BaseModel):
category: str
confidence: float
# Only use native function calling for Pydantic output types
adapter = dspy.ChatAdapter(
use_native_function_calling=True,
native_response_types=[BaseModel],
)
ChatAdapter formats demos as user/assistant message pairs. Demos come in two flavors:
Complete demos (all fields present):
User: [[ ## question ## ]]
What color is the sky?
Assistant: [[ ## answer ## ]]
Blue
[[ ## completed ## ]]
Incomplete demos (some fields missing -- common during bootstrapping):
User: This is an example of the task, though some input or output
fields are not supplied.
[[ ## question ## ]]
What color is the sky?
Assistant: [[ ## answer ## ]]
Blue
[[ ## completed ## ]]
The prefix on incomplete demos tells the LM not to infer missing fields from incomplete examples.
ChatAdapter handles dspy.History fields by converting them into alternating user/assistant message pairs inserted before the current input:
import dspy
class Chatbot(dspy.Module):
def __init__(self):
self.respond = dspy.Predict("history: dspy.History, question -> response")
def forward(self, history, question):
return self.respond(history=history, question=question)
# History becomes prior message pairs in the formatted prompt
history = dspy.History(
messages=[
{"role": "user", "content": "Hi there"},
{"role": "assistant", "content": "Hello! How can I help?"},
]
)
ChatAdapter can produce OpenAI-compatible fine-tuning data from your DSPy programs:
adapter = dspy.ChatAdapter()
# Generate fine-tuning format for a single example
finetune_data = adapter.format_finetune_data(
signature=my_signature,
demos=my_demos,
inputs={"question": "What is DSPy?"},
outputs={"answer": "A framework for programming LMs"},
)
# Returns: {"messages": [{"role": "system", ...}, {"role": "user", ...}, {"role": "assistant", ...}]}
This is useful when you want to fine-tune a model on the exact prompt format DSPy uses, ensuring the fine-tuned model responds in a way ChatAdapter can parse reliably.
| Aspect | ChatAdapter | JSONAdapter | TwoStepAdapter | XMLAdapter |
|--------|-------------|-------------|----------------|------------|
| Delimiter style | [[ ## field ## ]] headers | JSON object keys | Natural language (step 1) + ChatAdapter (step 2) | <field>...</field> XML tags |
| Parse resilience | Falls back to JSONAdapter | json_repair library | Delegated to extraction LM | Falls back to JSONAdapter |
| Native structured output | Optional (use_native_function_calling) | On by default | N/A | No |
| LM calls per prediction | 1 | 1 | 2 (main + extraction) | 1 |
| Best for | General use, most models | Reliable structured output, complex Pydantic types | Reasoning models (o1, o3) | Models that respond well to XML |
JSONAdapterTwoStepAdapterXMLAdapterdspy.configure(lm=lm) already uses it. Only instantiate explicitly when you need to change a parameter like use_json_adapter_fallback=False or use_native_function_calling=True.use_native_function_calling=True for all providers. Not all providers support native function calling. OpenAI and Anthropic do; many local models and smaller providers do not. If the provider does not support it, the call fails. Check provider capabilities before enabling, or let ChatAdapter fall back to text-based delimiters.[[ ## field ## ]] format, ChatAdapter automatically retries with JSONAdapter. Before adding manual error handling or switching adapters, check dspy.inspect_history() to see if the fallback already succeeded silently.DSPyInstrumentor().instrument() after the adapter is configured and expects to see adapter details in traces. The adapter formats and parses happen inside the LM call. Instrumentation captures the LM call, but adapter internals (which delimiter style was used, whether fallback triggered) are not always visible in traces. Use dspy.inspect_history() for adapter-level debugging.[[ ## completed ## ]] when manually constructing few-shot demos. If you build demos by hand (not via optimization), omitting the completion marker causes the LM to keep generating past the expected output. Let DSPy handle demo formatting through BootstrapFewShot or LabeledFewShot rather than manually constructing demos with delimiters.Install any skill:
npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill <name>
/dspy-adapters/dspy-signatures/dspy-lminspect_history -- see /dspy-utilsformat_finetune_data -- see /ai-fine-tuning/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.