Skip to main content
An adapter sits between your signature and the LM. It formats typed inputs into prompts and parses LM responses back into typed outputs.

What adapters do

The default adapter is ChatAdapter, which handles both formatting and parsing.

ChatAdapter

Prompt structure

The system message has four parts (in this order):
  1. Field descriptions - Lists input/output fields with types and descriptions
  2. Field structure - Shows the marker format + type schemas
  3. Response instructions - “Respond with [[ ## field ## ]]…”
  4. Task description - Your instruction from the signature docstring (at the end)
Example system message (simplified):

Marker protocol

Fields are delimited by markers:
This protocol (inspired by DSPy) allows mixing natural language with structured output.

Type schemas

For complex types, the adapter renders full schemas using BAML’s schema format:
Type names are simplified for readability:
  • Module paths stripped: my_crate::SentimentSentiment
  • class/enum prefixes removed
  • " | "" or " (more natural)

Robust parsing with jsonish

Response parsing uses BAML’s jsonish parser, which handles:
  • Malformed JSON - Missing quotes, trailing commas
  • Markdown fences - Extracts JSON from ```json blocks
  • Type coercion - "42"42, "true"true
  • Partial recovery - Returns what it can parse + all errors

Constraint evaluation

After parsing, the adapter runs constraints:
  • #[check] results → stored in metadata
  • #[assert] failures → returns ParseError::AssertFailed

Error handling

Parse errors aggregate - you get all problems, not just the first:

Using the adapter directly

Usually you don’t touch the adapter - Predict handles it. But for debugging:
This is useful for understanding what the LM sees.

Input formatting options

Input fields support two rendering paths:
  • #[format("json" | "yaml" | "toon")]: serialize a field using that format.
  • #[render(jinja = "...")]: render a field with a MiniJinja template.
  • #[format] and #[render] are mutually exclusive on the same field.
#[render] context in ChatAdapter:
  • this: current field value
  • input: full input object (plus top-level alias overlays)
  • field: { name, rust_name, type }
  • vars: adapter/surface vars (currently {})
ChatAdapter configures MiniJinja with strict undefined behavior. Available filters include:
  • MiniJinja built-ins
  • BAML parity helpers (regex_match, sum)
  • truncate for length-limited string rendering
If template rendering fails at runtime (for example, missing variables), ChatAdapter panics. Compiled templates are cached process-wide by template string.

Real example: Insurance claim extraction

Here’s what a prompt looks like for a complex nested type (from examples/16-insurance-claim-prompt.rs):
Generated system message:
Generated user message:
Notice how:
  • Enums with many variants become “Definitions” at the top
  • Doc comments become inline // comments in the schema
  • Option<T> renders as T or null
  • Nested objects show their full structure
  • The instruction comes from the signature docstring

Design notes

  • DSPy inspiration: Marker protocol from DSPy’s ChatAdapter
  • BAML inspiration: Schema rendering and jsonish parsing from BAML
  • Separation of concerns: Adapter doesn’t know about LMs - just formatting/parsing