n8n - 💡(How to fix) Fix AI Agent v3 does not set tool_choice on iteration 1, causing small models on OpenAI-compatible providers to skip tool calls

Official PRs (…)
ON THIS PAGE

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Root Cause

n8n's tools agent omits tool_choice from the request body on iteration 1. For OpenAI-compatible providers, omitting tool_choice is equivalent to "auto". Smaller models without strong tool-calling priors interpret "auto" as "you don't have to" and skip the call.

Setting tool_choice: "required" on iteration 1 only (then letting it revert to "auto" for iteration 2+ so the model can produce the final answer) deterministically fixes this.

Fix Action

Workaround

HTTP proxy in front of the model endpoint that injects tool_choice: "required" on iteration 1. Required for production use with any non-OpenAI provider in our deployment.

Code Example

{
  "model": "mistralai/Mistral-Small-24B-Instruct",
  "messages": [
    {
      "role": "system",
      "content": "You MUST call the `get_value` tool to answer any question. Do not answer without calling it."
    },
    {
      "role": "user",
      "content": "What is the value?"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_value",
        "description": "Returns the value.",
        "parameters": {
          "type": "object",
          "properties": {},
          "required": []
        }
      }
    }
  ],
  "n": 1,
  "temperature": 0.7,
  "stream": false
}

---

{
  "model": "mistralai/Mistral-Small-24B-Instruct",
  "messages": [ ... same ... ],
  "tools": [ ... same ... ],
  "tool_choice": "required",
  "n": 1,
  "temperature": 0.7,
  "stream": false
}

---

def should_force_tool_use(data: dict) -> bool:
    """True if this is iteration 1 of an agent loop and we should override
    tool_choice to 'required' to stop small models confabulating instead
    of calling the wired tools."""
    tools = data.get("tools") or []
    if not tools:
        return False
    existing = data.get("tool_choice")
    # Don't override if caller explicitly set something non-default.
    if existing and existing != "auto":
        return False
    # If any prior assistant turn already called a tool, we're past iteration 1
    # — let the model produce a final answer this turn.
    for msg in data.get("messages") or []:
        if msg.get("role") == "assistant" and (
            msg.get("tool_calls") or msg.get("function_call")
        ):
            return False
    return True


# In the request handler, after parsing JSON body:
if should_force_tool_use(data):
    data["tool_choice"] = "required"
RAW_BUFFERClick to expand / collapse

Bug description

n8n AI Agent (typeVersion 3) does not set tool_choice: "required" on iteration 1 when sending requests to OpenAI-compatible providers.

For native OpenAI / Anthropic, this doesn't matter — the model calls a tool eagerly. For OpenAI-compatible endpoints (vLLM, IONOS, Infomaniak, LM Studio, Ollama) serving smaller models that lack a strong tool-calling prior — e.g. Mistral-Small-24B — the model defaults to text completion and ignores the system-message instruction to invoke a tool, even when the tools[] array and a clear instruction are present.

The result: the agent confabulates an answer instead of calling the wired tool. The tool node never executes.

Real-world impact

This was originally observed in a production RAG workflow where the AI Agent is wired to a Qdrant vector store retrieval tool (Search_Knowledge_Base). Instead of querying the knowledge base, the model confabulates answers from its training data — producing plausible-sounding but factually wrong responses on niche-domain queries (in our case, a small German civic-tech platform not present in any model's training corpus).

The minimal reproduction below uses a trivial fake tool because the bug is independent of tool type — but the real cost of the bug is silently broken RAG.

To reproduce

  1. Create a minimal workflow:
    • Chat Trigger
    • AI Agent (typeVersion 3)
    • One trivial tool (e.g. a Code tool that returns a constant string)
    • OpenAI Chat Model (lmChatOpenAi, typeVersion 1.3, responsesApiEnabled: false) pointed at any OpenAI-compatible endpoint serving a small model — I used Mistral-Small-24B-Instruct on IONOS.
  2. Set the agent's system message to: You MUST call the get_value tool to answer any question. Do not answer without calling it.
  3. Send a chat message: What is the value?
  4. Observe: the model returns prose. The tool is never invoked.

Expected behavior

The agent invokes the tool. (Confirmed: with tool_choice: "required" forced into the outgoing request, the model calls the tool every time.)

Root cause

n8n's tools agent omits tool_choice from the request body on iteration 1. For OpenAI-compatible providers, omitting tool_choice is equivalent to "auto". Smaller models without strong tool-calling priors interpret "auto" as "you don't have to" and skip the call.

Setting tool_choice: "required" on iteration 1 only (then letting it revert to "auto" for iteration 2+ so the model can produce the final answer) deterministically fixes this.

Evidence

Outgoing request body from n8n on iteration 1 — structurally what n8n sends for the minimal reproduction above. Note tool_choice is absent:

{
  "model": "mistralai/Mistral-Small-24B-Instruct",
  "messages": [
    {
      "role": "system",
      "content": "You MUST call the `get_value` tool to answer any question. Do not answer without calling it."
    },
    {
      "role": "user",
      "content": "What is the value?"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_value",
        "description": "Returns the value.",
        "parameters": {
          "type": "object",
          "properties": {},
          "required": []
        }
      }
    }
  ],
  "n": 1,
  "temperature": 0.7,
  "stream": false
}

With tool_choice: "required" injected by a proxy on iteration 1 — same body, single extra field:

{
  "model": "mistralai/Mistral-Small-24B-Instruct",
  "messages": [ ... same ... ],
  "tools": [ ... same ... ],
  "tool_choice": "required",
  "n": 1,
  "temperature": 0.7,
  "stream": false
}

Model behavior:

  • Without injection: returns prose, no tool call (consistent across 10+ runs).
  • With injection: calls the tool every time.

The fix is contained in ~20 lines of proxy code: detect iteration 1 (tools[] present, no prior assistant turn with tool_calls), set tool_choice = "required" if it wasn't explicitly set.

<details> <summary>Proxy code (Python / FastAPI) — the load-bearing logic</summary>
def should_force_tool_use(data: dict) -> bool:
    """True if this is iteration 1 of an agent loop and we should override
    tool_choice to 'required' to stop small models confabulating instead
    of calling the wired tools."""
    tools = data.get("tools") or []
    if not tools:
        return False
    existing = data.get("tool_choice")
    # Don't override if caller explicitly set something non-default.
    if existing and existing != "auto":
        return False
    # If any prior assistant turn already called a tool, we're past iteration 1
    # — let the model produce a final answer this turn.
    for msg in data.get("messages") or []:
        if msg.get("role") == "assistant" and (
            msg.get("tool_calls") or msg.get("function_call")
        ):
            return False
    return True


# In the request handler, after parsing JSON body:
if should_force_tool_use(data):
    data["tool_choice"] = "required"
</details>

Workaround

HTTP proxy in front of the model endpoint that injects tool_choice: "required" on iteration 1. Required for production use with any non-OpenAI provider in our deployment.

Related closed/stalled issues

Several issues with what appears to be the same underlying mechanism, all closed without root-cause diagnosis:

  • #23185 — multi-turn V3 regression with OpenAI-compat providers (Cerebras, OpenRouter)
  • #16275 — single-turn tool calls failing with locally served (vLLM) Gemma
  • #15883 — Llama3.2:1b via Ollama inconsistently skipping tools

I believe these share the same root cause: n8n trusting that the model will call a tool unprompted, which is a safe assumption only for flagship OpenAI/Anthropic models.

Proposed fix

Two options:

  1. Default-on: n8n passes tool_choice: "required" on iteration 1 whenever the agent has at least one tool bound. Revert to "auto" on subsequent iterations.
  2. Opt-in toggle: expose a node-level option on the AI Agent ("Force tool use on first iteration") that does the above.

LangChain's bindTools already supports passing tool_choice through. The fix surface is small.

Operating System

macOS 26.5

n8n Version

2.20.7 (self-hosted, Docker, official n8nio/n8n image)

Node.js Version

24.14.1

Database

PostgreSQL

Execution mode

main (default)

Hosting

self hosted

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

The agent invokes the tool. (Confirmed: with tool_choice: "required" forced into the outgoing request, the model calls the tool every time.)

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING