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.
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()andaforward()integrations. Implement the engine interface. LegacyEngine,AsyncLegacyEngine, and customcomplete_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
nanswers use separate sequential requests, potentially billing input tokens more than once. Useengine="litellm"for that backend's nativenbehavior. - 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 requiresallow_unsafe_lm_state=True; only enable it for trusted state. - Native timeouts bound individual transport waits, not total generation time. An
httpx.Timeoutcomponent set toNoneselects LiteLLM underautoor raises under forcedlm15, 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
typesafeextra requirestypesafe-sdk>=0.6.0,<1.0.0; it is not added to the base installation. - ReActV2 output fields cannot be named
historyortermination_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. Useawait tool.acall(...). - Synchronous streaming propagates producer failures instead of silently ending.
streamifyunwraps a single nested task-group failure; multiple failures remain grouped. Evaluaterejects empty development sets with a descriptiveValueError.- 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
- Add experimental
Noul,Choice, andScoretypes and the optional TypeSafe/Jev client by @isaacbmiller (#10463). - Add the experimental ReAnchor calibration optimizer for Predict by @isaacbmiller and @dbreunig (#10475).
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-Afteracross 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
- Reject reserved ReActV2 output names by @tjdharamsi (#9852).
- Preserve output capture for single-line ProgramOfThought code by @chuenchen309 (#10050).
- Reject synchronous async-tool calls inside running event loops by @isaacbmiller (#10146).
- Add persistent LocalInterpreter by @isaacbmiller (#10238).
- Recommend ReActV2 in optimizer-authored Flex guidance by @michaelisaac-dev (#10265).
- Honor positive odd RLM truncation limits by @isaacbmiller (#10340).
- Raise when ReActV2 cannot submit final outputs by @isaacbmiller (#10355).
- Add async ReActV2 execution and MCP coverage by @isaacbmiller (#10356).
- Propagate LocalInterpreter host-tool cancellation and interrupts by @isaacbmiller (#10379).
- Allow global interpreter-factory configuration to replace the default PythonInterpreter by @adriaanm (#10383).
- Replace RLM's live-interpreter call-time override with keyword-only
interpreter_factory=and reserve that input name by @isaacbmiller (#10493).
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_proposerhook 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
- Propagate synchronous streaming errors by @he-yufeng (#9864).
- Use the correct base64 source type for PDF documents by @NishchayMahor (#9977).
- Coerce non-string Literal members during parsing by @spjosyula (#10010).
- Honor the remaining batch deadline in Unbatchify instead of resetting its timeout on each queue read by @chuenchen309 (#10038).
- Quote only string Literal members in BAMLAdapter schemas by @nikolauspschuetz (#10074).
- Defer AnyIO, OpenAI, and jiter imports to their use sites by @adriaanm (#10381).
- Allow disabled cache tiers without constructing their backends by @adriaanm (#10382).
- Drop
tool_choicewhen no tools are sent to the LM by @NithilanVishvanath (#10399).
Documentation
- Correct the documented ReAct
max_itersdefault by @ellacroix (#10013). - Explain GEPA training/validation roles and dataset splits by @ymxlx (#10048).
- Update homepage release status by @isaacbmiller (#10274).
- Add Mike versioning and historical documentation snapshots by @isaacbmiller (#10315).
- Migrate Current documentation to native Zensical by @isaacbmiller (#10316).
- Automate Current updates and post-publication release snapshots by @isaacbmiller (#10317).
- Remove one-time documentation migration scaffolding and legacy publication paths by @isaacbmiller (#10332).
- Correct CodeAct's stale constructor docstring by @simpleqt (#10351).
- Pin historical release badges by @isaacbmiller (#10413).
- Add a maintainer-guided release-notes skill by @isaacbmiller (#10414).
- Reposition the version picker in the header by @isaacbmiller (#10418).
- Polish the version picker and repository metadata by @isaacbmiller (#10421).
- Align the version picker with search by @isaacbmiller (#10438).
- Serve versioned documentation from the static root by @isaacbmiller (#10447).
- Preserve versioned documentation trailing slashes by @isaacbmiller (#10448).
Dependencies, CI, and Release Engineering
- Update main's version metadata for 3.3.1 by @github-actions (#10260).
- Require Pydantic 2.11 by @isaacbmiller (#10271).
- Update denoland/setup-deno from 2.0.4 to 2.0.5 by @dependabot (#10288).
- Update astral-sh/setup-uv from 8.2.0 to 10.0.1 by @dependabot (#10289).
- Update pypa/gh-action-pypi-publish from 1.14.0 to 1.14.2 by @dependabot (#10290).
- Update actions/setup-python from 6.2.0 to 7.0.0 by @dependabot (#10291).
- Update locked orjson from 3.11.9 to 3.12.0 by @dependabot (#10292).
- Update zizmor-action from 0.5.7 to 0.6.2 by @dependabot (#10293).
- Update locked json-repair from 0.63.0 to 0.63.3 by @dependabot (#10295).
- Update documentation Mistune from 3.2.1 to 3.3.4 by @dependabot (#10298).
- Update zizmor-action from 0.6.2 to 0.6.3 by @dependabot (#10369).
- Install pytest-mock in release test environments by @isaacbmiller (#10384).
- Update main's version metadata for 3.4.0b1 by @github-actions (#10385).
- Install uv for documentation bootstrap by @isaacbmiller (#10403).
- Stabilize the local-interpreter timeout test by @isaacbmiller (#10404).
- Keep documentation bootstrap reviewable and version-correct by @isaacbmiller (#10405).
- Skip alias-collision testing when symlink creation is unavailable by @iam-kira (#10410).
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