Tool calling

Multi-turn OpenAI-shape tool loops on Qwen-family models. Cline drop-in.
Malibu speaks OpenAI-compatible tool calling on /v1/chat/completions. It supports first-turn tool_calls[], role: "tool" results sent back on later turns, assistant tool-call history, and streamed tool-call deltas. The agent loop runs on your machine. The provider Mac only turns the model’s native tool-call markup into OpenAI’s tool_calls[] shape. It never runs your tools. Anchor framework: Cline. Point its OpenAI-compatible provider at the Malibu base URL and pick a Qwen-family model. The examples below use client from Using Malibu with the OpenAI SDK.

Supported models

The provider picks a tool-call parser from the model ID. There is no per-request setting for this. Qwen-family catalog models: mlx-community/Qwen3-Coder-30B-A3B-Instruct-4bit, mlx-community/Qwen2.5-Coder-32B-Instruct-4bit, mlx-community/Qwen3-8B-4bit, mlx-community/Qwen3-32B-4bit, mlx-community/Qwen3-30B-A3B-Instruct-2507-4bit, mlx-community/Qwen3.5-27B-4bit, mlx-community/Qwen3.5-35B-A3B-4bit, mlx-community/Qwen3.6-27B-4bit, mlx-community/Qwen3.6-35B-A3B-4bit, mlx-community/Qwen3.8-27B-4bit.
Being in the catalog does not mean a model is serving right now. GET https://api.malibu.tech/v1/status lists the models that currently have ready providers.
Qwen3-Coder writes tool calls in a native XML form (<function=name><parameter=key>…). Malibu parses that form as well as the JSON form. Tool and parameter names can use the OpenAI/MCP character set [A-Za-z0-9_-], so MCP-namespaced names such as my-server__shell work.

Single turn: emitting a tool call

What you get back when the model calls a tool:
  • finish_reason is "tool_calls" and message.content is null. Any prose the model wrote around the call is dropped.
  • Each tool_calls[] entry has id (call_ plus 32 lowercase hex characters), type: "function", function.name, and function.arguments.
  • function.arguments is a JSON-encoded string that holds a JSON object, as in OpenAI’s wire shape. Call json.loads on it to get a dict.
  • You only get calls to functions you declared in tools. If the model names any other function, the output comes back as plain text. On gpt-oss models, an undeclared function name returns 502 malformed_tool_call_final_json instead.
  • You never get a partial tool call. Malformed markup comes back as plain assistant text. If generation hits max_tokens partway through a call, the response ends with finish_reason: "length".

tool_choice and parallel_tool_calls

If your agent requires a tool call, check finish_reason and re-prompt whenever it isn’t "tool_calls". On providers running the current stable CLI, one turn can return several tool calls, in the order the model wrote them. Newer provider builds, not yet in a stable release, treat an omitted or false parallel_tool_calls as one call per turn: generation stops after the first complete call. Send parallel_tool_calls: true if you want more than one call per turn. Agents that run one tool per turn don’t need to change anything.

Multi-turn: feeding the result back

Run the tool locally. Then send the whole conversation back: the assistant message with its tool_calls, plus one role: "tool" message per call with the matching tool_call_id. The provider keeps no state between turns. It validates the tool-call chain inside each request.
Rules for the history you send back:
  • Every tool_call_id must match an assistant tool_calls[].id earlier in the same request. A missing ID returns 400 invalid_tool_call_id. An ID with no matching call returns 400 tool_call_id_not_found. A second result for the same ID returns 400 duplicate_tool_call_id.
  • Tool-call IDs minted by other OpenAI-compatible providers also work. The gateway rewrites any ID that doesn’t match Malibu’s call_ format to a deterministic call_… ID before validation, and assistant/tool pairs stay matched.
  • role: "tool" content must be a string. An empty string is fine. null returns 400 invalid_request.
  • Providers on the stable release accept at most 256 messages per request. Longer histories return 400 messages_too_long, so trim or summarize long coding-agent sessions.

Streaming tool-call deltas

Set stream=True and rebuild the tool calls from the deltas the same way you would with OpenAI, grouping by tool_calls[].index. The first delta for an index carries id, type, and function.name. Later deltas append pieces of function.arguments. Joined in order, the pieces form one JSON object, identical to the non-streaming arguments string.
If a tool call fails validation after streaming has started, the stream ends with an OpenAI-style data: {"error": …} event (code malformed_tool_call or malformed_tool_call_final_json), then data: [DONE]. That error event never carries finish_reason: "tool_calls". Don’t execute tool calls from a stream that ended this way. Check the error’s retryable field: malformed_tool_call_final_json is retryable, malformed_tool_call is not.

Limits

Sizes are UTF-8 bytes of the unescaped string value. A value exactly at the cap is accepted. Request-side errors return param: null; the error code identifies which cap was hit. Malibu never silently truncates a tool result.

Buyer-side validation

Emitted tool_calls[] reflect model output, not provider-verified intent. Validate every call before you run it. See Buyer-side validation.