Repository navigation
[Bugfix][Tool Parser] Fix hunyuan_a13b/xlam streaming: argument named "name" spawns a phantom tool call - #49535
Conversation
… "name" spawned a phantom tool call The streaming tool-name regex in both parsers matched any `"name":` key in the accumulated text, including an argument key also called `name` (e.g. `create_user(name="Alice", age=30)`). This inflated the detected tool count, causing a phantom tool call named after the argument's value and dropping the real call's arguments. Non-streaming parsing was unaffected. Anchor the regex on the following `"arguments"` key so it only matches a tool's own name field. Adds regression tests for both parsers covering non-streaming and streaming. Co-authored-by: Mahad Rehman <114791389+mahadrehmann@users.noreply.github.com> Signed-off-by: Mahad Rehman <114791389+mahadrehmann@users.noreply.github.com> Signed-off-by: muhammadfawaz1 <135441198+muhammadfawaz1@users.noreply.github.com>
chaunceyjiang
left a comment
There was a problem hiding this comment.
If you discovered these two issues locally, I would suggest migrating these two parsers to the new Parser Engine instead of continuing to modify the old logic.
|
@chaunceyjiang Thanks for the pointer , I can see the new Parser Engine exists (gemma4/minimax_m2/nemotron_v3/qwen3 are on it), but I don't see an existing effort tracking migration of hunyuan_a13b/xlam to it. Is that migration something you'd want done as a prerequisite for this fix, or would it be fine to land this minimal correctness fix now and track the engine migration as separate follow-up work? Happy to help with the migration too if that's the preferred path and it's not already being planned by someone else. |
|
I think we should prioritize the migration. As far as I know, no one is currently working on it. |
|
@chaunceyjiang Closing this PR, starting the migration instead. Two things I hit that I'd rather confirm than guess: The 4 engine-based parsers (Qwen3, MiniMax M2, Gemma4, Nemotron V3) all use explicit per-call delimiters (<function=, <invoke name=", etc.) so the state machine knows where one tool call ends and the next begins. Hunyuan_a13b/xlam instead emit one undelimited JSON array ([{...},{...}]) — boundaries are just JSON commas/braces, no terminal token. Does the engine already support that shape, or does it need new engine-level work? Hunyuan_a13b has a separate hunyuan_a13b_reasoning_parser.py (xlam doesn't). Since ParserEngine unifies reasoning + tool calls in one class, should the reasoning parser get folded in too, or stay separate? |
Summary
In streaming mode, the
hunyuan_a13bandxlamtool parsers treat any argument key"name"as the start of a new tool call. A tool call whose arguments contain anamefield, a common shape, e.g.create_user(name=...),greet(name=...), is streamed as two tool calls: the real call with its arguments dropped, and a phantom tool call named after the argument's value. Non-streaming parsing of the same output is correct.Root cause
Both parsers count/identify tool calls during streaming with a bare regex:
r'"name"\s*:\s*"([^"]+)"'which matches the argument key
"name"as well as a tool's ownnamefield. This fix anchors the regex on the following"arguments"key so it only matches a tool's own name field:r'"name"\s*:\s*"([^"]+)"(?=\s*,\s*"arguments"\s*:)'xlamadditionally had this pattern duplicated as inline string literals in two code paths; factored into a singleself.tool_name_regattribute, matchinghunyuan_a13b's existing style.Not a duplicate
"name"-matching regex, so it does not address this."name"tool-detection regex;xlamis unaffected either way.Test plan
test_hunyuan_a13b_tool_parser.pyandtest_xlam_tool_parser.pycovering the"name"-as-argument-key shape, non-streaming and streaming.python -m pytest tests/tool_parsers/test_hunyuan_a13b_tool_parser.py tests/tool_parsers/test_xlam_tool_parser.py -v— all relevant tests pass (1 pre-existing xfail, unrelated nested-JSON limitation, unchanged by this PR).pre-commit run --all-fileson changed files: ruff check, ruff format, mypy all pass.