Recover malformed Engine tool calls - #1111
Open
Baiju Meswani (baijumeswani) wants to merge 1 commit into
Open
Baiju Meswani (baijumeswani) wants to merge 1 commit into
Baiju Meswani (baijumeswani) wants to merge 1 commit into
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a415caeb-43ca-4fd3-bddb-018130b184b0
Baiju Meswani (baijumeswani)
added this pull request to stack #1086
September 16, 2026 04:40
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Copilot started reviewing on behalf of
Baiju Meswani (baijumeswani)
September 16, 2026 04:41
View session
Contributor
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.
Comment on lines
+336
to
+337
| if (!schema.contains("anyOf")) { | ||
| return IsCompatibleJsonValue(value, schema); |
| std::string_view payload, | ||
| const std::string& tools_json, | ||
| const std::unordered_map<std::string, ToolKind>& tool_kinds) { | ||
| const auto calls_json = Json::parse(payload, nullptr, false); |
Comment on lines
+1798
to
+1799
| EXPECT_NE(std::string(error.what()).find("Model emitted a malformed tool call and guided recovery did not produce a valid tool call"), | ||
| std::string::npos); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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