Skip to content

fix(server): accept malformed Qwen tool-call variants#552

Open
jkyamog wants to merge 1 commit into
Luce-Org:mainfrom
jkyamog:fix-funcname-tool-call-compat
Open

fix(server): accept malformed Qwen tool-call variants#552
jkyamog wants to merge 1 commit into
Luce-Org:mainfrom
jkyamog:fix-funcname-tool-call-compat

Conversation

@jkyamog

@jkyamog jkyamog commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Some Qwen agent responses contain recognizable tool calls but vary from the
documented <function=NAME> XML form. The server previously streamed these
variants as visible assistant text instead of returning structured
tool_calls.

This change adds strict compatibility for three observed forms:

<parameter name="bash"><parameter name="command">...</parameter></function>

<funcname>read
<parameter=path>...</parameter>
</function>

<function read>
<parameter=path>...</parameter>
</function>

The streaming detector now holds each opener until parsing completes, so tool
syntax does not leak into assistant content.

Safety

  • A parsed function name must match a tool declared in the request.
  • Compatibility forms require complete parameter elements.
  • Unexpected body text, duplicate parameters, unknown tools, and incomplete
    syntax are rejected rather than partially parsed or guessed.
  • Function arguments remain JSON strings in OpenAI-compatible responses.

Reproduction

The newest failure was a two-call response emitted as visible text:

Let me read the file and compute its SHA-256 hash.

<function read>
<parameter=path>
/tmp/tool-input.md
</parameter>
</function>

<function bash>
<parameter=command>
sha256sum /tmp/tool-input.md
</parameter>
</function>

After this change, the preamble remains assistant content and the two blocks
become ordered read and bash tool calls.

Review in cubic

@jkyamog
jkyamog force-pushed the fix-funcname-tool-call-compat branch from 4f7ecb4 to 66c5afa Compare July 21, 2026 12:15
@jkyamog

jkyamog commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Discovered a few more issues with the parser this is over 100-200M tokens generated in the last few days. 2 of the issues has happened more than 2x.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

3 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="server/src/server/tool_parser.cpp">

<violation number="1" location="server/src/server/tool_parser.cpp:852">
P1: Mixed compatibility variants are returned grouped by parser pattern rather than response order, so dependent calls can execute in the wrong sequence. Preserve source-position ordering before returning `result.tool_calls`.</violation>

<violation number="2" location="server/src/server/tool_parser.cpp:892">
P2: Wrapped attribute-style calls still leak `<tool_call>`/`</tool_call>` as assistant content. Extend this removal span over an adjacent wrapper, matching the handling in pattern 3d/3e.</violation>
</file>

<file name="server/test/test_server_unit.cpp">

<violation number="1" location="server/test/test_server_unit.cpp:1227">
P3: Add an `accumulated_text()` equality assertion to `test_emitter_funcname_tool_buffer_detection` matching the pattern in the sibling emitter tests, verifying that the `\n\n` preamble before `<funcname>` is preserved as assistant content rather than silently stripped or leaked.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

// tool and the entire body consists of fully closed parameter elements.
// This strict body check prevents partial or guessed arguments.
if (tools.is_array() && !tools.empty()) {
auto begin = std::sregex_iterator(text.begin(), text.end(), re_attribute_tool_xml());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Mixed compatibility variants are returned grouped by parser pattern rather than response order, so dependent calls can execute in the wrong sequence. Preserve source-position ordering before returning result.tool_calls.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/server/tool_parser.cpp, line 852:

<comment>Mixed compatibility variants are returned grouped by parser pattern rather than response order, so dependent calls can execute in the wrong sequence. Preserve source-position ordering before returning `result.tool_calls`.</comment>

<file context>
@@ -781,6 +843,113 @@ ToolParseResult parse_tool_calls(const std::string & text, const json & tools) {
+    // tool and the entire body consists of fully closed parameter elements.
+    // This strict body check prevents partial or guessed arguments.
+    if (tools.is_array() && !tools.empty()) {
+        auto begin = std::sregex_iterator(text.begin(), text.end(), re_attribute_tool_xml());
+        auto end = std::sregex_iterator();
+        for (auto it = begin; it != end; ++it) {
</file context>

if (!trim_ws(body.substr(cursor)).empty()) valid = false;
if (!valid || !found_param) continue;

add_call(fn_name, args, pos, pos + it->length());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Wrapped attribute-style calls still leak <tool_call>/</tool_call> as assistant content. Extend this removal span over an adjacent wrapper, matching the handling in pattern 3d/3e.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/src/server/tool_parser.cpp, line 892:

<comment>Wrapped attribute-style calls still leak `<tool_call>`/`</tool_call>` as assistant content. Extend this removal span over an adjacent wrapper, matching the handling in pattern 3d/3e.</comment>

<file context>
@@ -781,6 +843,113 @@ ToolParseResult parse_tool_calls(const std::string & text, const json & tools) {
+            if (!trim_ws(body.substr(cursor)).empty()) valid = false;
+            if (!valid || !found_param) continue;
+
+            add_call(fn_name, args, pos, pos + it->length());
+        }
+    }
</file context>

static void test_emitter_funcname_tool_buffer_detection() {
auto em = make_emitter(ApiFormat::OPENAI_CHAT, read_tools());
em.emit_start();
em.emit_token("\n\n<func");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: Add an accumulated_text() equality assertion to test_emitter_funcname_tool_buffer_detection matching the pattern in the sibling emitter tests, verifying that the \n\n preamble before <funcname> is preserved as assistant content rather than silently stripped or leaked.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/test/test_server_unit.cpp, line 1227:

<comment>Add an `accumulated_text()` equality assertion to `test_emitter_funcname_tool_buffer_detection` matching the pattern in the sibling emitter tests, verifying that the `\n\n` preamble before `<funcname>` is preserved as assistant content rather than silently stripped or leaked.</comment>

<file context>
@@ -1048,6 +1196,78 @@ static void test_emitter_bare_function_tool_buffer_detection() {
+static void test_emitter_funcname_tool_buffer_detection() {
+    auto em = make_emitter(ApiFormat::OPENAI_CHAT, read_tools());
+    em.emit_start();
+    em.emit_token("\n\n<func");
+    em.emit_token("name>read\n<parameter=limit>\n50\n</parameter>\n"
+                  "<parameter=offset>\n1\n</parameter>\n");
</file context>

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.

1 participant