Function Calling in DeepSeek: Which Models and What Format
8 min read · updated August 11, 2026
DeepSeek implements the OpenAI tool-calling schema field for field, so an existing client works unchanged. The differences are in which endpoint accepts it and in a reliability caveat DeepSeek publishes about its own implementation.
Which endpoints expose it
Function calling is a documented feature of the chat endpoint. Whether the reasoning endpoint accepts tools has changed across model versions — it was listed among the unsupported features for R1 and later became available in some configurations — which makes this the one fact in this page you should verify against the current docs rather than remember.
tools is configuration, not a constant, and you will not have to redeploy to follow it.There is a cheap runtime probe that beats reading release notes: send a one-token request with a trivial tool definition and tool_choice: "none". If the endpoint does not support tools you get a 400 naming the parameter; if it does you get a normal completion for a few tokens of spend. Run it once at startup, cache the answer, and log which way it went.
The request shape
Tools go in a top-level tools array. Each entry is an object with type: "function" and a function object holding name, description and parameters, where parameters is JSON Schema. tool_choice accepts "auto", "none", or an object naming one function to force.
curl https://api.deepseek.com/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $DEEPSEEK_API_KEY" \
-d '{
"model": "deepseek-chat",
"messages": [
{"role": "user", "content": "What is the weather in Amsterdam right now?"}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current conditions for a city. Call this whenever the user asks about weather; do not guess.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. Amsterdam"},
"unit": {"type": "string", "enum": ["c", "f"], "default": "c"}
},
"required": ["city"]
}
}
}
],
"tool_choice": "auto"
}'Two things about that description are load-bearing and are the most common reason a tool never gets called. It says when to call, not what the function does internally, and it forbids the alternative. A model deciding between answering from memory and calling a tool is doing next-token prediction over your description; a description that reads like an API reference gives it nothing to predict from.
The whole array is serialised into the prompt and billed as input tokens on every request, whether a tool is called or not. Ten thoroughly documented functions is a fixed several-thousand-token overhead per call. If you have a large tool surface, selecting a subset per request is a real cost lever and not premature optimisation.
The response shape
When the model decides to call, content is null and tool_calls is an array on the assistant message. Each call has an id, a type, and a function object with name and arguments. The finish_reason is tool_calls, which is the field to branch on rather than inspecting whether content is empty.
{
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_0_a1b2c3d4",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Amsterdam\",\"unit\":\"c\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
]
}arguments is a string containing JSON, not a JSON object. That is inherited from the OpenAI schema and it is deliberate — the field has to survive being streamed in fragments — but it means every consumer must call json.loads on it, and every consumer must handle that call failing. The model is generating that string token by token with no hard guarantee of validity, so treat a parse failure as a normal branch: retry once, and if it fails again return a tool error message rather than raising.
Closing the loop
The round trip is four messages, and the third one is where most implementations go wrong. After executing the function you append both the assistant message that requested the call and a tool message carrying the result, and the tool message must repeat the tool_call_id exactly.
messages = [
{"role": "user", "content": "What is the weather in Amsterdam right now?"},
{"role": "assistant", "content": None, "tool_calls": [
{"id": "call_0_a1b2c3d4", "type": "function",
"function": {"name": "get_weather",
"arguments": '{"city":"Amsterdam","unit":"c"}'}}]},
{"role": "tool", "tool_call_id": "call_0_a1b2c3d4",
"content": '{"temp_c": 17, "condition": "light rain"}'},
]
# second call, same tools array, produces the natural-language answerDropping the assistant turn and sending only the tool result is the classic bug. It produces a message sequence in which a result answers nothing, and the model responds by calling the tool again — which looks like a loop caused by the model and is actually a loop caused by the history. If you see repeated identical calls, print the exact messages array you sent before you blame the model.
The content of a tool message is a string. Serialise structured results with json.dumps and keep them small; the result is re-sent as input on every subsequent turn of the conversation, so a verbose tool response is a recurring cost rather than a one-off.
The documented instability
DeepSeek’s own function-calling documentation has carried a warning that the feature can produce looped calls or empty responses, and advises that it is being improved. That is an unusual thing for a vendor to publish and it should change how you build: assume the loop can happen for reasons that are not your message array.
- Cap the tool-call round trips per user request — a hard counter, typically five, after which you stop and return whatever you have. Without it, a looping call is an unbounded bill.
- Detect repeats by content, not by count. Hash
nameplus normalisedarguments; if the same pair comes back twice in a row, break and feed the model the result it already has with an instruction to answer. - Handle the empty response. A completion with no
contentand notool_callsis a documented possibility. One retry with the same input resolves most of these; two failures should fall through to a different model rather than to an error page. - Do not force a tool to fix looping.
tool_choicepinned to one function guarantees a call every turn, which makes a loop certain rather than unlikely.
Getting the right tool called
Once the plumbing works, the remaining failures are all about selection: no tool called when one was needed, the wrong one called, or the right one called with bad arguments. None of those is fixed by the request format, and all of them are shaped by how the tools are described.
- Write descriptions for the decision, not for the implementation. The model reads them to decide whether to call. “Returns current weather from the provider API” describes the function; “Call whenever the user asks about current or forecast weather anywhere; do not answer from memory” describes the decision. The second is what changes behaviour.
- Constrain arguments in the schema rather than in prose. An
enumfor a field with fixed values, arequiredlist, aformatfor dates. Constraints in the schema are structural; the same constraint mentioned in a description is a suggestion the model may not follow. - Keep the surface small. Selection accuracy falls as the number of similar tools rises, and near-duplicates are the worst case — two functions whose descriptions overlap force a coin flip. Merge them behind one function with an enum parameter, or select a subset per request from what the user is doing.
- Give the model somewhere to put uncertainty. If there is no way to ask a clarifying question, a model missing a required argument will invent one. A tool that returns a question to the user, or an explicit instruction to ask rather than guess, is what prevents a plausible fabricated city name reaching your API.
- Return errors as tool results, not as exceptions. When your function fails, feed a
toolmessage describing the failure back into the conversation. The model can then retry with different arguments or explain the problem, which is almost always a better outcome than an error page — and it is the same message shape as a success.