qwen3_coder: non-streaming cuts a tool-call argument at a literal </tool_call>, </function> or <parameter= inside the value
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 35/100
Direzione di ricerca
Start with Qwen3CoderDetector.detect_and_parse in python/freetoken/server/function_call_parser.py (around line 2036) and the regexes just above it (lines 1934-1937), then compare with InvokeParamStreamMixin (around line 660), which keeps string values until </parameter>. Run the repro script from the issue; it should print the full END = "</tool_call>" value in both modes. Done means the non-streaming parse returns the same arguments as streaming for the three literals, with tests covering them.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
What happened
When a qwen3_coder tool-call argument contains the text </tool_call>, </function> or <parameter=, non-streaming /v1/chat/completions cuts the value at that text and returns the call without an error. A coding agent that writes or edits a file whose content mentions these tags (tool-call parsers, their tests, prompt templates, docs) gets a truncated content argument and writes a broken file. Streaming returns the full value for the same output.
literal inside content |
non-streaming content |
streaming content |
|---|---|---|
</tool_call> |
END = " |
END = "</tool_call>"\nprint(END) |
</function> |
END = " |
END = "</function>"\nprint(END) |
<parameter= |
END = " |
END = "<parameter="\nprint(END) |
Minimal reproduction (CPU only, from a clean checkout, PYTHONPATH=python python repro.py):
import asyncio, json
from types import SimpleNamespace
from freetoken.message.frontend import UserReply
from freetoken.server.generation import GenSpec, ToolCallsDelta, generate_events, generate_full
TOOLS = [{"type": "function", "function": {"name": "write", "parameters": {"type": "object",
"properties": {"path": {"type": "string"}, "content": {"type": "string"}}}}}]
class State: # stands in for the engine: replays the model's text, one piece per reply
def __init__(self, pieces):
self.config = SimpleNamespace(tool_call_parser="qwen3_coder", reasoning_parser=None)
self.pieces = pieces
async def wait_for_ack(self, uid):
for i, p in enumerate(self.pieces):
yield UserReply(uid=uid, incremental_output=p, finished=i == len(self.pieces) - 1,
prompt_tokens_delta=int(i == 0), completion_tokens_delta=1)
spec = lambda: GenSpec(messages=[], sampling_params=None, parser_tools=TOOLS, template_tools=TOOLS)
for literal in ["</tool_call>", "</function>", "<parameter="]:
value = f'END = "{literal}"\nprint(END)'
text = ("<tool_call>\n<function=write>\n<parameter=path>\nt.py\n</parameter>\n"
f"<parameter=content>\n{value}\n</parameter>\n</function>\n</tool_call>")
full = asyncio.run(generate_full(1, spec(), State([text])))
async def stream():
return [ev async for ev in generate_events(1, spec(), State(list(text)))]
args = "".join(c.parameters for ev in asyncio.run(stream()) if isinstance(ev, ToolCallsDelta) for c in ev.calls)
print(f"{literal!r:16} non-stream content={json.loads(full.tool_calls[0].parameters).get('content')!r}")
print(f"{'':16} stream content={json.loads(args).get('content')!r}")
Output on main 9b68e007:
'</tool_call>' non-stream content='END = "'
stream content='END = "</tool_call>"\nprint(END)'
'</function>' non-stream content='END = "'
stream content='END = "</function>"\nprint(END)'
'<parameter=' non-stream content='END = "'
stream content='END = "<parameter="\nprint(END)'
Cause (current main, 9b68e007)
Qwen3CoderDetector.detect_and_parse (python/freetoken/server/function_call_parser.py:2036), which non-streaming reaches through generation._parse_tool_response (python/freetoken/server/generation.py:462), splits the text with three lazy regexes (function_call_parser.py:1934-1937):
tool_call_block_regex<tool_call>(.*?)</tool_call>ends the block at the first</tool_call>, even one inside a parameter value;function_regex<function=(.*?)</function>ends the function at the first</function>;parameter_regexalso ends a value at a lookahead(?=<parameter=)or(?=</function>).
The streaming path (InvokeParamStreamMixin, function_call_parser.py:660) instead stays inside a string value until </parameter>, so it keeps these literals.
Related: #710 reports a different failure of the same non-streaming parameter_regex (a value cut inside </parameter> by max_tokens).
Proposed fix
Have the one-shot parse follow the streaming grammar: inside a parameter, only </parameter> ends the value. The simplest way is for Qwen3CoderDetector.detect_and_parse to run the streaming parser over the whole text, so the two modes cannot drift apart. A literal </parameter> inside a value stays ambiguous in both modes; this proposal does not change that case.
Environment
Seen serving nvidia/Qwen3.8-Flash-Next-NVFP4 on an RTX 5080 16 GB; the reproduction above is CPU-only on main 9b68e007.
Related qwen3_coder stream/non-stream mismatches: #713, #714, #716.
- Lingua principale
- Python
- Stelle
- 14.2k
- Fork
- 1.4k
- Merge medio
- 21h 39m
- PR unite (30g)
- 100
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di FlashML-org/FreeToken
-
Desktop feature
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
FlashML-org/FreeToken#800 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
FlashML-org/FreeToken#767 ·
I maintainer di solito rispondono entro 1 giorno
-
test_gemv_path_matches_the_transcription fails on sm_120 (bf16 rounding tie exceeds the tolerance)Forse già presa @void-mckenzie l’ha presa 17 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 77/100
FlashML-org/FreeToken#649 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
FlashML-org/FreeToken#648 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
FlashML-org/FreeToken#646 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di FlashML-org/FreeToken
Issue simili
-
infrastructure
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
NatLabRockies/H2Integrate#977 ·
I maintainer di solito rispondono entro 1 giorno
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
topoteretes/cognee#6470 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
-
area:docs priority:p2 type:docs wave:9
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 84/100
jejjohnson/radtrax#293 ·
I maintainer di solito rispondono entro 1 giorno
-
bug core plugins
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
bunkerity/bunkerweb#4121 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno