fix(server): flush tool call when generation ends in "tool" state (Mistral/Devstral) - #1406
Closed
rinaldofesta wants to merge 1 commit into
Closed
fix(server): flush tool call when generation ends in "tool" state (Mistral/Devstral)#1406rinaldofesta wants to merge 1 commit into
rinaldofesta wants to merge 1 commit into
Conversation
Models whose tool_call_end is empty (Mistral / Devstral, format
[TOOL_CALLS]<name>[ARGS]{json}) never transition the state machine back
to "normal" — generation finishes while still in the "tool" state. The
end-of-generation flush was guarded on `prev_state == "tool"`, but the
terminal generation event resets prev_state, so the accumulated
`tool_text` was never appended to `tool_calls`. The server then returned
an empty assistant message (no content, no tool_calls) even though the
model emitted a valid tool call.
Flush whenever `tool_text` is non-empty (it is only ever populated while
in the "tool" state and is cleared on the tool->normal transition, so
this is safe for models that do close their tool calls).
Verified end-to-end against a Devstral-Small-2 server (mlx_lm.server):
before, tool calls were dropped (empty message); after, both streaming
and non-streaming return the structured tool_call.
Adds a regression test that drives the request handler with a stream
ending in the "tool" state.
Member
|
Closed due to #1501. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The OpenAI server drops tool calls for models whose
tool_call_endis empty — notably Mistral / Devstral, which emit[TOOL_CALLS]<name>[ARGS]{json}. The response comes back as an empty assistant message (nocontent, notool_calls) even though the model generated a perfectly valid tool call.Root cause
For these models the state machine never transitions back to
"normal"(there is no closing tool token), so generation finishes while still in the"tool"state. InAPIHandler.handle_completionthe end-of-generation flush is guarded onprev_state == "tool":But the terminal generation event resets
prev_state(it isNoneby the time the loop exits), so the accumulatedtool_textis never appended totool_calls. TheToolCallFormatterthen receives an empty list and emits nothing.I confirmed this with a debug print inside the flush:
tool_textis collected correctly (with[ARGS]intact) — the only problem is the guard.Fix
Flush whenever
tool_textis non-empty.tool_textis only ever populated while in the"tool"state and is reset to""on thetool -> normaltransition, so this is also correct for models that do close their tool calls (no double flush):Verification
End-to-end against a real
mlx_lm.serverservingmlx-community/Devstral-Small-2-24B-Instruct-2512-4bit:finish_reason: "stop", empty message,tool_callsabsent (both streaming and non-streaming).finish_reason: "tool_calls"with the structured call — non-streaming{"name":"read","arguments":"{\"file_path\": \"/tmp/foo.txt\"}"}, and the streaming deltas assemble to the same.Added
tests/test_server.py::TestMistralToolStateFlush, which drives the request handler with a generation stream ending in the"tool"state (the terminal event carriesstate=None, reproducing the reset). It fails onmainand passes with the fix.AI assistance disclosure
Diagnosed and implemented with AI assistance (Claude). I reproduced the bug against a live Devstral server, traced the root cause with an instrumented build, and verified the fix end-to-end (streaming + non-streaming) and via the new regression test before submitting. Happy to adjust the test harness to your preferred style.