[PR #14603] [CLOSED] model/renderers: fix Qwen 3.5 tool call format and unclosed think tags #14742

Closed
opened 2026-04-13 01:01:48 -05:00 by GiteaMirror · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/ollama/ollama/pull/14603
Author: @yossiovadia
Created: 3/4/2026
Status: Closed

Base: mainHead: fix/qwen35-tool-calling


📝 Commits (1)

  • feeef2a model/renderers: fix Qwen 3.5 tool call format and unclosed think tags

📊 Changes

4 files changed (+7 additions, -4 deletions)

View changed files

📝 model/parsers/parsers.go (+1 -1)
📝 model/renderers/qwen3vl.go (+2 -0)
📝 model/renderers/qwen3vl_thinking_test.go (+3 -2)
📝 model/renderers/renderer.go (+1 -1)

📄 Description

Fixes https://github.com/ollama/ollama/issues/14493

Problem

Qwen 3.5 tool calling is broken in two ways:

  1. Wrong tool call format. The "qwen3.5" renderer/parser name maps to Qwen3VLRenderer + Qwen3Parser, which emit Hermes-style JSON tool calls. Qwen 3.5 expects the Qwen3-Coder XML format (<tool_call><function=...>). The correct Qwen3CoderRenderer and Qwen3CoderParser already exist under "qwen3-coder" — they are just not wired to the "qwen3.5" name.

  2. Unclosed </think> tag in multi-turn. In Qwen3VLRenderer, when an assistant message has thinking content and tool calls but no text content, the </think> tag is never emitted. The tool calls end up inside the open <think> block, corrupting all subsequent turns.

Root cause

  1. rendererForName("qwen3.5") returns &Qwen3VLRenderer{...} and ParserForName("qwen3.5") returns &Qwen3Parser{...} — both are the wrong implementations for this model family.

  2. In qwen3vl.go:98-100, </think> is only written inside if content != "". When content is empty (tool-call-only response), the tag is left open.

Fix

  1. Map "qwen3.5" to Qwen3CoderRenderer and Qwen3CoderParser in the renderer/parser switch statements.

  2. Add else if i < len(messages)-1 clause to emit \n</think> when content is empty on non-prefill (historical) messages. The prefill case (last message) correctly leaves the tag open for the model to continue generating.

Before / After

Bug 1: Tool call format

Given a conversation where the assistant calls get_weather(location: "Paris"):

BeforeQwen3VLRenderer emits Hermes-style JSON (wrong for Qwen 3.5):

<|im_start|>assistant
<tool_call>
{"name": "get_weather", "arguments": {"location": "Paris"}}
</tool_call><|im_end|>

AfterQwen3CoderRenderer emits Qwen3-Coder XML (correct):

<|im_start|>assistant

<tool_call>
<function=get_weather>
<parameter=location>
Paris
</parameter>
</function>
</tool_call><|im_end|>

Bug 3: Unclosed think tag

Given an assistant message with thinking + tool call but no text content:

Before</think> missing, tool call trapped inside think block:

<|im_start|>assistant
<think>
I should call the weather API for Paris.
<tool_call>
{"name": "get_weather", "arguments": {"location": "Paris"}}
</tool_call><|im_end|>

After</think> properly closed before tool call:

<|im_start|>assistant
<think>
I should call the weather API for Paris.
</think>
<tool_call>
{"name": "get_weather", "arguments": {"location": "Paris"}}
</tool_call><|im_end|>

Test results

$ go test ./model/renderers/... ./model/parsers/...
ok   github.com/ollama/ollama/model/renderers
ok   github.com/ollama/ollama/model/parsers

Existing TestQwenRendererNameNoThinkBehaviorSplit updated to reflect the new "qwen3.5" renderer behavior.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/ollama/ollama/pull/14603 **Author:** [@yossiovadia](https://github.com/yossiovadia) **Created:** 3/4/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `fix/qwen35-tool-calling` --- ### 📝 Commits (1) - [`feeef2a`](https://github.com/ollama/ollama/commit/feeef2a19f34014cf0e490b07f311e04e13a229d) model/renderers: fix Qwen 3.5 tool call format and unclosed think tags ### 📊 Changes **4 files changed** (+7 additions, -4 deletions) <details> <summary>View changed files</summary> 📝 `model/parsers/parsers.go` (+1 -1) 📝 `model/renderers/qwen3vl.go` (+2 -0) 📝 `model/renderers/qwen3vl_thinking_test.go` (+3 -2) 📝 `model/renderers/renderer.go` (+1 -1) </details> ### 📄 Description Fixes https://github.com/ollama/ollama/issues/14493 ## Problem Qwen 3.5 tool calling is broken in two ways: 1. **Wrong tool call format.** The `"qwen3.5"` renderer/parser name maps to `Qwen3VLRenderer` + `Qwen3Parser`, which emit Hermes-style JSON tool calls. Qwen 3.5 expects the Qwen3-Coder XML format (`<tool_call><function=...>`). The correct `Qwen3CoderRenderer` and `Qwen3CoderParser` already exist under `"qwen3-coder"` — they are just not wired to the `"qwen3.5"` name. 2. **Unclosed `</think>` tag in multi-turn.** In `Qwen3VLRenderer`, when an assistant message has thinking content and tool calls but no text content, the `</think>` tag is never emitted. The tool calls end up inside the open `<think>` block, corrupting all subsequent turns. ## Root cause 1. `rendererForName("qwen3.5")` returns `&Qwen3VLRenderer{...}` and `ParserForName("qwen3.5")` returns `&Qwen3Parser{...}` — both are the wrong implementations for this model family. 2. In `qwen3vl.go:98-100`, `</think>` is only written inside `if content != ""`. When `content` is empty (tool-call-only response), the tag is left open. ## Fix 1. Map `"qwen3.5"` to `Qwen3CoderRenderer` and `Qwen3CoderParser` in the renderer/parser switch statements. 2. Add `else if i < len(messages)-1` clause to emit `\n</think>` when content is empty on non-prefill (historical) messages. The prefill case (last message) correctly leaves the tag open for the model to continue generating. ## Before / After ### Bug 1: Tool call format Given a conversation where the assistant calls `get_weather(location: "Paris")`: **Before** — `Qwen3VLRenderer` emits Hermes-style JSON (wrong for Qwen 3.5): ``` <|im_start|>assistant <tool_call> {"name": "get_weather", "arguments": {"location": "Paris"}} </tool_call><|im_end|> ``` **After** — `Qwen3CoderRenderer` emits Qwen3-Coder XML (correct): ``` <|im_start|>assistant <tool_call> <function=get_weather> <parameter=location> Paris </parameter> </function> </tool_call><|im_end|> ``` ### Bug 3: Unclosed think tag Given an assistant message with thinking + tool call but no text content: **Before** — `</think>` missing, tool call trapped inside think block: ``` <|im_start|>assistant <think> I should call the weather API for Paris. <tool_call> {"name": "get_weather", "arguments": {"location": "Paris"}} </tool_call><|im_end|> ``` **After** — `</think>` properly closed before tool call: ``` <|im_start|>assistant <think> I should call the weather API for Paris. </think> <tool_call> {"name": "get_weather", "arguments": {"location": "Paris"}} </tool_call><|im_end|> ``` ## Test results ``` $ go test ./model/renderers/... ./model/parsers/... ok github.com/ollama/ollama/model/renderers ok github.com/ollama/ollama/model/parsers ``` Existing `TestQwenRendererNameNoThinkBehaviorSplit` updated to reflect the new `"qwen3.5"` renderer behavior. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
GiteaMirror added the pull-request label 2026-04-13 01:01:48 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/ollama#14742