Skip to content

Tool calling in the local server (1.1.0) - #4

Merged
jkyberneees merged 2 commits into
mainfrom
feature/tool-calling
Oct 31, 2026
Merged

jkyberneees merged 2 commits into
mainfrom
feature/tool-calling

Conversation

@jkyberneees

Copy link
Copy Markdown
Contributor

What

Tool calling for the OpenAI-compatible server, which previously rejected any request with tools.

  • Chat Completions: tools, tool_choice (auto / none / required / named), parallel_tool_calls, assistant tool_calls, tool messages. Responses use content: null when only calls are made and finish_reason: "tool_calls". Streaming sends id, type and name first, then the arguments.
  • Responses API: flat function tools, function_call / function_call_output input items, function_call output items with output_item.added, function_call_arguments.delta / .done and output_item.done events. tools, tool_choice and parallel_tool_calls are echoed in the response object.
  • Native prompt formats:
    • Hermes (Qwen2.5, Qwen3), Llama 3.1+ and Mistral v3.
    • Golden tests check byte-for-byte equality with transformers.apply_chat_template on the official Qwen2.5 / Qwen3 / Llama 3.2 templates and with mistral-common 1.13.
    • Template quirks are reproduced: Qwen2.5 double braces, Llama JSON-quoted results, Mistral 9-character call ids.
  • tool_choice: required: the call opener is prefilled and the function name is constrained to the defined tools during decoding. Named choices prefill the name.
  • Mistral v0.3: its tool_use template is read from the named template list.
  • Safety:
    • Client text (definitions, arguments, results) is tokenized as plain text.
    • Validation: at most 128 tools, OpenAI name rule, unique names, JSON object arguments, tool results must answer an earlier call, nesting limit of 64.
    • Deprecated functions / function_call get a 400.
    • Models without a tool format (Phi-3.5) get a 400.

Testing

  • swift test: 111 tests pass, 28 of them new (golden templates, parser, streaming gate, JSON, name constraint, request validation).
  • Greedy output without tools is token-identical to 1.0.3 on Qwen3 0.6B, Qwen2.5 0.5B, Llama 3.2 1B, Phi-3.5 and Mistral 7B.
  • End to end with the official openai Python SDK 3.42.0, 17 checks per model: Qwen3 0.6B, Qwen3 8B, Qwen2.5 0.5B and Mistral 7B pass 17/17. Llama 3.2 1B passes 15/17: after a tool result it calls another tool, and mlx-lm with the official template does the same, so this is the model's limit.

🤖 Generated with Claude Code

…ive formats per model (1.1.0)

- Tools.swift: order-preserving JSON (Python json.dumps formatting), ToolDefinition/ToolCall,
  ToolCallParser (Hermes, Llama 3.1+, Mistral v3), ToolStreamGate for streaming.
- ChatTemplate: tool rendering matching the official Qwen2.5/Qwen3/Llama 3.2 templates and
  mistral-common v3 byte for byte (golden tests); tool/assistant-call history; forced-call prefill.
- Engine: tools and prefill through context trimming, tool-call marker tokens revealed only
  with tools, constrained function-name decoding for tool_choice "required".
- Server: request validation, OpenAI response and streaming shapes for both APIs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Failed ❌

View logs ↗
007398e 2026-10-31T17:28:10.079Z View logs ↗
  • Build: Failed ❌

View logs ↗
fb57fae 2026-10-31T17:17:55.004Z View logs ↗

- A forced call cut off by max_tokens reports finish_reason "length" instead of a 500.
- The function-name constraint runs through the comma after the name, so a forced call cannot
  close before its arguments; masking saves the allowed logits instead of hashing every id.
- Whole-body key-ordered parse allows 256 levels; the 64-level cap applies to tools only.
- One table of revealed tool-call markers; markers are stripped from unparsed output.
- Removed unused JSONValue.from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jkyberneees
jkyberneees merged commit e61f62b into main Oct 31, 2026
4 of 5 checks passed
@jkyberneees
jkyberneees deleted the feature/tool-calling branch October 31, 2026 17:29
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