MTPLX 2.3.0
This is the agent reliability release. The tool-call argument corruption
that plagued coding agents is root-caused and fixed, structured output
arrives with full speculative speed, agent sessions stop wasting time on
re-prefills and runaway reasoning, and a batch of hardening from an
independent source review closes real correctness holes. Much of this
release is community work: reported, diagnosed, and in several cases
authored by contributors. Thank you.
Numbering note: this cycle was staged as 2.2.1, but it grew well past a
patch. If you are on 2.2.0, everything below is new.
Structured output (#186, #187, #188)
response_format with json_object or json_schema is now enforced,
not silently ignored. Grammar masks from llguidance constrain decoding
so the response is valid JSON matching your schema. Designed and
contributed by @PhilipJohnBasile across three staged PRs.
- The grammar composes with MTP speculative decoding instead of turning
it off. Constrained requests keep full speed: decode parity is within
noise of unconstrained runs and the total masking cost is under five
milliseconds per request. We verified the sampling math end to end,
including a distributional check of the masked law through the real
sampler.
- On thinking models the grammar allows reasoning to finish before the
document is forced. Practical note: reasoning can consume your
max_tokens before the document starts, so for tight budgets pair
structured output with --reasoning off or a larger budget.
- Opt-in strict tool calls: launch with
MTPLX_TOOL_CALL_STRICT=1 and
every tool call is grammar-forced to a declared tool name with
schema-valid JSON arguments. A real OpenCode session built a complete
project through the strict lane with zero malformed calls.
- Structured output needs the llguidance package, which ships with the
[server] extra and inside the desktop app. A bare pip install
mtplx returns a clear 400 with an install hint if you request it.
Tool calls that hold together (#170, #190)
- Fixed the intermittent collapse of tool-call arguments to
{} on
Qwen3.6, the bug behind failed edits in OpenCode and similar agents
(about one in ten edit turns). The streaming parser silently discarded
function-body text that was not wrapped in parameter tags, and a
second lane fabricated empty-argument calls from bodies it could not
read. Both parsers now share one strict contract, and a model that
writes its arguments as a plain JSON object inside the tool envelope
gets its arguments delivered intact. Validated with a 168-turn live
A/B: zero collapses, where the old code reproduced them. Thanks
@druide67 for the report and the frequency estimate that matched our
measurements.
- The injected tool contract no longer shows empty
[] examples for
array parameters, which was teaching models the exact degenerate shape
we did not want.
parallel_tool_calls is honored. Declare false and you get at most
one tool call per turn; the old client-name heuristic now only applies
when the field is absent. Contributed by @PhilipJohnBasile. Android
Studio declares this field today and was being ignored.
Agent sessions and wall time
- Agent turns got dramatically leaner. The injected tool contract now
instructs whole-file reads and forbids echoing file contents into
visible text. In live agent sessions this eliminated double-emitted
file bodies and micro-read storms, and cut the reasoning share of
generated tokens from 75 percent to under 30.
- Sessions survive client history rewrites. When an agent client
compacts or rewrites its transcript, the session used to rotate to a
fresh anonymous identity and lose its warm cache, forcing cold
full-context re-prefills mid-session. A common-prefix fallback now
keeps the session identity when at least 4096 tokens and a quarter of
the prompt still match.
- An optional reasoning budget for agent turns:
--agent-thinking-budget
(env MTPLX_THINKING_BUDGET) force-closes a runaway thinking segment
at a token budget so the turn proceeds to its answer or tool call.
Off by default; nothing touches generation unless you opt in. Every
engagement is surfaced in telemetry.
- New diagnostics for agent workloads:
--request-log-jsonl appends
every request record as a durable JSON line, and
scripts/session_forensics.py correlates that log with an OpenCode
database into one timeline, with detectors for re-prefill rewinds,
stalls, thinking marathons, double emissions, and session identity
churn.
Hardening from the independent source review (#86, #103, #107, #182)
- Context-copy drafting no longer accepts past a stop token. A full
block accept could advance the cache and model state beyond the
emitted response and poison the session cache for the next turn on
recurrent models. Copy blocks are also strictly prompt-only now, so
the lane never drafts from the model's own output.
- Silent stream hangs are contained (#86). If a stream receives nothing
while the engine heartbeat is frozen for five minutes, the request
fails with a structured, diagnosable error instead of hanging forever.
Healthy long prefills never trip it, and the daemon is never killed.
- Long-running daemons no longer accumulate unbounded telemetry. The
session-bank eviction log, scheduler counters, and per-session
dashboards are all bounded now.
- Vision works on a bare
pip install mtplx (#103): Pillow moved from
the server extra into the base dependencies to match the advertised
image support.
- The desktop app recognizes assistant-pair model repos the same way the
Python runtime does (#107), so the official Gemma4 pair repos classify
as ready to download instead of unreadable.
- Models quantized at 5-bit and other widths that do not divide 32 now
load correctly. The group-size inference used floor division per
packed word and produced impossible group sizes. Diagnosed and fixed
by @Jonathangadeaharder, validated down to the MLX packing layer.
mtplx doctor --json output is guaranteed machine-parseable even when
third-party libraries print their own errors during import.
Contributor fixes (#178, #179, #180, #181)
- Interrupted model downloads are detected as incomplete instead of
being treated as ready. By @titan550.
- Fan restore goes through the ThermalForge daemon socket instead of a
CLI path that could kill the app, plus a fan-control code cleanup.
By @titan550.
Under the hood
- The runtime now stamps its real version everywhere, including forge
provenance. A version-skew bug class where an older engine silently
ignored newer recipe fields was diagnosed this cycle through exactly
that stamp (#184), and the fix work is queued.
- The agent wall-time lab used to validate this release ships in
scripts/walltime-lab/: a harness that drives real OpenCode builds
against a daemon and scores completion, useful for reproducing agent
performance reports.