Skip to content

Add <function=name> format support to Qwen tool parser#281

Merged
Thump604 merged 1 commit intowaybarrios:mainfrom
janhilgard:fix/qwen-tool-parser-function-format
Apr 11, 2026
Merged

Add <function=name> format support to Qwen tool parser#281
Thump604 merged 1 commit intowaybarrios:mainfrom
janhilgard:fix/qwen-tool-parser-function-format

Conversation

@janhilgard
Copy link
Copy Markdown
Collaborator

@janhilgard janhilgard commented Apr 11, 2026

Summary

  • Qwen3.5 models generate tool calls in the native <function=name><parameter=key>value</parameter></function> format, but the qwen parser only handled <tool_call>JSON</tool_call> and [Calling tool: name({...})] — causing tool calls to be returned as raw text instead of structured tool_calls
  • Add FUNCTION_PATTERN and PARAM_PATTERN to parse both <parameter> tags and inline JSON arguments
  • Add streaming partial-marker buffering to prevent leaking incomplete markers (e.g. <function before = arrives) as text content to the client
  • Buffer recovery: when a partial marker turns out to be a false positive, the buffered prefix is re-emitted with the next delta (no text lost)
  • Add safety net in server.py Anthropic streaming paths to suppress any <function content that slips through the streaming parser

Files changed

File Changes
qwen_tool_parser.py Add FUNCTION_PATTERN, PARAM_PATTERN, partial-marker streaming buffering
server.py Safety net for <function in Anthropic streaming, <function= in fallback detection

Context

When using --tool-call-parser qwen with Qwen3.5 models, the model outputs:

<function=get_weather>
<parameter=city>
Prague
</parameter>
</function>

This format was not recognized, so responses had tool_calls: null and the raw XML leaked as text content. Clients (e.g. Claude Code) reported "Invalid tool parameters" errors.

Streaming issue

Without partial-marker buffering, early tokens like <function (before = arrives) leaked as text content deltas. The fix buffers potential marker prefixes and re-emits them if they turn out not to be tool calls. An additional safety net in server.py catches any edge cases where markers slip through the streaming parser (e.g. due to multi-token MTP deltas or reasoning parser splits).

Test plan

  • Non-streaming: tool_calls correctly populated with name and arguments
  • Streaming (OpenAI): tool call chunks emitted correctly
  • Streaming (Anthropic): tool_use content blocks with input_json_delta, no leaked text
  • Existing formats still work (XML <tool_call>, bracket [Calling tool:])
  • JSON arguments inside <function> tags parsed correctly
  • Multiple <parameter> tags parsed into single arguments object
  • Parameter value type coercion (numbers, booleans, strings)
  • Partial marker buffering: <function suppressed until = confirms tool call
  • Buffer recovery: false positives (e.g. <div>) re-emit buffered prefix
  • Safety net: <function in content is suppressed before emission
  • End-of-stream fallback: <function= triggers tool call extraction

🤖 Generated with Claude Code

@janhilgard janhilgard force-pushed the fix/qwen-tool-parser-function-format branch 2 times, most recently from db1ec93 to 32c6bdd Compare April 11, 2026 09:53
Copy link
Copy Markdown
Collaborator

@Thump604 Thump604 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one blocker in the new streaming recovery path.

The parser now tries to recover false positives by re-emitting buffered text when a partial <function prefix turns out not to be a tool call. But the new server-side safety net swallows that recovered content again whenever it still contains <function. That means plain text like <functional or any literal content ending in <function at end-of-stream can be lost instead of re-emitted.

Minimal reproduction from the current logic:

  • delta 1: Look at <function -> parser buffers and emits nothing
  • delta 2: al interface -> parser correctly recovers "<functional interface"
  • server safety net sees <function in recovered content and suppresses it again
  • next delta only emits the later suffix, so the original recovered prefix is dropped

There is a second edge case at end-of-stream: a literal final "<function" with no = is buffered and then never flushed, because the fallback only checks for actual tool markers like <function=.

I would keep the parser-side buffering/recovery, but narrow or remove the server-side <function suppression so it does not fight the parser's recovery path. A focused regression test around a false-positive literal like <functional across a chunk boundary would catch this.

@janhilgard janhilgard force-pushed the fix/qwen-tool-parser-function-format branch 2 times, most recently from 32e4751 to fdc6735 Compare April 11, 2026 14:02
@janhilgard
Copy link
Copy Markdown
Collaborator Author

Updated based on review feedback:

  1. Removed server-side <function safety net — it was conflicting with the parser's recovery path. The parser correctly recovers false positives like <functional interface by re-emitting the buffered prefix, but the safety net was swallowing the recovered content again.

  2. Widened end-of-stream fallback from "<function=" to "<function" to handle truncated partial markers at stream end.

  3. Added regression tests for:

    • <functional false positive across chunk boundary
    • Single < followed by non-marker text recovery
    • Streaming with multiple <function= blocks

The diff is now clean: 3 files, +311/-3 lines (rebased on latest main after PR #278 merge).

@janhilgard
Copy link
Copy Markdown
Collaborator Author

@Thump604 Updated based on review feedback — removed the server-side safety net, added regression tests for false positives like <functional. Would appreciate another look when you get a chance. 🙏

@janhilgard janhilgard requested a review from Thump604 April 11, 2026 14:08
@janhilgard
Copy link
Copy Markdown
Collaborator Author

@Thump604 Review feedback addressed — ready for re-review.

@Thump604
Copy link
Copy Markdown
Collaborator

I rechecked the updated streaming path and there is still one blocker in the false-positive recovery logic.

Minimal repro on the current branch:

  • chunk 1: "Look at <function" -> returns None
  • chunk 2: "al interface" -> returns {"content": "<functional interface"}

That preserves the buffered marker prefix, but it still drops the earlier buffered content from the same first chunk ("Look at "). So the user-visible stream still loses text; it is now just losing a smaller prefix than before.

The current regression test does not catch this because it only asserts that "<function" and "al interface" are present in the recovered content. It should also assert that the pre-marker content is preserved, e.g. the recovered content should be exactly "Look at <functional interface".

I would keep the general direction, but the recovery branch needs to re-emit the full buffered suffix from the point buffering began, not only the matched partial marker fragment.

@janhilgard janhilgard force-pushed the fix/qwen-tool-parser-function-format branch from fdc6735 to 0ccb90f Compare April 11, 2026 14:30
@janhilgard
Copy link
Copy Markdown
Collaborator Author

Addressed the second review feedback — content before the partial marker is now preserved:

What changed:

  • Added _get_partial_marker_len() to compute the exact length of a partial marker suffix
  • Buffering now only holds back the marker suffix itself; content before it in the same delta (e.g. "Look at " from "Look at <function") is emitted immediately via delta_text[:safe_chars]
  • Added regression test asserting exact content: first chunk yields {"content": "Look at "}, recovery yields {"content": "<functional interface"}

All 23 Qwen tests pass. Ready for re-review @Thump604.

- Parse <function=name><parameter=key>value</parameter></function> format
  natively generated by Qwen3.5 models (both parameter tags and JSON body)
- Add streaming partial-marker buffering for <function, [Calling tool,
  and <tool_call prefixes to prevent raw markup from leaking to clients
- Content-preserving buffering: only the marker suffix is held back;
  text before it in the same delta is emitted immediately
- False-positive recovery: when a buffered prefix like <function turns
  out to be normal text (e.g. <functional), re-emit the buffered prefix
  together with the next delta
- Widen server-side end-of-stream fallback to catch incomplete <function
  markers (without requiring the = sign)
- Add comprehensive tests: 4 non-streaming format tests + 5 streaming
  buffering/recovery tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@janhilgard janhilgard force-pushed the fix/qwen-tool-parser-function-format branch from 0ccb90f to d67fec4 Compare April 11, 2026 14:41
Copy link
Copy Markdown
Collaborator

@Thump604 Thump604 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked the false-positive streaming case that blocked this earlier. The updated buffering logic now preserves the pre-marker content, the parser tests pass locally, and the server-side suppression conflict is gone. This looks good to merge.

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.

2 participants