Skip to content

Parse each Seed-OSS parallel call in its own <seed:tool_call> block (#1540) - #1542

Merged
leehack merged 1 commit into
mainfrom
fix/1540-seed-oss-parallel-calls
Oct 28, 2026
Merged

leehack merged 1 commit into
mainfrom
fix/1540-seed-oss-parallel-calls

Conversation

@leehack

@leehack leehack commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Summary

Closes #1540. With parallel tool calls, Seed-OSS replies put each call in its own <seed:tool_call> block. llamadart now returns one call per block, and its grammar lets the model emit those blocks, as llama.cpp 620d2225 does. Same failure class as #1530, fixed for Qwen3-Coder XML in #1539; this extends that mechanism to Seed-OSS.

Root cause. Two defects, both pre-existing:

  • parseXmlToolCalls read calls only inside the first scope. XmlToolCallFormat.seedOss did not set repeatsScope, so after the first </function> the parser found the next <function= behind </seed:tool_call>\n<seed:tool_call>\n, treated that as a malformed prelude and failed the whole parse: 0 calls, everything as content.
  • The Seed-OSS grammar went through the generic XML path: root ::= "<seed:tool_call>" tool-call+ "</seed:tool_call>", with no whitespace between tags. It rejects the line-broken form the template shows and llama.cpp parses, allows only one scope, and ignored parallelToolCalls. Its lazy trigger <seed:tool_call><function= never fired on that line-broken form.

Change.

  • seedOss sets repeatsScope, so it gets Parse each Qwen3-Coder XML parallel call in its own <tool_call> block (#1530) #1539's parser path: a whitespace-separated scope holding a valid call continues the calls, text between scopes ends them, and a later scope without a valid call stays content after the earlier calls.
  • buildXmlToolCallGrammar uses the scoped grammar for every repeatsScope format, not only qwen3Coder: root ::= tool-call-scope+, one call per scope, or tool-call-scope with parallel tool calls off, as llama.cpp's Seed-OSS grammar does. The shared value rule is renamed qwen3-coder-value → xml-value; the Qwen grammar is otherwise unchanged.
  • SeedOssHandler passes the request's parallel flag to the grammar and triggers on "<seed:tool_call>\n", llama.cpp's trigger.

Production-readiness scope

  • User-facing scope: Parallel tool calls work for Seed-OSS: generation can emit several <seed:tool_call> blocks, and parse and stream return each one. With parallel tool calls off, the grammar admits one call.
  • Supported platforms/paths: All. The change is in shared Dart template parsing and grammar (native llama.cpp and WebGPU bridge).
  • Unsupported or intentionally unavailable paths: Without a grammar, the parse still returns every block when parallel tool calls are off, where llama.cpp rejects the reply (same as Parse each Qwen3-Coder XML parallel call in its own <tool_call> block (#1530) #1539 for Qwen). The lazy grammar no longer starts on <seed:tool_call><function= without a line break; llama.cpp does not start on it either, and the parser still reads that form.
  • Out of scope / follow-ups: Pre-existing for single calls, not changed here: consecutive <seed:tool_call><function= scopes without the line break now parse as calls, as one such scope already did, where llama.cpp keeps any such scope as content (see Matrix Evidence); text after the last call stays content (llama.cpp rejects the reply); an unclosed last scope stays content (llama.cpp returns its partial call); a </seed:think> without its opening tag parses as reasoning (llama.cpp keeps it as content); with thinking off, a streamed thought is not reported as reasoning; the required-tool-choice grammar does not admit a thought or text before the call, as llama.cpp's does. Not filed yet.

Completeness checklist

  • Declared scope is fully implemented, or explicitly reduced above.
  • Unsupported platform/option combinations fail loudly with actionable diagnostics or are clearly documented as unavailable. (No new unsupported path.)
  • Public API docs, README/website docs, examples, support matrices, and changelog entries are updated where relevant. (Changelog in both files. No public API change; template-engine-internals.md does not describe this behavior.)
  • Regression coverage covers the original issue plus key negative/version-skew paths where relevant.
  • Public capability changes update the validation coverage registry with a pack or a reasoned, issue-linked exemption; inventory checks pass (or N/A with scope). N/A: bug fix, no capability change.
  • Security/privacy review completed: no secrets, bearer tokens, signed URLs, or raw secret-bearing paths leak through logs, cache keys, metadata, errors, or snapshots.
  • Follow-up work that is useful but not required for this PR is tracked in GitHub Issues, or explicitly marked "None" above. (Listed above, not filed yet.)

PR type guidance

  • Bugfix PR: Root cause and fix are above. Regression tests:

    • test/integration/core/engine/xml_parallel_tool_calls_upstream_test.dart (renamed from qwen3_coder_parallel_upstream_test.dart) now reads both upstream fixtures. New test/fixtures/seed_oss_parallel_upstream.json: 40 upstream cases on the newly vendored ByteDance-Seed-OSS.jinja (pinned ref b10549, identical at 620d2225), thinking on and off, 2 and 3 calls separated by nothing, \n, \n\n, a space or a tab, a trailing newline, and text and thought prefixes. Each case checks parse plus whole, byte-by-byte and 3-character streams against llama.cpp's content, reasoning and tool calls. The Qwen fixture keeps its 160 cases and now also records llama.cpp's tool calls (regenerated from the same probe; content and reasoning unchanged).
    • Parser unit tests (xml_tool_call_format_test.dart): the consecutive-scope group runs for Qwen3-Coder and Seed-OSS: separators, with and without schemas, forced-open and explicit thoughts, text between scopes, invalid, junk and unclosed later scopes, and an unclosed first scope. The grammar test checks both roots with parallel on and off.
    • Handler tests (seed_oss_handler_test.dart): grammar root per parallel flag, the trigger, and a three-scope parse after a thought.
    • llama.cpp test-gbnf-validator acceptance and rejection with parallel on and off now includes Seed-OSS (specialized_tool_grammar_validation_e2e_test.dart).

    On base, 53 of the new or changed unit and integration tests and the grammar validator test fail (one of them only for the xml-value rename).

Test Plan

  • dart run tool/prepare_workspace.dart
  • dart format --output=none --set-exit-if-changed .
  • dart analyze lib test tool, plus dart run tool/testing/check_platform_boundaries.dart
  • dart test -p vm -j 1 --exclude-tags local-only (isolated TMPDIR): 9464 passed, 123 skipped at fede2047e
  • dart test -p chrome --exclude-tags local-only test/unit/core/template test/unit/core/engine/chat_completion_stream_parser_test.dart: 1115 passed
  • ./tool/testing/run_template_parity_suites.sh, with LLAMA_CPP_GBNF_VALIDATOR set to a llama.cpp 620d2225 (v0.7.0, the runtime pin) validator: detection 74, diagnostic 93, unit template 1609, upstream parser 10, GBNF acceptance 58, all passed at fede2047e

Matrix Evidence

Matrix row Scope covered Platform / model / backend Result Evidence / notes
root-vm native-safe unit/integration macOS arm64 VM PASS 9464 passed, 123 skipped at fede2047e
root-chrome browser-compatible template and stream-parser tests Chrome PASS 1115 passed; the upstream integration test is VM-only
template-parity detection, render and parse parity; GBNF acceptance and rejection macOS arm64, llama.cpp b10549 templates, 620d2225 validator PASS detection 74, diagnostic 93, unit template 1609, upstream parser 10, GBNF acceptance 58, all passed at fede2047e
structured-output-adversarial grammar acceptance and rejection, parse plus whole/byte/3-char streaming, auto/required/none tool choice with thinking prefixes, upstream parity upstream common_chat_session probe at 620d2225 PASS (see note) Seed-OSS multi-call corpus (24 configs × 491 outputs = 11,784 records, Seed and plain <think> tags): 3,664 → 4,384 matching. 352 records match on base only because base rejects the whole reply: all are consecutive <seed:tool_call><function= scopes without the line break, which llama.cpp keeps as content even for one scope, while llamadart on base already parses one such scope as a call (and the old required grammar emitted that form). Head extends that to several scopes; no other record gets worse. Full corpus (94 templates, 183,672 records): identical results to base, 0 transitions. Mutation: 11 parser, grammar, handler and trigger mutants all killed.
high-risk-exact-head-independent-qa independent blocking review of exact head fresh auditor session, exact head fede204 PASS CLEAR: CLEAR at fede204; evidence /opt/UnitySrc/personal/llama/llamadart-evidence/pr-1542/audit/evidence-1542-fede204.json
Other rows — — N/A No runtime, backend, release or LiteRT-LM change

Review Notes

  • Independent review status: CLEAR at exact head fede204 by a fresh auditor session (0 P1, 0 P2).
  • CI status / head SHA: fede204, CI pending.

High-risk regression review

  • Classification: high-risk (surfaces: structuredOutput)
  • Implementation task: Seed-OSS: repeated <seed:tool_call> scopes parse as 0 tool calls #1540 lane (this PR's author)
  • Independent blocking QA task: fresh auditor session, CLEAR, 0 known PR-caused P1, 0 unresolved threads
  • Exact head / current base: fede204 / 7979ef1 (merge-tree clean)
  • Production-branch deletion, bypass, or miswire proof: Mutants that turn off Seed repeatsScope, keep the scoped grammar Qwen-only, hard-code or drop the handler's parallel flag, restore the old trigger, make the root always or never repeat, allow several calls per scope, drop whitespace after the scope start, or remove the rollback each fail the new tests.
  • Affected-family real-model/artifact evidence: No Seed-OSS weights locally; primary upstream 620d2225 emissions are in the durable fixture.
  • Explicit unavailable-family or other N/A evidence: Seed-OSS weights unavailable (36B; not downloaded).
  • Known PR-caused P1 regressions: 0
  • Unresolved review threads: 0

…1540)

With parallel tool calls, Seed-OSS replies put each call in its own
<seed:tool_call> scope. The parser read calls inside one scope only, so a
second scope failed the whole parse and returned no calls; llama.cpp
620d2225 returns one call per scope. The grammar wrapped every call in one
scope with no whitespace between tags, and its lazy trigger
<seed:tool_call><function= never fired on the line-broken form the template
shows and llama.cpp parses.

Seed-OSS now repeats scopes as Qwen3-Coder XML does since #1530: the parser
continues into whitespace-separated scopes, and the grammar admits one call
per scope, repeated only with parallel tool calls. It starts on
"<seed:tool_call>\n", llama.cpp's trigger.

The Qwen3-Coder parallel upstream test now reads both fixtures, and its
fixture records llama.cpp's tool calls as the Seed-OSS one does.
@leehack

leehack commented Oct 28, 2026

Copy link
Copy Markdown
Owner Author

Coordinator decision: accept the 352 Seed oracle rows (592 with the auditor's extra outputs) as documented leniency. All are <seed:tool_call> not followed by exactly one \n; upstream keeps them as content, main returned 0 calls for them, every output main parsed as calls is unchanged, and the scoped grammar's xml-space admits these forms, so parsing them keeps grammar and parser consistent. Independent audit CLEAR at fede2047e (llamadart-evidence/pr-1542/audit/evidence-1542-fede204.json). The one CI failure was Native Prompt Reuse Parity timing out in Setup Flutter (infra); rerun once.

@leehack
leehack marked this pull request as ready for review October 28, 2026 15:53
@leehack
leehack merged commit ce92458 into main Oct 28, 2026
63 of 65 checks passed
@leehack
leehack deleted the fix/1540-seed-oss-parallel-calls branch October 28, 2026 15:54
@github-actions

Copy link
Copy Markdown
Contributor

Chat app preview removed for leehack/llamadart-chat-pr-1542.

@leehack

leehack commented Nov 10, 2026

Copy link
Copy Markdown
Owner Author

Maintainer decisions for 0.19.4 (#1874), checked against llama.cpp cd959bae9:

  • Text after <seed:tool_call> that does not start with a line break: llama.cpp keeps the reply as content with no call; llamadart returned a call. Match llama.cpp ("Match llama.cpp (Recommended)"). A line break followed by more whitespace already parses as a call in both.
  • Unknown tool name, missing required argument: llama.cpp fails the reply. Keep lenient, document ("Keep lenient, document"). With JSON-schema tools, llamadart's engine already rejects these after parsing; with ToolParam tools or none, the call is returned.
    Tracked in Start Seed-OSS tool calls at the first line-broken scope, as llama.cpp does #1886.

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.

Seed-OSS: repeated <seed:tool_call> scopes parse as 0 tool calls

1 participant