Bug: tool_choice is not enforced and malformed tool calls are returned as successful responses
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 1.9k
- Forks
- 152
- Avg merge
- 4h 14m
- Merged PRs (30d)
- 11
Description
Summary
POST /v1/chat/completions accepts OpenAI-style tools, tool_choice, and parallel_tool_calls, but FastFlowLM does not enforce the requested tool-selection policy or validate the parsed tool call before returning it.
Across Gemma 4 and two Qwen families, deliberately truncating a required tool call produces HTTP 200 with finish_reason: "tool_calls" even though the function name and/or arguments are invalid. A client will attempt to dispatch these fabricated calls and fail downstream.
This is distinct from model quality: the server owns the API contract and should never label an unusable parser result as a completed tool call.
Environment
- FastFlowLM: stock Linux release
0.9.46 - Hardware: AMD Ryzen AI NPU / XDNA2
- Endpoint:
/v1/chat/completions, non-streaming - Context length: 8192 for the reproduction matrix
- Temperature: 0.1
parallel_tool_calls: false- Test date: 2026-08-03
Minimal reproduction
Start any tested model, for example:
flm serve qwen3-it:4b --ctx-len 8192 --port 52625
Then send a required tool call with an intentionally small output budget:
curl -sS http://127.0.0.1:52625/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen3-it:4b",
"messages": [
{"role": "user", "content": "Get the weather for Miami using the function."}
],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a location",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"]
}
}
}],
"tool_choice": "required",
"parallel_tool_calls": false,
"temperature": 0.1,
"max_tokens": 1
}'
Actual results
All three tool-capable model families returned HTTP 200 and finish_reason: "tool_calls" for an invalid, truncated call:
| Model | Returned function | Returned arguments |
|---|---|---|
gemma4-it:e2b |
call (not declared) |
"{}" |
qwen3-it:4b |
empty string | empty string |
qwen3.5:2b |
empty string | "{}" |
Representative Qwen response:
{
"choices": [{
"message": {
"role": "assistant",
"tool_calls": [{
"type": "function",
"function": {"name": "", "arguments": ""}
}]
},
"finish_reason": "tool_calls"
}]
}
There is a second policy-enforcement symptom on gemma3:1b: with tool_choice: "required" and a normal token budget, FastFlowLM returned plain assistant text with finish_reason: "stop". If that model does not support tools, the server should reject the request rather than silently ignore the required policy.
A longer Gemma 4 workflow also leaked raw template syntax as ordinary content instead of a structured call:
list directory{path:<|"|>.<|"|>}<tool_call|>
Expected behavior
tool_choice: "none"must prevent tool calls.tool_choice: "required"must not return a successful response without at least one valid declared tool call.- A named function choice must call exactly that declared function.
parallel_tool_calls: falsemust allow at most one call in the response.- Every returned call must name a declared function and contain a JSON-encoded object in
function.arguments. - If generation is truncated or parsing fails, return a non-dispatchable result such as
finish_reason: "length", or a clear model/server error. Do not returnfinish_reason: "tool_calls"with an invalid call. - If a model lacks tool support, reject requests that require tool use with a clear error.
Cross-check
The same test corpus includes a normal required call and a three-turn chain (read_dataset -> calculate_total -> final answer). Those succeed on the tested Gemma 4 and Qwen models, demonstrating that the models and parsers can produce valid calls when generation completes. The defect is that invalid results are accepted without enforcing the request contract.
A local response-validation prototype was also tested with gemma4-it:e4b; it passed the normal required call, fail-closed truncation, multi-turn chain, and a longer skill workflow. That suggests this can be fixed centrally in the OpenAI REST boundary instead of with per-model argument repair.
Prototype commit: https://github.com/waw2637/FastFlowLM/commit/4978c1a18fddd235e438e8ffd063083617395adb
Related issues
- #537 covers argument normalization/serialization.
- #559 and #641 cover model workflow/output quality.
- #634 covers updated Gemma 4 templates.
Those may improve how often a model emits valid calls, but they do not ensure that the REST API enforces tool_choice or fails closed when parsing produces an invalid call.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at the OpenAI REST boundary handling POST /v1/chat/completions and review prototype commit 4978c1a18fddd235e438e8ffd063083617395adb. Reproduce the curl case and the stated tool-call corpus, then verify that tool_choice, parallel_tool_calls, declared function names, and JSON arguments are enforced and invalid or truncated results fail closed rather than returning finish_reason "tool_calls".
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100