Function Calling in the Mistral API: tool_choice Options
9 min read · updated August 11, 2026
tool_choice is the difference between asking a model whether it wants a tool and telling it that it has no other option. Mistral exposes both, plus a way to name one tool specifically, and the response shape is different in each case.
The values and what each one means
Mistral’s function calling guide documents three values, and the chat completions API reference lists the accepted set as none, auto, any, required, or an object naming a specific tool, with auto as the default.
auto— the default. The model decides for itself whether to call a tool or answer in prose. Both outcomes are normal and your code must handle both on every call.any— forces tool use. The model must emit a call to one of the tools you supplied; a prose answer is not an available output.required— also forces a tool call, and is the spelling OpenAI-compatible clients send. If you are porting code, this is why your existing request does not error.none— prevents tool use. The tools stay in the request and stay in the context, but the model will answer in prose.- a named tool object — forces one particular function, not merely some function.
A request with the forced mode set looks like this:
curl https://api.mistral.ai/v1/chat/completions \
-H "Authorization: Bearer $MISTRAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "mistral-large-2512",
"messages": [
{"role": "user", "content": "What is the status of transaction T1001?"}
],
"tools": [
{
"type": "function",
"function": {
"name": "retrieve_payment_status",
"description": "Get the payment status of a transaction",
"parameters": {
"type": "object",
"properties": {
"transaction_id": {"type": "string"}
},
"required": ["transaction_id"]
}
}
}
],
"tool_choice": "any"
}'Why ‘any’ exists
The interesting mode is any, and the reason it exists is worth understanding rather than memorising. Under auto, the model is sampling from a distribution that includes both prose continuations and tool-call continuations. Whether you get a call depends on how confidently the prompt and the tool descriptions push it that way, which means a marginal prompt produces a tool call sometimes and an apology other times. That variance is invisible in testing and expensive in production, because the branch of your code that parses tool_calls is not exercised on the calls that returned prose.
Forcing the mode removes the branch. The decoder is constrained so that a tool call is the only valid output, so your handler always has something to parse. That is the right setting for a step in a pipeline whose entire purpose is to produce structured arguments — an extraction step, a router, a classifier expressed as a function.
It is the wrong setting for a conversational agent, and the failure is instructive. If the user says “thanks, that’s all” and you have forced a tool call, the model cannot say “you’re welcome”. It will call something, with arguments it invented, because you removed the option not to. Forced tool use converts a refusal into a fabrication, which is why the default is auto and should stay auto anywhere a human is in the loop. The related case — a model declining a call you expected — is a different problem with different causes; see when a Mistral model refuses to call your tool.
The response shape a tool call produces
When the model calls a function, the assistant message carries a tool_calls array instead of, or alongside, its content. Mistral’s function calling guide documents each entry as having a type of "function", an id, and a function object holding name and arguments:
{
"id": "cmpl-e5cc70bb28c444948073e77776eb30ef",
"object": "chat.completion",
"model": "mistral-large-2512",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "",
"tool_calls": [
{
"type": "function",
"id": "D681PevKs",
"function": {
"name": "retrieve_payment_status",
"arguments": "{\"transaction_id\": \"T1001\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
]
}Three details in there catch people out. arguments is a string containing JSON, not a JSON object — you have to parse it, and it can be malformed, so parse it defensively. The id is what you echo back: your result goes into the next request as a message with role: "tool" and a matching tool_call_id, and a mismatch is how multi-turn tool loops break. And finish_reason is tool_calls rather than stop, which is the field to branch on rather than testing whether content happens to be empty.
Sending the result back
A tool call is half a transaction. The model asked for something; it has not seen an answer. To continue, you append the assistant message that contained the call and then a message with the tool role carrying your result, and send the whole array again:
"messages": [
{"role": "user", "content": "What is the status of transaction T1001?"},
{"role": "assistant", "content": "", "tool_calls": [
{"type": "function", "id": "D681PevKs",
"function": {"name": "retrieve_payment_status",
"arguments": "{\"transaction_id\": \"T1001\"}"}}]},
{"role": "tool", "name": "retrieve_payment_status",
"tool_call_id": "D681PevKs",
"content": "{\"status\": \"Paid\", \"settled_at\": \"2026-07-31\"}"}
]Three requirements are easy to get wrong here and each produces a different failure. You must include the original assistant message with its tool_calls intact — omitting it leaves a tool result answering a question that, as far as the model can see, was never asked. The tool_call_id must match the id from the call exactly; with parallel calls in flight, a mismatch silently pairs the wrong result to the wrong request. And content on a tool message is a string, so a structured result has to be serialised, which means deciding how much of it to include: the tool’s output is now prompt tokens on every subsequent turn of the conversation.
One further point about the forced modes. On this second request, if tool_choice is still set to any or required, the model cannot summarise the result for the user — it is obliged to call something again, and will loop. Forced tool use is a per-request setting and should be flipped back to auto, or to none, on the turn where you want an answer rather than another action. An agent that spins on the same tool is very often this, not a prompting problem.
Turning tools off without removing them
none looks redundant — why send tools you have forbidden? — and it is the mode with the most specific use. Tool definitions are part of the prompt. They are tokenised, they occupy context, and on a provider with prompt caching they form part of a cached prefix. Removing them from a request changes that prefix and invalidates the cache for it.
So none lets you keep the prefix byte-identical across a conversation while suppressing calls on the turns where you want a summary rather than an action. Send the same tools every time; flip tool_choice to none for the final “now explain what you did” turn. The cache survives and the model does not go looking for another action to take.
Documented limits
Mistral’s known limitations page states three things worth designing around. The maximum number of tools per request is 128. Tool descriptions count toward token usage — they are prompt, and you pay for them on every call. And parallel function calls are supported but may return calls in any order, so your executor must not assume the array is sequenced by dependency. The parallel_tool_calls parameter defaults to true in the API reference, which means multiple calls in one response is the expected case rather than the exception; see parallel tool calls in the Mistral API.
The 128-tool cap is generous enough that few applications hit it, and it is the wrong limit to design against anyway. Long before you reach 128, the tool descriptions themselves are consuming meaningful context and, more importantly, the model’s selection accuracy is degrading: choosing correctly between eighty similar functions is a harder problem than choosing between eight, and it is the same kind of problem as retrieval over a large corpus. If your agent has thirty tools and picks the wrong one regularly, adding clearer descriptions helps less than reducing the set. The usual restructuring is to expose a small number of tools per stage of the workflow, swapping the array as the conversation moves, rather than presenting the entire surface area on every call.
The ordering caveat deserves the same treatment. “May return calls in any order” means the array is a set, not a plan. If two calls genuinely depend on each other, the dependency belongs in your orchestration — execute, return the result, let the model ask for the next thing — not in an assumption about array position that will hold in testing and fail the first time the model reorders them.
tool_choice have grown over time — required is a later addition for OpenAI compatibility. Read the set off the API reference for the version you are targeting rather than assuming a value is rejected because an older guide did not mention it.