Skip to content

fix: return finish_reason=tool_calls when tool calls detected#990

Open
eloe wants to merge 2 commits intoBlaizzy:mainfrom
eloe:upstream/finish-reason-tool-calls
Open

fix: return finish_reason=tool_calls when tool calls detected#990
eloe wants to merge 2 commits intoBlaizzy:mainfrom
eloe:upstream/finish-reason-tool-calls

Conversation

@eloe
Copy link
Copy Markdown

@eloe eloe commented Apr 9, 2026

Summary

Per the OpenAI Chat Completions spec, finish_reason should be "tool_calls" when the model emits tool calls, not "stop". Strict OpenAI-compatible clients (including agent frameworks like LangChain, CrewAI, and others) use finish_reason == "tool_calls" as the branch condition for entering the tool-execution loop.

Currently both streaming and non-streaming paths hardcode "stop". This PR fixes both:

  • Non-streaming (ChatChoice): finish_reason = "tool_calls" if tool_calls.get("calls") else "stop"
  • Streaming (ChatStreamChoice): Final chunk uses "tool_calls" finish_reason when tools detected

Tests

4 tests covering all combinations:

  • test_chat_completions_finish_reason_stop_no_tools — plain text, no tools defined
  • test_chat_completions_finish_reason_tool_calls — non-streaming with tool calls
  • test_chat_completions_finish_reason_stop_tools_no_calls — tools defined but model doesn't call any
  • test_chat_completions_streaming_finish_reason_tool_calls — streaming with tool calls
python -m pytest mlx_vlm/tests/test_server.py -k "finish_reason" -v

Related

This overlaps with #964 by @michaelstingl which addresses the non-streaming path. This PR additionally covers the streaming path and includes 4 tests (streaming + non-streaming, with and without tool calls).

eloe and others added 2 commits April 8, 2026 20:28
OpenAI-compatible clients check finish_reason to decide whether to
enter the tool execution loop. Previously mlx-vlm always returned
"stop" even when process_tool_calls found calls in the model output.

Now both streaming and non-streaming /chat/completions responses
return finish_reason="tool_calls" when tool calls are present,
and "stop" otherwise.

Adds 3 tests covering: stop without tools, tool_calls with tools,
stop with tools but no calls made.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove unused asyncio import and add streaming test for
finish_reason=tool_calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant