Skip to content

fix(serve): recover drifted Qwen tool calls - #10

Open
Sciguymjm wants to merge 1 commit into
Neroued:masterfrom
Sciguymjm:fix/tolerant-qwen-tool-calls
Open

fix(serve): recover drifted Qwen tool calls#10
Sciguymjm wants to merge 1 commit into
Neroued:masterfrom
Sciguymjm:fix/tolerant-qwen-tool-calls

Conversation

@Sciguymjm

@Sciguymjm Sciguymjm commented Jul 31, 2026

Copy link
Copy Markdown

Summary

Qwen3.6 can occasionally drift from its XML-like tool-call format during long agentic sessions. NInfer's strict parser then returns the complete response as text, so clients such as OpenCode display a naked <tool_call> instead of receiving a structured tool call.

This adds an opt-in --tolerant-tool-calls serving mode that recovers complete function calls despite common Qwen wrapper/suffix drift, missing outer </tool_call>, or a call emitted before </think>.

Design

  • Strict parsing remains the default.
  • Tolerant recovery only promotes a complete function with parsed parameters; incomplete calls are not executed.
  • Streaming tolerant tool requests buffer reasoning until parsing completes, preventing malformed call text from leaking as reasoning or being emitted twice.
  • Request-log schema is bumped to v5 and the benchmark consumer is updated accordingly.

Verification

  • Built the serving binary and focused C++ tests.
  • ninfer_tool_call_parser_test: passed.
  • ninfer_serve_options_test: passed.
  • ninfer_request_log_test: passed.
  • PYTHONPATH=. python3 -m pytest -q tests/test_serve_corpus.py: 2 passed.
  • Manual validation was performed through OpenCode → llama-swap → NInfer against Qwen3.6-27B; the malformed/naked tool-call behavior was reproduced in the affected workflow.

No automated live-model HTTP integration fixture is included.

@jmander11

Copy link
Copy Markdown

Have you ever seen this with qwen3.8-27b?

@paolobarbolini

Copy link
Copy Markdown

I've seen it happen a few times a day. Very long thinking and/or writing long files seems to increase the likelyhood of it.

igorls referenced this pull request in igorls/ninfer Aug 25, 2026
igorls referenced this pull request in igorls/ninfer Aug 25, 2026
…ciliation (issue #5)

Final parser verified equivalent to upstream #10's grammar grafted onto the
fork's streaming filter; adversarial-review findings from the first attempt
resolved by faithful re-port (tolerant mode relaxes exactly two checks,
<tool_call>-only trigger). allowed_types byte-faithful to upstream Neroued#57.
Clean-volume independent test verification green.

Claude-Session: https://claude.ai/code/session_01Wv1ehCcaeL86hBzw74iqgr

# Conflicts:
#	RUNBOOK.md
igorls referenced this pull request in igorls/ninfer Aug 25, 2026
Near-miss tag recovery was removed in the faithful #10 re-port; the header
comment (upstream's own wording) describes what tolerant mode actually does.

Claude-Session: https://claude.ai/code/session_01Wv1ehCcaeL86hBzw74iqgr
@engharat

Copy link
Copy Markdown

I confirm this is happening to me too. I converted a QUASAR qwen3.8-27b NVFP4 model from vllm to ninfer and I am experiencing this tool broken command, and I was wondering where the issue was coming from.

@gregtakacs

Copy link
Copy Markdown

Can confirm it is happening occasionally on 3.8-27b NVFP4 model.

engharat added a commit to engharat/ninfer that referenced this pull request Aug 27, 2026
Adapt upstream Neroued/ninfer PR Neroued#10 (7aa9200)
to this fork's diverged serving stack:

- add --tolerant-tool-calls to recover complete Qwen calls with trailing
  wrapper tags, suffix text, a missing outer tool-call close tag, or a call
  emitted inside the reasoning channel before the closing think tag
- buffer streaming reasoning for tolerant tool requests so malformed call
  text is not emitted as thinking before the final parser classifies it
- request log schema v10 -> v11: server_start records carry
  server.tolerant_tool_calls; the bench corpus runner constant is aligned
  with the C++ version (it had lagged at v9 since 3b60754)
- update serve docs, usage text, and the affected parser/option/log/schema
  tests
@gregtakacs

gregtakacs commented Aug 27, 2026

Copy link
Copy Markdown

Confirmed this still reproduces and this fix still resolves it as of today (2026-08-27), including on qwen3_8_27b_nvfp4 (NVFP4) specifically — same symptom others reported here: occasional naked <tool_call> leaking as plain content under long agentic tool-use sessions (Cline, in our case).

Hand-merged this PR's actual fix onto a more current pin (master@9dbc0740, ~4 weeks and 68 commits ahead of where this branch forked) to unblock ourselves in the meantime, since mergeable_state here is dirty against current master (mainly request-log schema drift, v5→v17, unrelated to the actual fix). Built clean and all three relevant tests pass against the merged tree. Happy to share the resolved diff if it's useful for rebasing this branch — just say the word.

(Separately: filed #101 for an unrelated tool-call parsing bug we hit even with this fix applied — a strictly-typed parameter's JSON value containing an un-doubled backslash, e.g. a literal grep/regex \* or \|, rejects the whole call outright since your tolerant recovery here only engages once a call has already been parsed.)

gzenz pushed a commit to gzenz/ninfer that referenced this pull request Aug 27, 2026
Hand-port of the parser portion of upstream PR Neroued#10 (Neroued/ninfer,
'fix(serve): recover drifted Qwen tool calls' by Sciguymjm). The PR itself
targets an older base (request-log schema v4->v5 vs v9 here) and predates
the Responses API, preserve_thinking, and ToolCallStreamFilter that this
tree carries, so cherry-picking it either failed to build or silently
dropped those features. Only the parser semantics + the opt-in flag are
ported; the PR's request-log schema bump, docs, and bench changes are not.

Motivation: Qwen drifts from the tool-call syntax during long agentic
sessions. The strict parser then returns the whole thing as plain text, so
the client renders raw <tool_call> XML instead of executing the call. That
also forces the client to rewrite history, which breaks the prompt prefix
and costs a full KV re-prefill on the next turn (~5s at 37k ctx, ~20s at
100k) - observed in a Claude Code session against this server.

--tolerant-tool-calls (default off, strict behaviour unchanged) recovers a
complete function call despite: duplicate/garbage closing tags, a missing
</tool_call>, or trailing explanatory prose. A call is only promoted when
its parameters parse; genuinely unparseable output still falls back to text.

Tests: tests/test_tolerant_tool_calls.cpp (10 assertions covering both
modes, the real drift captured from a live session, and the must-still-fall-
back cases). Existing ninfer_tool_call_parser_test still passes unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants