Skip to content

Broaden tool-calling to Phi and Qwen3-Coder dialects - #271

Open
stikves wants to merge 5 commits into
apple:mainfrom
stikves:sukru/tool-call-dialects
Open

stikves wants to merge 5 commits into
apple:mainfrom
stikves:sukru/tool-call-dialects

Conversation

@stikves

@stikves stikves commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Tool-calling covers the Phi and Qwen3-Coder dialects across both the server (llm-server) and the FoundationModels (CoreAILanguageModel) paths.

Dialects

  • Phi: chat template reads tools from a system message's tools key, not the top-level tools variable, and emits <|tool_call|> markers.
  • Qwen3-Coder: shares Qwen3's <tool_call> markers (detected as JSON) but emits an XML <function=NAME><parameter=K>V</parameter></function> body.

Changes

  • Parsing (shared, ToolCallParser.swift): detectToolCallFormat probes the <|tool_call|> / <|/tool_call|> family; ToolCallParser adds an XML .xmlFunction format for <function=…> bodies, and the JSON path routes to it via effectiveToolCallFormat when the body is <function=…>. Both the server and the FM path build their parser from this.
  • Phi tools-in-system-message injection (shared): toolsJSONForSystemMessage serializes the tool specs (sorted keys) and applyToolsToSystemMessage folds them into the system message, gated on ToolCallDetection.toolsInSystemMessage. The server's ChatHandler and the FM path's makeTokens both call these; top-level tools is still passed for Qwen3-style models.

Tests

  • <|tool_call|> detection, XML-function parse (explicit + JSON→XML fallback), <tool_call> JSON regression guard, toolsInSystemMessage gating.
  • toolsJSONForSystemMessage sorted-keys serialization.
  • FM makeTokens with a capturing tokenizer: Phi injects into the system message, Qwen3 keeps tools top-level.

On-device Phi validation of the rendered template output is still pending.

Tool-calling worked only for the JSON-`<tool_call>` dialect (Qwen3). Two other
families failed silently:

- Phi: its chat template reads tools from a system message's `tools` key, not
  the top-level `tools` variable, and it emits `<|tool_call|>` markers. So the
  model was never told about the tools and, even if it emitted a call, the
  detector did not recognize the markers.
- Qwen3-Coder: shares Qwen3's `<tool_call>` markers (so it is detected) but emits
  an XML `<function=NAME><parameter=K>V</parameter></function>` body, which the
  JSON-only parser dropped, leaving finish_reason=stop with no tool call.

Fixes: expose the tools JSON on the system message so `message['tools']` templates
render them (top-level `tools` models are unaffected); probe the `<|tool_call|>`
marker family; add an XML `.xmlFunction` parse format, with the JSON path falling
back to it when the body is `<function=…>` (Qwen3-Coder is detected as JSON via the
shared markers). Adds 5 parser/detection tests incl. a Qwen3 JSON regression guard.

Found via the pi coding-agent campaign: Qwen3-Coder-30B scored 0/3 not from a
capability gap but because the server dropped its tool calls.
… scoped tool injection

- Coerce XML-function parameters to bool/Int/Double/array/object so JSON-schema
  types survive (was stringifying, e.g. {"count":"5"} instead of {"count":5}).
- Route <function=…> bodies through the .xmlFunction switch branch via
  effectiveToolCallFormat; drop the substring fallback in parseJSONToolCalls.
- Scope system-message tool injection to the Phi dialect (toolsInSystemMessage)
  so top-level-tools families (Qwen3) are not perturbed by a synthetic system
  message. Extract applyToolsToSystemMessage helper and cover it in tests.
@stikves stikves changed the title Broaden server tool-calling to Phi and Qwen3-Coder dialects Broaden tool-calling to Phi and Qwen3-Coder dialects Sep 22, 2026
stikves and others added 2 commits September 22, 2026 11:32
Extract toolsJSONForSystemMessage next to applyToolsToSystemMessage and call it from both ChatHandler and CoreAILanguageModel.makeTokens, gated on toolCallDetection.toolsInSystemMessage. Qwen3-style dialects keep top-level tools.
@stikves
stikves marked this pull request as ready for review September 22, 2026 19:30
@stikves stikves self-assigned this Sep 22, 2026
…en Phi assertion

Drop the bespoke DialectTokenizer/CapturingTokenizer structs in
ToolCallDialectTests in favor of the shared TestUtilities versions.
CapturingTokenizer moves to TestUtilities, wrapping MockTokenizer and
delegating everything except the captured applyChatTemplate overload.

Also tighten fmInjectsToolsForPhi to assert the injected system tools
JSON equals toolsJSONForSystemMessage's output exactly, instead of a
substring contains check.
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.

1 participant