Recover malformed Engine tool calls - #1111
Baiju Meswani (baijumeswani) merged 3 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
🟡 Changes recommended
Strict retry validation can accept duplicate JSON fields and inconsistently handles nested anyOf schemas.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds Engine-only recovery for malformed Qwen native tool calls while preserving Generator behavior.
Changes:
- Classifies narrowly recoverable malformed tool output.
- Retries once with strict tool-only guidance on a fresh Engine request.
- Adds recovery, cancellation, streaming, usage, and fallback tests.
File summaries
| File | Description |
|---|---|
web_service_test.cc |
Extends failure-envelope assertions. |
tool_call_stream_accumulator_test.cc |
Tests malformed-output classification. |
chat_session_test.cc |
Covers Engine recovery behavior. |
tool_call_stream_accumulator.h |
Propagates malformed status. |
tool_call_payload_parser.h |
Adds malformed disposition. |
qwen_xml_tool_call_decoder.h |
Exposes recovery-aware parsers. |
qwen_xml_tool_call_decoder.cc |
Implements classification and strict retry parsing. |
onnx_engine_chat_stream.h |
Adds explicit close support. |
onnx_engine_chat_stream.cc |
Implements checked, idempotent closing. |
chat_session.cc |
Implements one-shot guided recovery. |
chat_generator.h |
Adds the generator close contract. |
chat_generator.cc |
Provides the default no-op close. |
Review details
- Files reviewed: 12/12 changed files
- Comments generated: 3
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a415caeb-43ca-4fd3-bddb-018130b184b0
a595eae to
b2dafc2
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a415caeb-43ca-4fd3-bddb-018130b184b0
There was a problem hiding this comment.
🟡 Changes recommended
The schema change regresses nested anyOf handling in the classic Generator path despite the stated compatibility contract.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 11/11 changed files
- Comments generated: 1
- Review effort level: Balanced
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a415caeb-43ca-4fd3-bddb-018130b184b0
Tianlei Wu (tianleiwu)
left a comment
There was a problem hiding this comment.
Reviewed the malformed Engine tool-call recovery path at the current head. The retry is narrowly gated, keeps rejected first-attempt output out of caller-visible and durable state, explicitly closes the admitted Engine request before replacement, and strictly validates the accepted retry against declared tool schemas. The cancellation, exhaustion, usage, streaming, compatibility, and parser edge cases are covered comprehensively. No blocking concerns found.
This change prevents malformed native Qwen tool-call output from being returned as a successful-looking assistant
response when Foundry Local uses the Engine backend.
Automatic tool choice remains natural and unconstrained on the first generation attempt. If a qualified Qwen model
produces a narrowly recognized malformed native tool call before any semantic output, Foundry Local discards that
attempt, closes its Engine Request, and retries once on a fresh Engine Request using the existing tool-only guidance.
The retry is buffered and accepted only when it produces a complete, schema-valid structured tool-call batch. If
recovery is unavailable or fails, the logical request fails through the existing internal/inference error path and no
assistant turn is committed.
This PR is stacked on
baijumeswani/qwen-auto-tools/ #1104.Motivation
A qualified Qwen model can occasionally produce output such as:
The output clearly attempts to invoke a declared tool, but its parameter framing is malformed. Before this change,
the strict native decoder correctly refused to execute it, but the rejected XML became visible assistant text. If the
model then emitted EOS, the request ended with a normal
stopresult, leaving the coding harness with neither ausable response nor an executable tool call.
The recovery policy separates model-protocol failure from ordinary assistant text without constraining normal
automatic generation.
Flow
Each generation remains speculative until Foundry Local accepts and commits it:
Implementation
Engine-only malformed classification
The native Qwen decoder can now distinguish a recovery-eligible structural failure from an ordinary rejected
candidate.
Recovery eligibility is deliberately narrow:
Undeclared tools, unsupported schemas, unknown or duplicate parameters, missing required parameters,
schema-incompatible values, fenced examples, reserved nested framing, oversized candidates, and mixed valid/invalid
batches retain the existing rejected-visible fallback behavior.
The recovery-aware parser mode is selected only for automatic tool calls on
ChatBackendKind::kEngine. The classicGenerator path retains its previous byte-for-byte behavior and never retries.
No semantic-output rollback
Foundry Local retries only when no semantic generated output preceded the malformed candidate. Semantic output
includes visible text, reasoning, raw tool output, or a parsed structured call.
This keeps streaming behavior honest: output that has already been exposed is never retracted. If a safe prefix was
streamed before a malformed candidate, the malformed bytes remain suppressed, the stream terminates through the
existing error path, and no transcript turn is committed.
Fresh checked Engine Request
The malformed attempt is never committed to the canonical transcript. Its generated tokens do exist in the first
Engine Request, so that Request cannot be continued safely.
Recovery therefore:
The retry does not use Engine rewind, classic Generator rewind, a hidden corrective message, or automatic prefix
caching. Prefix reuse can remain a future performance optimization; it is not required for correctness.
Strict, schema-aware retry
The guided retry is fully buffered before publication. It must produce:
The retry path does not accept legacy parser repairs such as missing end markers, missing braces, alternate argument
aliases, singleton shorthand, unknown properties, missing required values, or incompatible value types.
If the retry is canceled, truncated, malformed, empty, text-only, reasoning-only, or otherwise invalid, Foundry Local
publishes no retry output and commits no turn.
Existing API and usage contracts