Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

qwen3_coder: non-streaming cuts a tool-call argument at a literal </tool_call>, </function> or <parameter= inside the value

Aperta
#715 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

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
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
python
Ambito
backend

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_regex also 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

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di FlashML-org/FreeToken

Tutte le issue di FlashML-org/FreeToken

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.