Skip to content

Support automatic Qwen tool calls for coding workflows - #1104

Merged
Tianlei Wu (tianleiwu) merged 3 commits into
mainfrom
baijumeswani/qwen-auto-tools
Sep 17, 2026
Merged

Tianlei Wu (tianleiwu) merged 3 commits into
mainfrom
baijumeswani/qwen-auto-tools

Conversation

@baijumeswani

@baijumeswani Baiju Meswani (baijumeswani) commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR enables automatic tool use for the qualified Qwen chat-template dialect used by coding harnesses.

Qwen emits native XML when tool_choice is omitted or auto. Foundry Local previously returned that XML as visible
assistant text, so clients such as GitHub Copilot CLI and Toolkit could not execute the requested tool through the
normal tool-calling path.

This change:

  • recognizes the exact qualified Qwen XML tool-call form;
  • converts complete valid output into Foundry Local's existing provider-neutral structured tool calls;
  • supports one contiguous, atomic batch of adjacent calls;
  • renders multi-call tool results back to the qualified Qwen template in original call order, even when results arrive
    in a different order;
  • composes with the raw-envelope behavior from Support declared raw-envelope output for custom tools #1088 without changing its public contract.

Example

Qwen output such as:

<tool_call>
<function=view>
<parameter=path>
src/main.cc
</parameter>
</function>
</tool_call>

is exposed to the client as the equivalent structured view call 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:

  • the loaded model type is exactly qwen3_5_text;
  • production-template probes confirm the exact qualified call-output and positional-result behavior;
  • tools are present;
  • tool choice is omitted or auto, with no forced tool;
  • no explicit guidance is active;
  • the effective call markers are exactly <tool_call> and </tool_call>;
  • every serialized tool declaration is recognized and supported by the exact decoder.

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 $ref resolution; general reference rebasing remains out of scope.

guidance_type and guidance_data must be supplied together. Partial explicit guidance is rejected before generator
construction 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:

  • declared strings remain strings and preserve internal newlines;
  • declared number, integer, boolean, null, array, and object values must parse as compatible JSON;
  • custom-tool declarations are accepted only in their canonical normalized shape.

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_patch envelope still
uses 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:

  • internal model-type and production-template capability detection;
  • exact Qwen automatic XML decoding;
  • schema-aware conversion to existing structured calls;
  • adjacent-call batch atomicity;
  • positional result projection for the qualified template;
  • composition with Support declared raw-envelope output for custom tools #1088 raw-envelope routing.

Not included:

  • arbitrary XML or dialect registration;
  • a new public model-dialect API;
  • malformed-call repair or retry;
  • patch execution;
  • cancellation/resource-safety work;
  • prefix caching;
  • model packaging or dependency pinning.

Validation

Exact pushed head a4ce726609dd7c3b7e601f3f6681fd44d56cb74b:

  • canonical RelWithDebInfo build passed;
  • 2,004 native tests passed;
  • one qualified-package capability test skipped because it runs only when FOUNDRY_QUALIFIED_QWEN_MODEL_PATH explicitly selects the qualified external package;
  • five pre-existing tests remain disabled;
  • independent correctness, C++ safety, architecture/compatibility, and adversarial-coverage reviews completed, with the
    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_patch
coexistence, malformed fallback, concurrent mixed traffic, and a 128K-token call/result continuation.

@baijumeswani
Baiju Meswani (baijumeswani) added this pull request to stack #1086 September 13, 2026 22:47
@vercel

vercel Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
foundry-local Ready Ready Preview Sep 17, 2026 6:52am UTC

Request Review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment thread sdk_v2/cpp/src/inferencing/generative/chat/chat_session.cc Outdated
Comment thread sdk_v2/cpp/src/inferencing/generative/toolcalling/tool_call_stream_accumulator.h Outdated
Comment thread sdk_v2/cpp/src/inferencing/generative/toolcalling/tool_call_stream_accumulator.h Outdated
Comment thread sdk_v2/cpp/test/internal_api/chat/chat_session_test.cc Outdated
Comment thread sdk_v2/cpp/test/internal_api/toolcalling/tool_call_stream_accumulator_test.cc Outdated
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.
Base automatically changed from baijumeswani/apply-patch-tool to main September 16, 2026 20:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread sdk_v2/cpp/src/inferencing/generative/chat/chat_session.cc
Comment thread sdk_v2/cpp/src/inferencing/generative/toolcalling/grammar.cc Outdated
Comment thread sdk_v2/cpp/src/inferencing/generative/toolcalling/grammar.cc
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
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
@tianleiwu
Tianlei Wu (tianleiwu) merged commit e06e92c into main Sep 17, 2026
60 checks passed
@tianleiwu
Tianlei Wu (tianleiwu) deleted the baijumeswani/qwen-auto-tools branch September 17, 2026 08:33
Baiju Meswani (baijumeswani) added a commit that referenced this pull request Sep 17, 2026
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

This branch was successfully deployed

1 active deployment
Preview — 4e5086f0 Deployed Sep 17, 2026 by vercel[bot]
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.

3 participants