DSPy 3.4.0 Beta 1
DSPy 3.4.0b1 is the first beta of 3.4. It moves language-model execution to a shared engine interface, adds a local CPython interpreter for trusted code, and brings async execution to ReActV2. It also adds custom Flex code proposals in GEPA and fixes evaluation, streaming, and demonstration-sampling bugs.
This is a prerelease, not the stable 3.4.0 release. APIs and behavior may change before stable. We especially welcome feedback on native versus LiteLLM compatibility, tool calling and streaming, custom LM migration, saved programs, and multi-answer latency and costs. Please include your engine selection and a minimal reproduction when reporting issues.
Install this beta explicitly:
pip install --upgrade "dspy==3.4.0b1"3.4 is the LM transition release; 3.5 is the migration deadline. Ordinary DSPy programs and list-returning prompt calls remain supported, but the experimental LM types introduced in 3.3 are replaced in this release. Review the compatibility notes if you use those types, implement a custom LM, pass OpenAI-style messages directly to an LM, or request multiple answers with n.
Highlights
Native LM Engines and a New Custom-Backend Interface — @MaximeRivest
DSPy's LM layer now uses the lm15 request, response, and streaming types bundled with DSPy. Import them from dspy.lm15; no separate lm15 installation is needed.
The default engine="auto" prefers native lm15 execution for supported routes and representable inputs. Unsupported routes, client settings, and ordinary provider-specific inputs that cannot be represented faithfully select LiteLLM before execution. Authentication failures, timeouts, and provider errors do not trigger a switch to another backend.
import dspy
# Prefer native execution where supported; select compatibility where needed.
lm = dspy.LM("openai/gpt-4o-mini")
# Explicitly retain the LiteLLM compatibility backend.
compat_lm = dspy.LM("openai/gpt-4o-mini", engine="litellm")Use engine="lm15" to require native execution and reject unsupported mappings. LiteLLM remains installed and supported; it is not deprecated.
Custom backends can now implement complete(Request) -> Response instead of subclassing BaseLM and returning provider-shaped objects:
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())Supply async_engine= for async calls and implement canonical streaming events when needed. DSPy owns response caching, managed retries, callbacks, history, and usage accounting. Custom engines are caller-owned and are not closed by DSPy.
Ordinary calls retain their existing cache-key format and can read existing SDK-response cache entries. New native cache entries contain plain serialized data and support restricted deserialization; explicit typed requests use a separate cache namespace. Existing dspy.streamify listeners remain supported.
See the LM migration guide and custom-engine tutorial.
LocalInterpreter: Persistent CPython for Trusted Code — @isaacbmiller
dspy.LocalInterpreter runs generated Python in a persistent local CPython subprocess, using the current Python executable. It provides ordinary Python compatibility without Deno, while separating the worker's memory, stdout, and lifecycle from the DSPy process.
rlm = dspy.RLM(
"question: str -> answer: int",
interpreter_factory=dspy.LocalInterpreter,
)State and imports persist within an interpreter session. LocalInterpreter supports JSON-compatible inputs and tool results, sync and async host tools, typed SUBMIT, interpreter callbacks, and execution timeouts. It works with both RLM and Flex. The usual factory lifecycle still creates a fresh interpreter for each module invocation.
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 a remote sandbox for untrusted code; the default has not changed.
execution_timeout includes host-tool time and terminates the worker when exceeded. It cannot forcibly stop a running host callable; that callable may finish later, but its result is discarded. Guest threads must finish before an execution returns, or the interpreter session becomes terminal.
Host-tool cancellation and interrupts propagate to the caller and shut down the stranded worker, including when no execution timeout is configured. They no longer leave execution waiting indefinitely for a worker reply. Ordinary tool errors remain recoverable.
Async ReActV2 and Explicit Final-Output Failures — @isaacbmiller
The experimental dspy.ReActV2 now supports await agent.acall(...), including async prediction, async tools, and forced final submission. This enables async MCP tools to run through the agent's structured tool-call history, preserving call IDs and tool results across turns.
Tools execute sequentially on the async path. This does not add parallel tool execution or automatically offload blocking synchronous tools.
ReActV2 also no longer returns an incomplete Prediction when final submission fails. Missing or invalid submission raises ValueError; parse and context-window failures from the forced submission propagate. Successful predictions continue to include declared outputs, history, and a termination reason.
The MCP guide includes an async ReActV2 client/server example.
Custom Flex Code Proposals and More Reliable GEPA Evaluation — @dbreunig
dspy.GEPA now accepts code_proposer=, complementing instruction_proposer=. Custom proposers receive the selected Flex code components, candidate source, reflective examples, task descriptions, and context blurbs, and return replacement module source for each component.
This lets applications customize code-generation constraints and domain guidance without monkeypatching the built-in proposer. The built-in proposer remains the default, and programs without Flex components are unaffected.
GEPA's trace-capture evaluation also keeps outputs and scores aligned with the input batch when a program crashes on an example. Failed examples receive failure_score rather than disappearing and causing an indexing error or shifting later results. This addresses the missing/misaligned validation-results issue noted in the 3.3.1 release; it does not require an upstream GEPA upgrade.
API and Compatibility Changes
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, not Pydantic models, and their validation and data shapes differ. Old pickles containing removed experimental classes are not automatically migrated; load and export them in their original environment first. Ordinary provider-response caches are a separate compatibility path and remain readable.
experimental=True no longer changes ordinary LM calls into typed responses. Ordinary prompt calls return lists; explicit requests return a Response:
from dspy.lm15 import Config, Message, Request
lm = dspy.LM("openai/gpt-4o-mini")
response = lm(Request(
model=lm.model,
system="Be concise.",
messages=(Message.user("What is DSPy?"),),
config=Config(max_tokens=200),
))
print(response.text)An explicit request's model must match the LM. Set generation options in its Config; LM generation defaults are not added. Config.cache controls provider-side prompt caching, not DSPy's response cache.
DSPy's signature types, including Image, Audio, File, Tool, ToolCalls, and History, are not removed. Existing public DSPy LM error classes remain supported.
Legacy LM Integrations Are Deprecated for Removal in 3.5
The following still execute in 3.4 but emit DeprecationWarning:
- Public OpenAI-style
lm(messages=[...])calls, including provider SDK message objects. Migrate to explicit lm15 requests. - Custom
BaseLM.forward()andaforward()integrations. Migrate to the engine interface. LegacyEngine,AsyncLegacyEngine, and customcomplete_legacy()shortcuts. These are transition tools, not permanent compatibility interfaces.
lm("hello") remains a list-returning convenience 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 depend on the removed dspy.clients.openai_format module.
To reveal deprecation warnings during development:
python -W default::DeprecationWarning your_program.pyNative Multi-Answer Calls, Retries, and Streaming
- Native
nanswers use separate sequential requests. This can increase latency and bill input tokens more than once compared with a provider-native multi-answer request. Chooseengine="litellm"to retain that backend's nativenbehavior. - DSPy owns managed retries and honors valid provider retry hints. A stream is not retried after a chunk has been emitted, and incomplete responses are not cached as successful results.
- Native cost estimates remain unknown when reliable pricing is unavailable; unknown cost is not reported as zero.
- Typed LiteLLM streaming currently supports Chat Completions only. Native Responses streaming is supported.
- Native listener-facing chunks need not be LiteLLM class instances. Custom chunk consumers should not rely on that identity.
PRs for the LM changes above: #10366, #10371
Other Compatibility Changes
- Pydantic 2.11.0 is now the minimum supported version. #10271
- ReActV2 rejects signatures with output fields named
historyortermination_reason, which collide with its prediction metadata. Rename those outputs. #9852 - Calling an async tool synchronously from a running event loop raises a descriptive
ValueError, even withallow_tool_async_sync_conversionenabled. Useawait tool.acall(...). #10146 - Synchronous streaming now propagates producer failures to the consumer instead of silently ending. #9864
Evaluaterejects an empty development set with a descriptiveValueErrorinstead of failing withZeroDivisionError. #9978
Full PR List
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).
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 module 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 end-to-end coverage by @isaacbmiller (#10356).
- Propagate LocalInterpreter host-tool cancellation and interrupts instead of hanging by @isaacbmiller (#10379).
Optimization, Evaluation, and Examples
- Make
Examplehashing order-insensitive to match equality by @Kymi808 (#9858). - Preserve the input/label split when copying an
Exampleby @iamsharduld (#9946). - Sample labeled demonstrations independently from the full available pool for each BootstrapFewShot predictor by @iamsharduld (#9948).
- Raise a descriptive error for empty evaluation sets by @roli-lpci (#9978).
- Add GEPA's custom Flex
code_proposerhook by @dbreunig (#10212). - Keep GEPA trace-capture results aligned after program failures by @dbreunig (#10305).
Adapters, Streaming, and Batching
- Propagate synchronous streaming errors by @he-yufeng (#9864).
- Use the correct base64 source type for PDF documents by @NishchayMahor (#9977).
- Coerce non-string
Literalmembers during parsing by @spjosyula (#10010). - Honor the remaining batch deadline in
Unbatchifyinstead of resetting the full timeout on every queue read by @chuenchen309 (#10038). - Quote only string
Literalmembers in BAMLAdapter schemas by @nikolauspschuetz (#10074).
Documentation
- Correct the documented ReAct
max_itersdefault by @ellacroix (#10013). - Explain GEPA training/validation roles and dataset-split guidance by @ymxlx (#10048).
- Update homepage release status by @isaacbmiller (#10274).
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-denofrom 2.0.4 to 2.0.5 by @dependabot (#10288). - Update
astral-sh/setup-uvfrom 8.2.0 to 10.0.1 by @dependabot (#10289). - Update
pypa/gh-action-pypi-publishfrom 1.14.0 to 1.14.2 by @dependabot (#10290). - Update
actions/setup-pythonfrom 6.2.0 to 7.0.0 by @dependabot (#10291). - Update locked
orjsonfrom 3.11.9 to 3.12.0 by @dependabot (#10292). - Update
zizmorcore/zizmor-actionfrom 0.5.7 to 0.6.2 by @dependabot (#10293). - Update locked
json-repairfrom 0.63.0 to 0.63.3 by @dependabot (#10295). - Update documentation Mistune from 3.2.1 to 3.3.4 by @dependabot (#10298).
Contributors
Thank you to @chuenchen309, @dbreunig, @ellacroix, @he-yufeng, @iamsharduld, @isaacbmiller, @Kymi808, @MaximeRivest, @michaelisaac-dev, @nikolauspschuetz, @NishchayMahor, @roli-lpci, @spjosyula, @tjdharamsi, and @ymxlx for contributing to this release.
Automation contributions were made by @dependabot and @github-actions.
Full Changelog: 3.3.1...3.4.0b1