Skip to main content
All guides
Developer guides

Function calling with Mistral: the practical guide

Function calling on Mistral is OpenAI-compatible in shape, but with stricter JSON-schema validation. Here's the working pattern.

Last updated July 18, 2026

What function calling with Mistral gets you

Function calling — sometimes called tool use — lets a Mistral model return structured JSON that matches a tool definition you supplied, instead of free text. That JSON is what your code executes. The pattern turns a conversational LLM into a router for real actions: database reads, third-party API calls, code execution, retrieval. Every serious agent framework on Mistral is built on this primitive.

The tool schema

You pass a `tools` array where each tool has a name, description, and JSON-schema parameters block. Mistral requires strict Draft-07 with `additionalProperties: false` on every object — loose schemas are the number-one cause of `invalid_tool_schema` 400s that the docs describe but the error surface makes hard to debug. Write your schemas with a validator in your test suite before you ever call the API.

The two-step dance

  • First call: the model returns `tool_calls` on the assistant message. There is no `content` text yet.
  • You execute the tool locally and append a `role: tool` message with the result, keyed to the `tool_call_id`.
  • Second call: the model reads the tool result and writes the final assistant reply.

Parallel tool calls

Mistral can emit multiple tool_calls in a single turn. Execute them in parallel — Promise.all in Node, asyncio.gather in Python — then append all the `role: tool` messages before the next model call. Do not serialize unless the tools genuinely depend on each other. Latency compounds across sequential tool round-trips; the parallel case is often what makes an agent feel fast.

tool_choice: forcing and forbidding calls

Pass `tool_choice: 'auto'` to let the model pick; `'any'` to force some tool call; `'none'` to disable tools for this turn. Pass an object like `{ type: 'function', function: { name: 'my_tool' } }` to force a specific tool. Note that Mistral uses `'any'` where OpenAI uses `'required'` — the semantics are the same but the string differs.

Common failure modes

  • Schema uses `type: 'any'` or omits `additionalProperties: false` → 400 invalid_tool_schema.
  • You send the model back without a `role: tool` message → it hallucinates a plausible result.
  • You put natural-language errors in the tool response → the model treats them as data. Return structured `{ error: '...' }` instead so it can plan a retry.
  • You forget to include `tool_call_id` on the response message → the model cannot match the reply to the call.
  • You mutate the conversation history between calls → the second call sees a different tool_calls block than the first emitted, and refuses to continue.

Which Mistral models support tools

Commercial Mistral Large, Mistral Small, and Ministral all support tool calling with the same schema. Codestral is a code-specialist model and does not — reach for Large if you need tools in a code workflow. Open-weight releases (Mistral 7B, Mixtral) support tools only when you run them through a serving layer that adds the harness.

Testing your tool schema

Write a Jest or pytest suite that validates each tool schema against a Draft-07 JSON-schema validator. Then run a smoke test that hits Mistral with an obviously tool-triggering prompt for each tool and asserts the returned tool_calls parses. This catches 90% of the surface errors before they reach production and turns invalid_tool_schema from a mystery 400 into a green light in CI.

Streaming and tool_calls

Mistral supports SSE streaming for chat completions, including turns that emit tool_calls. The `tool_calls` field arrives incrementally — you'll see the function name first, then the arguments string built up chunk by chunk. Do not try to JSON.parse the arguments until the stream signals finish_reason. Buffer the argument string across chunks, parse once at the end, and dispatch the tool call. Naive parsers that run on every chunk will throw and be swallowed silently in production.

Debugging invalid_tool_schema quickly

When a request 400s with invalid_tool_schema, do not guess. Copy the tool JSON out of your request, drop it into an online Draft-07 validator, and read the error path. Nine times out of ten the fix is one of: missing `additionalProperties: false` on a nested object, a `type: 'any'` that should be `type: 'string'` with an enum, or a `required` array that names a property the schema does not actually declare. Fix in isolation, then re-run the request.

Agents built on Mistral tool use

An agent is a loop: model call → tool_calls → execute tools → append role: tool messages → model call again → repeat until the model returns a plain assistant message with no tool_calls. Mistral's `agents` endpoint wraps this loop for you, but rolling your own is a hundred lines of code and gives you the observability hooks production actually needs. Log every step with timing so you can spot the tool call that dominates end-to-end latency.

End-to-end example: weather tool in ~30 lines

Define the tool schema: `{ type: 'function', function: { name: 'get_weather', description: 'Get current weather for a city', parameters: { type: 'object', additionalProperties: false, required: ['city'], properties: { city: { type: 'string', description: 'City name, e.g. Paris' } } } } }`. First call: `client.chat.complete({ model: 'mistral-large-latest', messages: [{ role: 'user', content: 'Weather in Paris?' }], tools: [weatherTool], tool_choice: 'auto' })`. The response comes back with `choices[0].message.tool_calls = [{ id: 'call_abc', function: { name: 'get_weather', arguments: '{"city":"Paris"}' } }]`. Parse the arguments string with `JSON.parse`, execute your real function, then append two messages to history: the assistant message with `tool_calls`, and a `{ role: 'tool', tool_call_id: 'call_abc', content: '{"temp_c":18,"conditions":"cloudy"}' }` message. Second call with the extended history returns the plain assistant reply — 'It's 18°C and cloudy in Paris right now.' That is the entire pattern; every agent framework is variations on it.

Mistral vs OpenAI function calling: what actually differs

The shape is identical — same `tools` array, same `tool_calls` on the assistant message, same `role: 'tool'` for results. The differences are small but bite in production. Mistral uses `tool_choice: 'any'` where OpenAI uses `'required'`; both mean 'force some tool call'. Mistral validates JSON schema more strictly — `additionalProperties: false` is effectively mandatory on every object, and `type: 'any'` will 400 where OpenAI silently tolerates it. Mistral's parallel-tool-call behavior is the same, but the specific triggering prompts differ slightly because model-side tool routing is trained differently. Streaming payload shapes match. Migration from OpenAI to Mistral is usually a base URL swap plus tightening any loose schemas your OpenAI code got away with.

Common pitfalls that waste half a day

The five failures we see repeatedly: shipping without a JSON-schema validator in CI (invalid_tool_schema in prod is embarrassing and completely preventable); parsing tool_call arguments incrementally during streaming instead of buffering (throws swallowed silently); forgetting the `tool_call_id` on the `role: tool` message so the model cannot match the reply; putting free-text errors in tool responses instead of structured `{ error: '...' }` (model treats them as data and hallucinates a plan around them); and stuffing 40+ tools into every request because selection is 'the model's problem' (past ~20 tools, selection quality collapses — narrow with a classifier first). Fixing all five is under a day of work and saves a week of debugging over the next quarter.

FAQ

Which Mistral models support function calling?
The current commercial Mistral Large, Mistral Small, and Ministral all support tool calling. Codestral is code-specialist and does not — use Large for code workflows that need tools.
Can I force Mistral to call a specific tool?
Yes — pass `tool_choice: { type: 'function', function: { name: 'my_tool' } }` to force that call on the next turn. `'any'` forces some tool call; `'none'` disables tools.
Is Mistral's tool schema OpenAI-compatible?
The shape is identical, but Mistral validates JSON-schema more strictly. `additionalProperties: false` on every object is effectively required — omitting it is the leading cause of invalid_tool_schema 400s.
How do I handle parallel tool calls?
Execute them concurrently with Promise.all or asyncio.gather, then append one `role: tool` message per tool_call_id before your next model request. Do not serialize unless the tools depend on each other.
Can Mistral stream tool_calls?
Yes — SSE streaming works for turns that emit tool_calls. Arguments arrive as an incrementally built string; buffer across chunks and JSON.parse once the stream signals finish_reason. Parsing on every chunk will throw halfway through the string and often gets swallowed silently in production log pipelines, so make the buffered pattern the default.
What is the token cost of tool JSON?
Tool definitions live in every request that could trigger them, so their JSON counts against input tokens on every call. Verbose descriptions and generous examples add up fast at production QPS. Keep tool descriptions tight, prefer enums over free-text where possible, and audit your definitions quarterly to remove tools that no longer see traffic.
How many tools is too many?
There is no hard cap, but selection quality degrades past roughly twenty tools per request because the model has to reason across all of them. If you are pushing forty-plus tools, route the request through a cheap classifier that narrows to the five or ten tools relevant to the current turn and only send those. Latency and quality both improve.
Do I need to validate arguments before executing?
Yes, always. The model returns arguments as JSON that matches your schema in shape but can hallucinate values — an invented user ID, a nonexistent enum member, a date in the wrong format. Validate against your schema and against real business constraints before dispatch, and return a structured `{ error }` object if it fails so the model can retry with corrected arguments.

Related on this site

Keep reading