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: false must 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 return finish_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: waw2637@4978c1a
Related issues
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.
Summary
POST /v1/chat/completionsaccepts OpenAI-styletools,tool_choice, andparallel_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
0.9.46/v1/chat/completions, non-streamingparallel_tool_calls: falseMinimal reproduction
Start any tested model, for example:
Then send a required tool call with an intentionally small output budget:
Actual results
All three tool-capable model families returned HTTP 200 and
finish_reason: "tool_calls"for an invalid, truncated call:argumentsgemma4-it:e2bcall(not declared)"{}"qwen3-it:4bqwen3.5:2b"{}"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: withtool_choice: "required"and a normal token budget, FastFlowLM returned plain assistant text withfinish_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:
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.parallel_tool_calls: falsemust allow at most one call in the response.function.arguments.finish_reason: "length", or a clear model/server error. Do not returnfinish_reason: "tool_calls"with an invalid call.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: waw2637@4978c1a
Related issues
Those may improve how often a model emits valid calls, but they do not ensure that the REST API enforces
tool_choiceor fails closed when parsing produces an invalid call.