
Dspy Chatadapter
- 2 installs
- 11 repo stars
- Updated June 28, 2026
- lebsral/dspy-programming-not-prompting-lms-skills
Deep dive into dspy.ChatAdapter, the default adapter that formats DSPy signatures into chat messages and parses responses back into typed objects, for debugging and customization.
About
Explains how ChatAdapter formats prompts with field delimiters, parses LM output, and falls back to JSONAdapter on failure. A developer uses it to debug format-parse errors, customize prompt rendering, enable native function calling, or generate fine-tuning data.
- Format and parse jobs use [[ ## field_name ## ]] delimiters
- Covers callbacks, native function calling, and JSON fallback control
Dspy Chatadapter by the numbers
- 2 all-time installs (skills.sh)
- Ranked #13,958 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/lebsral/dspy-programming-not-prompting-lms-skills --skill dspy-chatadapterAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 11 |
| Last updated | June 28, 2026 |
| Repository | lebsral/dspy-programming-not-prompting-lms-skills ↗ |
What it does
Deep dive into dspy.ChatAdapter, the default adapter that formats DSPy signatures into chat messages and parses responses back into typed objects, for debugging and customization.
Files
dspy.ChatAdapter -- How DSPy Formats Prompts
Step 1: Understand what you need
Before diving into adapter internals, clarify:
1. Are you debugging a formatting issue? (model ignores format, parse errors, wrong output structure) 2. Do you need to customize how prompts are built? (system messages, field order, special providers) 3. Are you generating fine-tuning data? (need OpenAI-compatible message format) 4. Do you need native function calling or structured output? (provider-specific features)
If you just need to pick the right adapter, start with /dspy-adapters instead -- it covers the decision between ChatAdapter, JSONAdapter, TwoStepAdapter, and XMLAdapter.
What ChatAdapter does
ChatAdapter is the default adapter in DSPy. Every time a module calls an LM, ChatAdapter handles two jobs:
1. Format: Converts signature + demos + inputs into a list of chat messages (system, user, assistant) 2. Parse: Extracts output fields from the LM response using [[ ## field_name ## ]] delimiters
You never call it directly -- DSPy uses it behind the scenes. But understanding its internals helps you debug formatting issues and customize behavior.
Constructor
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 |
use_native_function_calling | bool | False | Use provider-native function calling for structured output |
native_response_types | `list[type] \ | None` | None |
use_json_adapter_fallback | bool | True | Automatically retry with JSONAdapter when parsing fails |
How formatting works
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)The field delimiter system
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.
Inspecting what gets sent to the LM
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)How parsing works
When the LM responds, parse():
1. Splits the response text on [[ ## field_name ## ]] headers 2. Maps each section to the corresponding output field 3. Calls parse_value() to cast each value to its declared Python type 4. Validates all required output fields are present 5. Returns a dict of field names to typed values
If any step fails, the adapter raises AdapterParseError -- which triggers the JSON fallback (if enabled).
The JSON fallback mechanism
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 JSONAdapterThis 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 immediatelyNative function calling
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 promptUse 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],
)Few-shot demo formatting
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.
Conversation history
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?"},
]
)Generating fine-tuning data
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.
ChatAdapter vs the other adapters
| 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 |
When to switch away from ChatAdapter
- Parse errors on complex output types (nested Pydantic, lists of objects) ->
JSONAdapter - Reasoning model produces worse answers with format constraints ->
TwoStepAdapter - Model responds better to XML structure (some Anthropic models) ->
XMLAdapter - No issues -> Keep ChatAdapter (the default is good)
Gotchas
- Claude instantiates ChatAdapter when it is not needed. ChatAdapter is the default --
dspy.configure(lm=lm)already uses it. Only instantiate explicitly when you need to change a parameter likeuse_json_adapter_fallback=Falseoruse_native_function_calling=True. - Claude sets `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.
- Claude does not realize parse failures auto-heal via JSON fallback. When a model garbles the
[[ ## field ## ]]format, ChatAdapter automatically retries with JSONAdapter. Before adding manual error handling or switching adapters, checkdspy.inspect_history()to see if the fallback already succeeded silently. - Claude calls `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. - Claude forgets `[[ ## 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
BootstrapFewShotorLabeledFewShotrather than manually constructing demos with delimiters.
Additional resources
- dspy.ChatAdapter API docs
- dspy.JSONAdapter API docs
- dspy.TwoStepAdapter API docs
- dspy.XMLAdapter API docs
- reference.md — constructor, all methods, format/parse protocol
- examples.md — debugging parse failures, mixed adapter pipelines, fine-tuning data generation
Cross-references
Install any skill: npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill <name>- All adapters overview (ChatAdapter vs JSONAdapter vs TwoStepAdapter vs XMLAdapter) -- see
/dspy-adapters - Signatures that adapters format and parse -- see
/dspy-signatures - LM configuration that adapters communicate with -- see
/dspy-lm - Debugging and inspection tools including
inspect_history-- see/dspy-utils - Fine-tuning with data generated by
format_finetune_data-- see/ai-fine-tuning - 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
[
{
"prompt": "My DSPy program works with GPT-4o but fails to parse outputs with a different model. How do I debug what ChatAdapter is sending and receiving?",
"expected_output": "Uses dspy.inspect_history() to examine the formatted prompt and response, checks for delimiter compliance",
"assertions": [
"Uses dspy.inspect_history(n=1) to inspect the exact messages sent",
"Checks whether the response contains [[ ## field ## ]] delimiters",
"Mentions that JSON fallback may have already self-healed the error",
"Suggests switching to JSONAdapter if ChatAdapter consistently fails with the model"
]
},
{
"prompt": "I have a pipeline where one step needs reliable JSON output from a Pydantic model and another step just generates freeform text. Can I use different adapters for each?",
"expected_output": "Uses set_adapter() to assign JSONAdapter to the structured step while keeping ChatAdapter as default",
"assertions": [
"Uses module.set_adapter(dspy.JSONAdapter()) for the structured output step",
"Keeps ChatAdapter as the global default for freeform text steps",
"Does NOT change the global adapter with dspy.configure for just one module",
"Explains that set_adapter overrides per-module without affecting other modules"
]
},
{
"prompt": "I want to fine-tune a model on the exact prompt format DSPy uses so it responds correctly. How do I generate the training data?",
"expected_output": "Uses ChatAdapter.format_finetune_data() to generate OpenAI-compatible JSONL training data",
"assertions": [
"Uses adapter.format_finetune_data(signature, demos, inputs, outputs)",
"Outputs JSONL format with {messages: [system, user, assistant]} records",
"Notes that the fine-tuned model will respond with [[ ## field ## ]] delimiters that ChatAdapter can parse natively",
"Does NOT manually construct the chat message format"
]
}
]
dspy-chatadapter Examples
Example 1: Debugging a parse failure
A common scenario: your DSPy module works with one model but breaks with another because the new model does not follow the field delimiter format consistently.
import dspy
# Works fine with GPT-4o-mini
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini")) # or "anthropic/claude-sonnet-4-5-20250929", etc.
class ExtractContact(dspy.Signature):
"""Extract contact information from the text."""
text: str = dspy.InputField()
name: str = dspy.OutputField()
email: str = dspy.OutputField()
phone: str = dspy.OutputField()
extractor = dspy.Predict(ExtractContact)
result = extractor(text="Call John Smith at john@acme.com or 555-0123")
# Step 1: Inspect what ChatAdapter sent and received
dspy.inspect_history(n=1)
# Look for the [[ ## field ## ]] delimiters in the prompt and response.
# If the response lacks delimiters, the model is ignoring the format.
# Step 2: Check if JSON fallback kicked in
# Run the same call again and inspect history for TWO consecutive calls.
# If you see a second call with JSON instructions, fallback triggered automatically.
# Step 3: If fallback also fails, switch to JSONAdapter explicitly
adapter = dspy.JSONAdapter()
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"), adapter=adapter)
result = extractor(text="Call John Smith at john@acme.com or 555-0123")
print(f"{result.name}, {result.email}, {result.phone}")What this demonstrates:
- `dspy.inspect_history()` is the primary tool for debugging adapter behavior
- JSON fallback often self-heals parse failures without any code changes
- Switching to JSONAdapter is the fix when ChatAdapter's delimiter format consistently fails with a specific model
Example 2: Mixed adapter pipeline with per-module assignment
A pipeline where summarization uses ChatAdapter (freeform text is fine) but data extraction uses JSONAdapter (need reliable structured output).
import dspy
from pydantic import BaseModel
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini")) # or "anthropic/claude-sonnet-4-5-20250929", etc.
# --- Pydantic model for structured extraction ---
class MeetingAction(BaseModel):
assignee: str
task: str
deadline: str
class MeetingExtraction(dspy.Signature):
"""Extract action items from meeting notes."""
notes: str = dspy.InputField()
summary: str = dspy.OutputField(desc="2-3 sentence summary")
action_items: list[MeetingAction] = dspy.OutputField()
# --- Pipeline ---
class MeetingProcessor(dspy.Module):
def __init__(self):
# Simple summarization -- ChatAdapter is fine
self.summarize = dspy.ChainOfThought("notes -> summary")
# Structured extraction -- JSONAdapter for reliability
self.extract = dspy.Predict(MeetingExtraction)
def forward(self, notes):
summary = self.summarize(notes=notes)
extraction = self.extract(notes=notes)
return extraction
processor = MeetingProcessor()
# Assign JSONAdapter only to the extraction step
processor.extract.set_adapter(dspy.JSONAdapter())
result = processor(notes="""
Q3 planning meeting, Oct 15.
- Sarah will finalize the budget by Oct 20.
- Mike to hire 2 engineers by end of November.
- Team agreed to ship v2.0 by December 1.
""")
print(f"Summary: {result.summary}")
for item in result.action_items:
print(f" {item.assignee}: {item.task} (by {item.deadline})")What this demonstrates:
- `set_adapter()` assigns a different adapter to a specific module without changing the global config
- ChatAdapter for freeform output (summaries, reasoning) where exact formatting does not matter
- JSONAdapter for structured output (Pydantic models, lists of objects) where parse reliability matters
- No global adapter change needed -- only the module that needs stricter parsing gets JSONAdapter
Example 3: Generating fine-tuning data from a DSPy program
Export the exact prompt format ChatAdapter uses so you can fine-tune a model that responds correctly to it.
import json
import dspy
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini")) # or "anthropic/claude-sonnet-4-5-20250929", etc.
# --- Define signature and collect examples ---
class ClassifyTicket(dspy.Signature):
"""Classify a support ticket by category and urgency."""
ticket_text: str = dspy.InputField()
category: str = dspy.OutputField()
urgency: str = dspy.OutputField()
# Labeled examples for fine-tuning
examples = [
{"inputs": {"ticket_text": "Payment failed twice, charged both times"},
"outputs": {"category": "billing", "urgency": "high"}},
{"inputs": {"ticket_text": "How do I change my email address?"},
"outputs": {"category": "account", "urgency": "low"}},
{"inputs": {"ticket_text": "App crashes when I upload photos"},
"outputs": {"category": "bug", "urgency": "medium"}},
# ... hundreds more examples
]
# --- Generate OpenAI fine-tuning JSONL ---
adapter = dspy.ChatAdapter()
with open("finetune_data.jsonl", "w") as f:
for ex in examples:
finetune_record = adapter.format_finetune_data(
signature=ClassifyTicket,
demos=[], # no few-shot demos in fine-tuning data
inputs=ex["inputs"],
outputs=ex["outputs"],
)
# Each record is {"messages": [system, user, assistant]}
f.write(json.dumps(finetune_record) + "\n")
print(f"Wrote {len(examples)} fine-tuning examples to finetune_data.jsonl")
# The fine-tuned model will respond using [[ ## field ## ]] delimiters
# that ChatAdapter can parse natively -- no adapter changes needed.What this demonstrates:
- `format_finetune_data()` produces OpenAI-compatible
{"messages": [...]}format - Delimiter consistency -- the fine-tuned model learns to respond with
[[ ## field ## ]]headers that ChatAdapter already knows how to parse - No adapter switch needed after fine-tuning -- the model speaks ChatAdapter's format natively
- Production pattern for teams using
dspy.BootstrapFinetunewho want to understand what data format is generated
Condensed from dspy.ai/api/adapters/ChatAdapter/. Verify against upstream for latest.
dspy.ChatAdapter — API Reference
Constructor
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 | Description |
|---|---|---|---|
callbacks | `list[BaseCallback] | None` | None |
use_native_function_calling | bool | False | Use provider-native function calling for structured output (OpenAI, Anthropic). |
native_response_types | `list[type] | None` | None |
use_json_adapter_fallback | bool | True | Automatically retry with JSONAdapter when ChatAdapter parsing fails. |
Methods
Core call methods
| Method | Signature | Returns | Description |
|---|---|---|---|
__call__ | (lm, lm_kwargs, signature, demos, inputs) | list[dict[str, Any]] | Execute adapter with automatic JSON fallback on errors (except context window exceeded). |
acall | (lm, lm_kwargs, signature, demos, inputs) | list[dict[str, Any]] | Async version with identical fallback behavior. |
Format methods
| Method | Signature | Returns | Description |
|---|---|---|---|
format | (signature, demos, inputs) | list[dict[str, Any]] | Convert signature + demos + inputs into multiturn chat messages (system, user, assistant). |
format_system_message | (signature) | str | Generate system instructions from signature docstring, field descriptions, and type constraints. |
format_user_message_content | (signature, inputs, prefix='', suffix='', main_request=False) | str | Structure user input with [[ ## field ## ]] markers and optional output format reminders. |
user_message_output_requirements | (signature) | str | Return lightweight format reminder for long conversations to maintain output structure awareness. |
Parse methods
| Method | Signature | Returns | Description |
|---|---|---|---|
parse | (signature, completion) | dict[str, Any] | Extract output fields from LM response using [[ ## field_name ## ]] delimiters. Raises AdapterParseError on failure. |
Fine-tuning
| Method | Signature | Returns | Description |
|---|---|---|---|
format_finetune_data | (signature, demos, inputs, outputs) | dict[str, list[Any]] | Format data for OpenAI API fine-tuning as {"messages": [system, user, assistant]}. |
Field delimiter protocol
ChatAdapter uses [[ ## field_name ## ]] headers to delimit fields in both prompts and responses:
[[ ## question ## ]]
What is the capital of France?
[[ ## answer ## ]]
Paris
[[ ## completed ## ]]The [[ ## completed ## ]] marker signals that all output fields have been provided.
Fallback behavior
ChatAdapter.parse() succeeds? → Return result
fails? → ContextWindowExceededError? → Re-raise
→ Other error? → Retry entire call with JSONAdapterPer-module adapter assignment
# Assign a different adapter to a specific module
my_module.set_adapter(dspy.JSONAdapter())This overrides the global adapter for that module only.