Support automatic Qwen tool calls for coding workflows - #1104
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
cce3c3d to
41d9043
Compare
## 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.
8b1e3e7 to
0f4e908
Compare
0f4e908 to
ecdd8e4
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Valid json_object requests and supported JSON Schema constructs currently regress.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 29/29 changed files
- Comments generated: 3
- Review effort level: Balanced
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
ecdd8e4 to
a4ce726
Compare
Use type-explicit comparisons and a compiler-neutral expected JSON string so the Qwen test sources build cleanly with MSVC. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6883972e-cc0b-4076-a3a5-86106e7a1133
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: ```xml <tool_call> <function=rename_session> <title>Trapping Rain Water Solution </parameter> </function> </tool_call> ``` 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 `stop` result, leaving the coding harness with neither a usable 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: ```text Committed transcript + current user input | v Attempt 1: natural automatic generation | +-------+------------------+ | | normal response or qualified malformed valid native tool call native tool-call attempt | | v v publish and commit recovery eligibility check | +-------------+-------------+ | | not eligible eligible (semantic output, stop, (Engine, auto, limit, cancellation, natural end, unsupported schema, etc.) no semantic output) | | v v existing inference error checked close of and no transcript commit first Engine Request | v fresh Engine Request with tool-only guidance | +---------+---------+ | | valid strict call invalid retry | | v v publish and commit existing inference accepted attempt error; commit nothing ``` ## 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: - exact qualified Qwen tool-call framing was entered; - the function name resolves to a declared tool; - the tool has a schema supported by the native decoder; - the failure is structural or framing-related. 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 classic Generator 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: 1. explicitly and synchronously closes the completed first Engine Request; 2. reuses the already captured transcript, current input, tool snapshot, options, system prefix, and prepared prompt; 3. creates a fresh Engine Request; 4. changes only the internal output policy to tool-only guidance; 5. performs at most one retry. 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: - a complete canonical structured-tool envelope; - one or more advertised tool calls; - exact canonical fields; - parameter objects that satisfy the effective declared schema; - an atomically valid complete batch. 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 - No public C API, C++ API, provider option, error enum, or model-dialect API is added. - No ONNX Runtime GenAI change is required. - Existing SDK, Chat Completions, and Responses error translation is reused. - Public usage and finish reason describe the accepted retry attempt. - Classic Generator behavior is unchanged. --------- Copilot-Session: a415caeb-43ca-4fd3-bddb-018130b184b0
Summary
This PR enables automatic tool use for the qualified Qwen chat-template dialect used by coding harnesses.
Qwen emits native XML when
tool_choiceis omitted orauto. Foundry Local previously returned that XML as visibleassistant text, so clients such as GitHub Copilot CLI and Toolkit could not execute the requested tool through the
normal tool-calling path.
This change:
in a different order;
Example
Qwen output such as:
is exposed to the client as the equivalent structured
viewcall rather than visible XML.If Qwen emits multiple adjacent calls, Foundry Local preserves their source order. Public call IDs remain unchanged,
while a copied prompt-render group reorders corresponding results into the positional order expected by the qualified
template.
Activation and compatibility
The Qwen-native decoder is internal and enables only when all of these conditions hold:
qwen3_5_text;auto, with no forced tool;<tool_call>and</tool_call>;Required/forced tool guidance and unrelated model types continue through the existing provider-neutral paths.
Broader structurally supported JSON Schema keywords remain available to shared guidance; a schema outside the Qwen decoder's supported
subset disables only native XML decoding rather than redefining global schema compatibility. Reference-bearing tool schemas
fail generated guidance closed because embedding them would change local
$refresolution; general reference rebasing remains out of scope.guidance_typeandguidance_datamust be supplied together. Partial explicit guidance is rejected before generatorconstruction instead of producing asymmetric routing.
Decoding and fallback
The decoder accepts only the qualified LF-delimited function/parameter form. Argument conversion follows the declared
tool schema:
The selected payload is bounded to 64 KiB. A contiguous multi-call batch is published atomically, so a malformed or
unsupported later call cannot expose an earlier partial batch.
Malformed, incomplete, fenced, oversized, unsupported, or undeclared candidates remain visible non-actionable text.
The implementation does not repair or fabricate calls.
Raw-envelope and structured candidates retain ownership of bytes once opened. A valid raw
apply_patchenvelope stilluses the #1088 path; a valid Qwen XML batch uses the structured path; ordinary output remains text.
Positional result projection
For the qualified template, a multi-call assistant turn followed by its contiguous tool-result group must contain an
exact call-ID bijection. Foundry Local reorders only a copied render group into assistant-call order.
The canonical transcript and public result IDs are not mutated. Duplicate, missing, unknown, intervening, or otherwise
ambiguous result groups fail before generation with an invalid-request error. Non-Qwen rendering is unchanged.
Scope
Included:
Not included:
Validation
Exact pushed head
a4ce726609dd7c3b7e601f3f6681fd44d56cb74b:FOUNDRY_QUALIFIED_QWEN_MODEL_PATHexplicitly selects the qualified external package;identified shared-schema compatibility blocker corrected and re-reviewed.
Feature qualification during development also covered direct Chat Completions and Responses, streaming and
non-streaming calls, one-call and two-call automatic tool use, reverse-arrival result association, raw
apply_patchcoexistence, malformed fallback, concurrent mixed traffic, and a 128K-token call/result continuation.