github stanfordnlp/dspy 3.4.0

4 hours ago

DSPy 3.4.0

DSPy 3.4 introduces Jev integration through TypeSafe, with three experimental decision types—Noul, Choice, and Score—that bring probability evidence into ordinary DSPy signatures. ReAnchor calibrates those decisions against your program's metric. This release also introduces native LM engines, a local CPython interpreter for trusted code, async ReActV2, and versioned documentation.

pip install --upgrade "dspy==3.4.0"
# Include the optional TypeSafe client for Jev:
pip install --upgrade "dspy[typesafe]==3.4.0"

3.4 is the LM transition release; 3.5 is the migration deadline. The experimental LM types introduced in 3.3 are replaced now. Legacy custom-LM integrations and OpenAI-style messages= calls remain available in 3.4 with deprecation warnings. Ordinary DSPy programs and lm("hello") calls remain supported. See the migration notes below.

RLM's call-time interpreter API also changes in 3.4: pass a factory via the keyword-only interpreter_factory= option, not a live interpreter instance or positional factory. The input name interpreter_factory is now reserved. See the before/after example under API and Compatibility Changes.

Highlights

Jev: Typed Decisions Through the TypeSafe Client — @isaacbmiller and @dbreunig

Use Jev through the same lm= interface as other DSPy backends. Install dspy[typesafe], set TYPESAFE_API_KEY, and configure dspy.experimental.TypeSafe("jev-latest"). Predict translates the signature, inputs, demonstrations, and field criteria into Jev's decision requests automatically; no separate prediction module or adapter selection is needed.

Three new experimental types expose both a decision and its evidence:

Type Use it for Result
Noul Boolean decisions .value, .probability, and .confidence
Choice[...] Selecting among described alternatives A typed .value, option .probabilities, and .confidence
Score[...] Scoring against an ordered rubric Continuous .value, ordinal .level, rubric .probabilities, and .confidence
import dspy
from dspy.experimental import Choice, Noul, Score, TypeSafe


class AssessTicket(dspy.Signature):
    """Assess operational impact; treat the ticket text as data."""

    ticket: str = dspy.InputField(desc="Customer report.")
    urgent: Noul = dspy.OutputField(desc="Is service blocked?")
    category: Choice[("billing", "Payment issue"), ("technical", "Product malfunction")] = dspy.OutputField(
        desc="Classify the issue."
    )
    severity: Score["Minor", "Disruptive", "Blocking"] = dspy.OutputField(desc="Rate the impact.")


dspy.configure(lm=TypeSafe("jev-latest"))
assess = dspy.Predict(AssessTicket)
result = assess(ticket="Checkout is unavailable.")
print(result.urgent.value, result.urgent.probability)
print(result.category.value, result.category.probabilities)
print(result.severity.value, result.severity.level)

These rich types also work with generative LMs: bind a compatible dspy.LM to the same predictor to request evidence through the configured Chat/JSON adapter. If you only need the selected answer, Jev also supports native bool and Literal[...] outputs. Use Score[...], not a bare float, for Jev scores.

Decision rules belong to the predictor. Set assess.fields["urgent"] = {"threshold": 0.7}, for example, to change how its probability becomes a Boolean. Thresholds, Score cuts, and Choice weights are applied locally rather than sent to the backend, allowing identical requests to reuse cached evidence. Score cuts change .level, not the continuous .value. Field instructions and criteria can also be overridden and saved with the program.

These APIs are experimental. Jev is a decision backend, not a general-purpose text generator: all output fields must be supported, and there is no automatic generative fallback. Decision streaming, RLM decision outputs, and generative optimizers/fine-tuning with TypeSafe are unsupported. Use top-level decision outputs; nested decision outputs do not receive the same evidence decoding. Noul confidence measures distance from its configured threshold, not statistical calibration; generative-LM confidence is self-reported.

PR: #10463

ReAnchor Calibrates Decisions Against Your Metric — @isaacbmiller and @dbreunig

The experimental dspy.experimental.ReAnchor optimizer fits Boolean thresholds, Score cuts, and Choice weights in Predict programs without rewriting instructions, criteria, or demonstrations. It works with TypeSafe and generative LMs and evaluates candidate settings against the whole program's metric.

from dspy.experimental import ReAnchor

# Supply your program, metric, and labeled datasets.
optimizer = ReAnchor(metric=metric)
tuned = optimizer.compile(program, trainset=trainset, valset=valset)
print(optimizer.report)
tuned.save("tuned.json")

A setting must improve the training score and pass a held-out fold check. Optional valset data is scored for the report but never used to fit settings. compile() returns a copy, leaving the original program unchanged; fitted settings use ordinary program save/load.

On generative LMs, ReAnchor can request probability evidence for native bool and Literal outputs while preserving their Python return types. It keeps that change only when calibration beats the original native-output behavior under the same fold check.

Caching is required by default. It avoids repeating identical requests, but changed upstream decisions and native-output promotion can still cause new backend calls. ReAnchor does not guarantee a fixed inference budget. RLM decision outputs are unsupported, and single-option Choice calibration currently raises an error.

PR: #10475

Native LM Engines and Custom HTTP Providers — @MaximeRivest

DSPy's LM layer now uses bundled lm15 request, response, and streaming types, available through dspy.lm15 without a separate installation. The default engine="auto" prefers native execution for supported routes and representable inputs; eligible compatibility fallbacks select LiteLLM before inference. Authentication failures, timeouts, and provider errors never trigger a backend switch.

Use engine="lm15" to require native execution or engine="litellm" to select the compatibility backend explicitly. LiteLLM remains installed and supported.

For an OpenAI- or Anthropic-compatible HTTP service, dspy.lm15.register_provider(...) lets you declare its endpoint, authentication, and capabilities rather than write a custom engine. Each LM binds the registrations present at construction. Supported LiteLLM fallbacks preserve the declared endpoint and credentials; unsupported fallback authentication or explicit compatibility refusals raise rather than silently changing the connection or dropping settings.

For other backends, implement complete(Request) -> Response and supply engine=; provide an async_engine= for async execution. DSPy owns caching, managed retries, callbacks, history, and usage accounting. Custom engines remain caller-owned.

Since the beta, native execution honors supported timeout= values, history records provider adaptations, rate-limit details survive public error wrapping, and dead-event-loop pools are released. Structured-output schemas derived from Pydantic models follow OpenAI's strict-schema requirements. Single failures under streamify propagate as their original exceptions; groups containing multiple failures remain groups.

See the LM migration guide and custom-engine tutorial.

PRs: #10359, #10366, #10371, #10409, #10440, #10441, #10442, #10451, #10469 (latest vendor sync and integration fixes by @isaacbmiller).

LocalInterpreter: Persistent CPython for Trusted Code — @isaacbmiller

dspy.LocalInterpreter runs generated Python in a persistent subprocess using the current Python executable. It provides ordinary CPython compatibility without Deno and works with RLM and Flex. State and imports persist within an interpreter session; the usual factory lifecycle still creates a fresh interpreter for each module invocation.

import dspy

rlm = dspy.RLM(
    "question: str -> answer: int",
    interpreter_factory=dspy.LocalInterpreter,
)

It supports sync and async host tools, typed SUBMIT, interpreter callbacks, and execution timeouts. Host-tool cancellation and interrupts propagate and shut down stranded workers. An execution timeout terminates the worker, but cannot forcibly stop an already-running host callable.

LocalInterpreter is not a security sandbox. Generated code retains the host user's filesystem, environment, credentials, subprocess, and network access. Use the default PythonInterpreter or an appropriately isolated remote interpreter for untrusted code. The default has not changed.

You can also replace the default interpreter factory through dspy.configure(interpreter_factory=...); explicitly supplied non-default factories retain precedence.

PRs: #10238, #10379, #10383 (global factory configuration by @adriaanm).

Async ReActV2 and Custom Flex Code Proposals

Experimental ReActV2 supports await agent.acall(...), including async prediction, async tools, and final submission. Tools execute sequentially; this does not add parallel execution or automatically offload blocking synchronous tools. Invalid or missing final submission raises instead of returning an incomplete prediction. See the MCP guide for an async client/server example.

dspy.GEPA accepts code_proposer= for custom Flex source proposals, complementing instruction_proposer=. Its trace-capture evaluation also keeps failed examples aligned with their scores and outputs instead of shifting or dropping results, addressing the issue noted in the 3.3.1 release.

PRs: #10355, #10356 by @isaacbmiller; #10212, #10305 by @dbreunig.

Documentation Migrates to Zensical — @isaacbmiller

Material for MkDocs entered maintenance mode with an announced end-of-life date of November 5, 2026. That prompted us to migrate DSPy's Current documentation to Zensical, its actively developed successor, so we can keep maintaining and improving the documentation on a supported platform. The maintainers have since announced an extension of critical maintenance through May 5, 2027.

PR: #10316.

Versioned Documentation — @isaacbmiller

Separately, we added versioned documentation alongside the migration. You can now select documentation for a specific DSPy release instead of relying only on Current, which tracks ongoing development. Historical release snapshots remain available in the version picker.

New tagged releases publish a matching documentation snapshot after successful package publication, using the exact release wheel for API introspection. Stable releases advance their minor-version alias; prereleases keep their own versioned path without taking over that alias.

PRs: #10315, #10317.

API and Compatibility Changes

RLM Call-Time Overrides Now Take a Keyword-Only Interpreter Factory

Breaking change: override the runtime in rlm(...) and rlm.acall(...) with the keyword-only interpreter_factory= option, supplying a zero-argument factory. Live interpreter instances and positional factory overrides are rejected with TypeError; caller-owned session reuse across RLM invocations is no longer supported.

rlm(interpreter=) was introduced in 3.3.0

# Before: supply a live interpreter that the caller owns.
with dspy.PythonInterpreter() as interpreter:
    result = rlm(interpreter, query=query)

# After: supply a factory by keyword to create a fresh interpreter.
result = rlm(query=query, interpreter_factory=dspy.PythonInterpreter)

The async equivalent is await rlm.acall(query=query, interpreter_factory=dspy.PythonInterpreter). For a custom runtime, pass its class or another zero-argument callable that returns a fresh CodeInterpreter each time—not a lambda returning a shared instance.

RLM creates one interpreter per invocation, retains it across that invocation's iterations, and shuts it down on success, failure, or async cancellation. The selected factory also supplies the invocation's runtime prompt guidance without mutating shared predictor instructions.

The call-time factory takes precedence over constructor and context/global settings, including an explicit call-time dspy.PythonInterpreter. The constructor's interpreter_factory= API and default PythonInterpreter remain unchanged; context/global configuration still replaces the default when no call-time override is supplied.

Rename any RLM signature input named interpreter_factory. That name is now reserved for runtime configuration; declaring it as a signature input raises ValueError when constructing RLM.

This change applies to RLM only. CodeAct and ProgramOfThought retain their existing call-time APIs.

PR: #10493 by @isaacbmiller.

The Experimental 3.3 LM Types Are Replaced Now

The old dspy.LMRequest, dspy.LMResponse, and related experimental exports are removed. Importing dspy.core.types raises a migration error, and forward_contract="typed_lm" is rejected. These are replacements, not aliases:

Experimental 3.3 API 3.4 replacement
dspy.LMRequest, dspy.LMResponse dspy.lm15.Request, dspy.lm15.Response
dspy.LMMessage, dspy.LMConfig dspy.lm15.Message, dspy.lm15.Config
dspy.System(text) Request(system=text, ...)
dspy.User(text), dspy.Assistant(text) Message.user(text), Message.assistant(text)
response.outputs[0].parts response.message.parts

The new types are frozen dataclasses rather than Pydantic models, with different validation and shapes. Old pickles containing removed experimental classes are not automatically migrated; load and export them in their original environment first. Ordinary provider-response cache compatibility is separate and remains supported.

experimental=True no longer makes ordinary prompt calls return typed responses. Use an explicit request:

import dspy
from dspy.lm15 import Config, Message, Request

lm = dspy.LM("azure/your-deployment")

# Before: OpenAI-style messages, with a list result (deprecated).
outputs = lm(messages=[{"role": "user", "content": "What is DSPy?"}])

# After: a typed request and response.
response = lm(Request(
    model=lm.model,
    system="Be concise.",
    messages=(Message.user("What is DSPy?"),),
    config=Config(max_tokens=200),
))
print(response.text)

Use your configured provider/deployment and credentials. An explicit request's model must match the LM; put generation options in Config, since LM generation defaults are not added. Config.cache controls provider-side prompt caching, not DSPy's response cache. DSPy's signature types—such as Image, Audio, File, Tool, ToolCalls, and History—and public LM error classes remain supported.

Legacy LM Interfaces Are Deprecated Until the 3.5 Cutoff

The following still execute in 3.4 but emit DeprecationWarning and are scheduled for removal in 3.5:

  • Public OpenAI-style lm(messages=[...]) calls, including SDK message objects. Use explicit requests as above.
  • Custom BaseLM.forward() and aforward() integrations. Implement the engine interface.
  • LegacyEngine, AsyncLegacyEngine, and custom complete_legacy() shortcuts. These are transition tools, not permanent escape hatches.

lm("hello") stays supported and list-returning in 3.5. Built-in adapters still use an internal dictionary/list boundary in 3.4; their canonical request/response migration is scheduled for 3.5. Custom adapters must not rely on the removed dspy.clients.openai_format module.

A minimal replacement for a custom legacy LM is:

import dspy
from dspy.lm15 import Message, Response, Usage


class EchoEngine:
    def complete(self, request):
        return Response(
            id=None,
            model=request.model,
            message=Message.assistant("hello"),
            finish_reason="stop",
            usage=Usage(),
        )


lm = dspy.LM("custom/echo", engine=EchoEngine())
print(lm("hello"))

Enable migration warnings during development with python -W default::DeprecationWarning your_program.py. See the 3.5 cutoff.

Engine Behavior, Costs, and Saved State

  • Native n answers use separate sequential requests, potentially billing input tokens more than once. Use engine="litellm" for that backend's native n behavior.
  • Custom engine objects own their connection. Put api_key, api_base, timeout, and related client settings on the engine itself; DSPy rejects them on LM construction, copying, and calls with custom engines. copy(engine=...) replaces the sync/async engine pair.
  • Custom engines need importable classes and JSON-compatible dump_state()/load_state() implementations to save and restore. Restoring them requires allow_unsafe_lm_state=True; only enable it for trusted state.
  • Native timeouts bound individual transport waits, not total generation time. An httpx.Timeout component set to None selects LiteLLM under auto or raises under forced lm15, rather than silently substituting a finite timeout.
  • A stream is not retried after emitting a chunk. Incomplete responses are not cached as successful results. Rate-limit errors preserve retry hints, request IDs, and rate-limit headers when available.
  • Typed LiteLLM streaming supports Chat Completions, not Responses. Native Responses supports streaming and early stop matching; closing early can lose final usage and does not guarantee provider generation or billing stops. Typed LiteLLM Responses applies stops after the full reply and retains reported usage.
  • Unknown cost remains unknown rather than zero. Provider adaptations are recorded in LM history. Native listener chunks need not be LiteLLM class instances.

Other Compatibility Notes

  • Pydantic 2.11.0 is the minimum supported version. The new optional typesafe extra requires typesafe-sdk>=0.6.0,<1.0.0; it is not added to the base installation.
  • ReActV2 output fields cannot be named history or termination_reason; rename those fields to avoid metadata collisions.
  • Calling an async tool synchronously inside a running event loop raises ValueError, even with async/sync conversion enabled. Use await tool.acall(...).
  • Synchronous streaming propagates producer failures instead of silently ending. streamify unwraps a single nested task-group failure; multiple failures remain grouped.
  • Evaluate rejects empty development sets with a descriptive ValueError.
  • Native strict schemas derived from Pydantic models now require all properties as OpenAI expects. Caller-supplied raw schemas are left unchanged.
  • Cache implementations load only when their tiers are enabled, allowing both tiers to be disabled without constructing filesystem-backed cache storage. Exact embedding retrieval shares the corpus table across queries; its FAISS path is unchanged.

Full PR List

Jev, Decision Types, and Calibration

Language Models and Engines

  • Vendor lm15 as a package subtree by @MaximeRivest (#10359).
  • Run LM execution through lm15 engines and retire the experimental 3.3 types by @MaximeRivest (#10366).
  • Preserve Retry-After across bundled lm15 async and auxiliary calls by @MaximeRivest (#10371).
  • Update bundled lm15 and integrate native timeouts, stop handling, and error diagnostics by @MaximeRivest (#10409).
  • Update bundled lm15 with declared-provider support by @MaximeRivest (#10440).
  • Enforce custom-engine connection ownership and explicit state serialization by @MaximeRivest (#10441).
  • Register HTTP providers with per-LM bindings and connection-preserving fallback by @MaximeRivest (#10442).
  • Fix streamify error propagation, dead-loop pools, strict schemas, and scoped cloudpickle registrations by @MaximeRivest (#10451).
  • Sync bundled lm15, preserve structured judgment outputs, and retain rate-limit evidence at DSPy's public boundary by @isaacbmiller (#10469).

Agents, Tools, and Interpreters

Optimization, Evaluation, Retrieval, and Usage

  • Make Example hashing order-insensitive to match equality by @Kymi808 (#9858).
  • Preserve the input/label split when copying an Example by @iamsharduld (#9946).
  • Sample labeled demonstrations independently per BootstrapFewShot predictor by @iamsharduld (#9948).
  • Raise a descriptive error for empty evaluation sets by @roli-lpci (#9978).
  • Roll nested usage trackers into their parent on scope exit by @asparagus (#10065).
  • Add GEPA's custom Flex code_proposer hook by @dbreunig (#10212).
  • Keep GEPA trace-capture results aligned after program failures by @dbreunig (#10305).
  • Reduce exact embedding-retrieval memory use by sharing the corpus table across queries by @kiteretsu903 (#10398).

Adapters, Streaming, Imports, and Caching

Documentation

Dependencies, CI, and Release Engineering

Contributors

Thank you to @adriaanm, @asparagus, @chuenchen309, @dbreunig, @ellacroix, @he-yufeng, @iam-kira, @iamsharduld, @isaacbmiller, @kiteretsu903, @Kymi808, @MaximeRivest, @michaelisaac-dev, @nikolauspschuetz, @NishchayMahor, @NithilanVishvanath, @roli-lpci, @simpleqt, @spjosyula, @tjdharamsi, and @ymxlx.

Automation contributions were made by @dependabot and @github-actions.

Full Changelog: 3.3.1...3.4.0

Don't miss a new dspy release

NewReleases is sending notifications on new releases.