Skip to content

Bug: tool_choice is not enforced and malformed tool calls are returned as successful responses #649

Description

@waw2637

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions