Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

qwen3_coder: a bare `<function=NAME>` block without `<tool_call>` is a call plus raw markup in non-streaming, and only text in streaming

オープン
#714 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
35/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
python
領域
ai, backend

調査の方向性

Start with Qwen3CoderDetector.detect_and_parse in python/freetoken/server/function_call_parser.py (around lines 2036-2059) and the streaming idle-mode branch in InvokeParamStreamMixin.parse_streaming_increment. Run the CPU reproduction in the issue, which uses FunctionCallParser with qwen3_coder. Done means a bare <function=NAME> block gives the same calls and content in streaming and non-streaming, and literal <function= in prose yields no call.

索引モデルが issue の本文から書いたものです。

説明

What happened

If the model writes a <function=NAME>…</function> block without the <tool_call> wrapper, the two modes handle it differently:

Mode tool_calls content
Non-streaming the call the call, plus the raw <function=…>…</function> markup
Streaming none the raw markup

An agentic client executes the call when it is not streaming, and the call's markup is also shown as text. When it is streaming, the call is never executed. The non-streaming fallback also makes a call out of any literal <function= in prose or code. The tag does not need a closing </function> or a declared tool name, so an explanation such as "write a call as <function=NAME>" returns a call named NAME.

Minimal reproduction (CPU only, PYTHONPATH=python):

from freetoken.server.function_call_parser import FunctionCallParser
tools = [{"type": "function", "function": {"name": "edit", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}}]
bare = ["Doing it.\n", "<", "function", "=", "edit", ">\n", "<parameter=path>\n", "a.py\n", "</parameter>\n", "</function>"]
p = FunctionCallParser(tools, "qwen3_coder")
ev = [e for c in bare for e in p.parse_stream_events(c)]
print("stream calls:", [e for e in ev if e[0] == "calls"], "text:", repr("".join(x for k, x in ev if k == "text")))
r = FunctionCallParser(tools, "qwen3_coder").parse_non_stream("".join(bare))
print("non-stream:  ", r.calls, "content:", repr(r.normal_text))
r = FunctionCallParser(tools, "qwen3_coder").parse_non_stream("Write a call as `<function=NAME>` then the parameters.")
print("prose:       ", r.calls)
stream calls: [] text: 'Doing it.\n<function=edit>\n<parameter=path>\na.py\n</parameter>\n</function>'
non-stream:   [ToolCallItem(tool_index=0, name='edit', parameters='{"path": "a.py"}')] content: 'Doing it.\n<function=edit>\n<parameter=path>\na.py\n</parameter>\n</function>'
prose:        [ToolCallItem(tool_index=0, name='NAME', parameters='{}')]
Cause (current main, 9b68e007)
  • python/freetoken/server/function_call_parser.py:2036-2059, Qwen3CoderDetector.detect_and_parse: if there is no <tool_call>…</tool_call> block, the whole text is parsed as a block (tool_call_blocks = [text], line 2046). function_regex (line 1935) also matches an unclosed <function=… up to the end of the text. However, normal_text is only cut at <tool_call> (line 2038), so with no wrapper the full text, markup included, stays in normal_text. _parse_tool_response (generation.py:452-465) returns both.
  • The undeclared NAME is accepted because _should_forward_unknown_tool (line 63) is true by default (FREETOKEN_FORWARD_UNKNOWN_TOOLS, line 58). That default is fine for wrapped calls. In this fallback, it lets any <function= in plain text become a call.
  • InvokeParamStreamMixin.parse_streaming_increment, idle mode (lines 745-764): the stream only enters a block at _ps_outer_open (<tool_call>), so a bare <function= is released as text.
Proposed fix

Handle the bare form the same way in both modes:

  • When it is a call: only when NAME is a declared tool and the body after > starts, ignoring whitespace, with <parameter= or </function>.
  • Non-streaming: remove the parsed span from content.
  • Streaming: in idle mode, hold back a possible <function= prefix until the name and the first body tag decide the case. Then enter the invoke state as if <tool_call> had been seen, or release the held text as content.

With these checks, a literal <function= in prose or code stays text, and an undeclared name in the bare form is not forwarded. Wrapped calls are not affected, and neither is the forwarding of unknown tools inside <tool_call>. This is a different path from the truncated <tool_call> blocks in #710 and the partial openers in #347.

Environment

Found while serving nvidia/Qwen3.8-Flash-Next-NVFP4 on an RTX 5080 16 GB (I have not measured how often the model drops the <tool_call> wrapper); the reproduction above is CPU-only on main 9b68e007.

Related qwen3_coder stream/non-stream mismatches: #713, #715, #716.

主要言語
Python
スター
14.2k
フォーク
1.4k
平均マージ
21時間 39分
マージ済み PR(30日)
100

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

FlashML-org/FreeToken のほかの issue

FlashML-org/FreeToken の issue をすべて見る

似ている issue

Python の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。