Support automatic Qwen tool calls for coding workflows - #1104
Open
Baiju Meswani (baijumeswani) wants to merge 18 commits into
Open
Baiju Meswani (baijumeswani) wants to merge 18 commits into
Baiju Meswani (baijumeswani) wants to merge 18 commits into
Conversation
Baiju Meswani (baijumeswani)
added this pull request to stack #1086
September 13, 2026 22:47
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Copilot started reviewing on behalf of
Baiju Meswani (baijumeswani)
September 14, 2026 01:13
View session
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Boundary handling, duplicate-schema ambiguity, required-tool enforcement, and streaming performance issues remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds qualified Qwen XML tool-call decoding and positional result ordering across chat backends.
Changes:
- Detects compatible Qwen model templates and enables native XML decoding.
- Adds schema-aware, streaming-safe tool-call parsing and result projection.
- Expands ABI, fallback, streaming, and integration coverage.
File summaries
| File | Description |
|---|---|
sdk_v2/cpp/CMakeLists.txt |
Builds the Qwen decoder. |
sdk_v2/cpp/test/CMakeLists.txt |
Registers capability tests. |
tool_definition_abi_test.cc |
Covers legacy serialized tools. |
tool_call_utils_test.cc |
Tests malformed metadata handling. |
tool_call_stream_accumulator_test.cc |
Covers Qwen streaming and fallbacks. |
grammar_test.cc |
Tests malformed schemas. |
genai_model_instance_test.cc |
Tests capability probing. |
chat_transcript_test.cc |
Tests positional result projection. |
chat_session_test.cc |
Adds end-to-end Qwen coverage. |
tool_call_utils.cc |
Safely reads advertised names. |
tool_call_stream_accumulator.h |
Adds payload-parser streaming mode. |
tool_call_payload_parser.h |
Defines the parser contract. |
qwen_xml_tool_call_decoder.h |
Declares Qwen decoder creation. |
qwen_xml_tool_call_decoder.cc |
Implements schema-aware XML decoding. |
markdown_fence_tracker.h |
Prevents fenced examples becoming calls. |
grammar.cc |
Hardens schema generation. |
genai_model_instance.h |
Exposes detected capabilities. |
genai_model_instance.cc |
Probes model type and templates. |
onnx_engine_chat_stream.h |
Accepts prepared messages. |
onnx_engine_chat_stream.cc |
Renders projected Engine prompts. |
onnx_chat_generator.h |
Accepts prepared messages. |
onnx_chat_generator.cc |
Renders projected Generator prompts. |
chat_template.h |
Defines positional projection APIs. |
chat_template.cc |
Implements result reordering. |
chat_session.h |
Adds parser and preparer integration. |
chat_session.cc |
Routes Qwen calls through production paths. |
chat_generator.h |
Updates the backend interface. |
Review details
Suppressed comments (1)
sdk_v2/cpp/src/inferencing/generative/toolcalling/qwen_xml_tool_call_decoder.cc:173
- This parameterless-schema branch also ignores duplicate insertion failure. Two declarations with the same name and empty/object-with-no-properties schemas therefore leave one declaration valid, even though the selected schema is ambiguous. Use the same duplicate-invalidating insertion path for this branch.
if (parameters.empty() ||
(parameter_type.has_value() && *parameter_type == "object" && has_no_properties &&
has_no_required_parameters)) {
schema.valid =
!parameters.contains("properties") || parameters["properties"].is_object();
schemas.emplace(name, std::move(schema));
continue;
- Files reviewed: 27/27 changed files
- Comments generated: 6
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Baiju Meswani (baijumeswani)
force-pushed
the
baijumeswani/qwen-auto-tools
branch
from
September 15, 2026 06:26
cce3c3d to
41d9043
Compare
Baiju Meswani (baijumeswani)
added a commit
that referenced
this pull request
Sep 16, 2026
## Summary
Add provider-layer support for text custom tools to the
OpenAI-compatible Chat Completions and Responses APIs.
A function tool receives a structured JSON object. A text custom tool
receives one free-form string payload. This PR
preserves that distinction across request parsing, prompt construction,
generated output, streaming, storage, replay,
and continuation.
## Supported wire formats
### Chat Completions
```json
{
"type": "custom",
"custom": {
"name": "edit",
"description": "Replace file content",
"format": {
"type": "text"
}
}
}
```
A generated call is returned as:
```json
{
"id": "call_abc123",
"type": "custom",
"custom": {
"name": "edit",
"input": "replacement text"
}
}
```
### Responses
```json
{
"type": "custom",
"name": "edit",
"description": "Replace file content",
"format": {
"type": "text"
}
}
```
A completed call is returned as:
```json
{
"id": "ctc_abc123",
"type": "custom_tool_call",
"call_id": "call_abc123",
"name": "edit",
"input": "replacement text",
"status": "completed"
}
```
The caller can continue the Responses conversation with a correlated
result:
```json
{
"previous_response_id": "resp_abc123",
"input": [
{
"type": "custom_tool_call_output",
"call_id": "call_abc123",
"output": "Edit applied"
}
]
}
```
## Behavior
- Supports mixed function and text custom tools in one request.
- Supports automatic, required, and explicitly forced choices at the
provider-contract level.
- Preserves the effective tool name and kind in an immutable request
snapshot.
- Uses an internal synthesized `{"input": "<text>"}` schema so existing
model templates can represent a text tool.
- Returns the original text payload through the provider's custom-tool
shape.
- Preserves newlines, indentation, tabs, trailing spaces, quotes,
Unicode, empty input, and JSON-looking text.
- Streams one stable call ID through call creation, payload deltas,
completion, storage, and later result correlation.
- Keeps parallel calls on one assistant turn and validates results
against their call IDs.
- Rebuilds stored custom calls and results using the same canonical
prompt representation as a live continuation.
## Validation
The provider adapters reject:
- unknown tool types;
- missing or empty tool names;
- duplicate names;
- a forced choice for an undeclared tool or the wrong tool kind;
- function-only fields on a custom tool;
- non-text custom formats or unsupported text-format constraints;
- `strict: true` function tools, because strict schema enforcement is
not implemented;
- malformed custom calls/results and unmatched call IDs.
Unsupported constraints are rejected rather than silently ignored.
## Scope
This PR implements provider transport and lifecycle support for generic
text custom tools. It does **not** implement:
- the stock GitHub Copilot `apply_patch` Lark grammar or
marker-delimited raw-envelope recognition; that is #1088;
- model-specific automatic Qwen XML tool-call decoding; that is #1104;
- reordering parallel tool results for positional model templates.
Accordingly, this PR supplies the provider foundation needed by
patch-style tools, but does not by itself make the
stock Copilot `apply_patch` declaration executable.
Decode qualified Qwen XML function-call output under automatic tool choice and project positional tool results into model call order without mutating canonical history. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Accept Qwen XML calls for canonical custom-tool schemas and unwrap their free-form input through the existing custom-tool path. Preserve raw-envelope support and reject ambiguous framing or noncanonical custom schemas. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Support Copilot's constrained glob.paths union, preserve source comparison operators in exact string arguments, and reject nested Qwen framing without broadening automatic XML decoding. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Reject schema assertions and nested structures the XML decoder cannot validate, keeping automatic calls fail-closed. Remove an unused projection wrapper and use protocol-neutral selected-payload finalization names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Reject unsupported root parameter assertions and cap recursive schema/value validation while preserving the qualified fail-closed interpretation of structured union prefixes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Reject malformed tool-only schemas before generation, invalidate duplicate Qwen declarations consistently, and make selected-payload streaming bounded and amortized linear at the 64 KiB boundary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Preserve exact-limit calls across whitespace-prefixed ordinary output, structurally validate required guidance schemas, reject duplicate legacy guidance declarations, and extend parameterless duplicate coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Validate nested guidance schema types and cover exact-limit whitespace adjacency, partial-marker EOS, duplicate guidance names, and parameterless duplicate decoder branches. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
Baiju Meswani (baijumeswani)
force-pushed
the
baijumeswani/qwen-auto-tools
branch
from
September 16, 2026 00:33
8b1e3e7 to
0f4e908
Compare
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.
What this changes
This PR makes Foundry Local work better with coding-style tool use on qualified Qwen chat models.
Before this change, Qwen could emit native XML tool calls, but Foundry Local treated that output as plain assistant text. That meant clients like GitHub Copilot CLI and Toolkit could see text that looked like a tool call, but they could not actually execute the tool through the normal tool-calling path.
This PR teaches Foundry Local to recognize the exact Qwen XML tool-call format we qualified, convert it into normal structured tool calls, and keep tool results associated with the correct call when the model emits multiple calls in one turn.
Why this is needed
The coding scenario depends on reliable tool use.
Simple examples:
viewa file and get the file content back.bashfor a focused test and get the test output back.apply_patchwhen that path is explicitly supported, without breaking ordinary structured tool calls.Without this change, Qwen-native tool output leaks back to the client as visible XML, or parallel tool results can be rendered back to the model in the wrong order for the qualified Qwen template.
Examples
Before
The model emits text like:
Foundry Local returns that as assistant text instead of a structured tool call, so the client cannot run the tool through the normal tool-calling loop.
After
The same output is decoded as a normal tool call:
{ "name": "view", "arguments": { "path": "src/main.cc" } }The client receives an actual tool-call item, executes it, and sends back the tool result through the existing tool-result path.
Multiple calls in one turn
If the model emits:
Foundry Local preserves that as two adjacent structured calls in source order.
If the tool results come back in a different completion order, Foundry Local still renders the results back to the qualified Qwen template in the assistant-call order the model expects.
Raw
apply_patchstill worksIf output is already claimed by the raw-envelope path for
apply_patch, that path keeps ownership of the bytes.This PR does not replace or weaken the raw-tool behavior added for the coding harness. It composes with it.
Scope
Included:
Not included:
Design intent
Keep the change narrow and fail closed.
This PR only turns the behavior on for the exact qualified Qwen shape:
qwen3_5_text;If those checks do not match, Foundry Local keeps the existing behavior.
This avoids introducing a broad XML parser, a generic dialect system, or behavior changes for unrelated models.
Behavior details
Tool-call decoding
The decoder accepts only the exact qualified form:
It converts the decoded payload into the existing provider-neutral parsed tool-call shape.
Argument conversion follows the declared tool schema:
Fallback behavior
These cases remain visible text and are not turned into tool calls:
Multi-call result ordering
For the qualified Qwen template, consecutive tool results are interpreted positionally.
This PR reorders only the copied render group that is sent back through the prompt-building path. It does not mutate the canonical transcript or public tool-call IDs.
If the result history is ambiguous, Foundry Local rejects the request before generation instead of guessing.
User-visible effect
For qualified Qwen coding models, automatic tool use becomes much more reliable.
Examples of workflows this enables:
view.rg.bash.That is the core behavior needed for coding harnesses that rely on automatic tool calling.
Validation summary
The change was qualified against targeted unit and integration coverage, including:
apply_patchcoexistence;Real Qwen qualification showed the expected improvement for coding-style tool loops such as reading files, running shell commands, and continuing after tool results.