DeepSeek moved V4 Pro out of preview on August 12, 2026, and the launch coverage leads with agentic workflows: coding, tool use, and long-horizon tasks that chain dozens of steps without losing the thread. That positioning makes function calling the API feature that matters most—and it is the one feature most launch-week guides have not covered.
This guide implements it end to end: define a tool schema, make a tool call with the standard Python openai SDK, build an agent loop, then test the workflow in Apidog before shipping. If you do not have a DeepSeek API key, first follow this guide on how to use the DeepSeek V4 API.
TL;DR
-
deepseek-v4-pro(GA build DeepSeek-V4-Pro-0813) supports OpenAI-style function calling: send atoolsarray, receivetool_calls, and return results astoolmessages. The standardopenaiSDK works withhttps://api.deepseek.com. - A complete agent loop is roughly 30 lines of Python: call the model, execute requested tools, append results, and repeat until the model returns a normal response.
- Parallel tool calls and structured outputs are supported. Thinking mode adds
reasoning_content. - Automatic prefix caching prices cache-hit input at $0.003625 per million tokens, 120x cheaper than a cache miss.
- Tool-calling quality depends on your schemas and runtime harness. Test real tools against the live model rather than relying only on benchmarks.
Why tool calling is V4 Pro’s headline use case
DeepSeek built V4 Pro for agents, and its specifications map directly to agent-runtime requirements:
| Spec | DeepSeek V4 Pro |
|---|---|
| Architecture | Sparse MoE: 1.6T total parameters, 49B active per token |
| Context window | 1M tokens |
| Max output | 384K tokens |
| Input price | $0.435/M tokens (cache miss), $0.003625/M (cache hit) |
| Output price | $0.87/M tokens |
| Function calling | OpenAI-compatible tools array and tool_calls responses |
| Other surfaces | Anthropic Messages format, DeepSeek Responses API |
The 1M-token window can retain a long agent’s tool-result history. The 384K output ceiling leaves room for large structured payloads. Prefix caching makes repeated loop turns more affordable.
For provider comparisons, the model is listed on OpenRouter as deepseek-v4-pro-0813.
One important caveat: in the Hacker News launch discussion, developers reported that tool-calling results were sensitive to the harness. Framework choice, prompt scaffolding, and schema style can change outcomes. Validate the model against your own tools and production-like requests.
How DeepSeek function calling works
Function calling does not let the model execute code directly. Instead, the model returns a structured request such as:
{
"name": "get_order",
"arguments": "{\"order_id\":\"ORD-10442\"}"
}
Your application executes the function, sends the result back, and lets the model continue with real data.
The request cycle is:
- Send
messagesand atoolsarray describing available functions with JSON Schema. - Receive
tool_callswithfinish_reason: "tool_calls"when the model needs a tool. - Parse arguments and execute the corresponding function in your runtime.
- Append the tool result as a
role: "tool"message using the call ID. - Repeat until the model produces a final answer without tool calls.
If you have used OpenAI function calling, this is the same wire format. Most existing agent code can be adapted by changing the base URL and model name.
The official DeepSeek docs also cover Anthropic-compatible Messages and a Responses API. This article uses the OpenAI-compatible Chat Completions surface.
Step 1: Set up the client
Install the OpenAI Python SDK and configure a DeepSeek API key:
pip install openai
export DEEPSEEK_API_KEY="sk-..."
Create a client that points to DeepSeek:
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="https://api.deepseek.com",
)
The examples below use model="deepseek-v4-pro", which resolves to the GA build DeepSeek-V4-Pro-0813.
Step 2: Define a tool schema
This example creates a support agent for an online store. Its first tool retrieves an order.
A useful tool definition includes:
- A stable, descriptive function name
- A clear description explaining when the model should use it
- A JSON Schema defining valid parameters
tools = [
{
"type": "function",
"function": {
"name": "get_order",
"description": (
"Look up a customer order by its ID. Returns the order status, "
"carrier, tracking number, and estimated delivery date. Use this "
"whenever the user asks where an order is or what state it's in."
),
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The order ID, formatted like 'ORD-10442'.",
}
},
"required": ["order_id"],
},
},
}
]
The description is operational, not decorative. The model uses it to decide whether the function applies. Vague descriptions are a common reason models skip a relevant tool or select the wrong one.
Now implement the local function. This example uses an in-memory database; replace it with your API or service call.
def get_order(order_id: str) -> dict:
"""Stub for your real order service."""
fake_db = {
"ORD-10442": {
"status": "shipped",
"carrier": "DHL",
"tracking_number": "4281337005",
"estimated_delivery": "2026-08-15",
},
"ORD-10587": {
"status": "processing",
"estimated_ship_date": "2026-08-14",
},
}
return fake_db.get(order_id, {"error": f"Unknown order ID: {order_id}"})
Step 3: Make your first tool call
Send a question the model cannot answer without querying the order system:
messages = [
{"role": "system", "content": "You are a support agent for an online store."},
{"role": "user", "content": "Where is my order ORD-10442?"},
]
response = client.chat.completions.create(
model="deepseek-v4-pro",
messages=messages,
tools=tools,
)
message = response.choices[0].message
print(message.tool_calls[0].function.name)
# get_order
print(message.tool_calls[0].function.arguments)
# {"order_id": "ORD-10442"}
The model responds with a request to run get_order rather than inventing an answer.
A representative raw response looks like this:
{
"id": "chatcmpl-8f3a1c",
"object": "chat.completion",
"model": "deepseek-v4-pro",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_0_f1c29a44",
"type": "function",
"function": {
"name": "get_order",
"arguments": "{\"order_id\": \"ORD-10442\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
],
"usage": {
"prompt_tokens": 312,
"completion_tokens": 24,
"total_tokens": 336,
"prompt_cache_hit_tokens": 0,
"prompt_cache_miss_tokens": 312
}
}
Three details are important:
-
finish_reasonis"tool_calls", which tells your runtime to execute requested functions. - Every call has an
id. You must return that ID in the tool result. -
argumentsis a JSON string, so parse and validate it before using it.
Step 4: Execute the function and return the result
After receiving a tool call:
- Parse the function arguments.
- Execute the local function.
- Append the assistant message containing
tool_calls. - Append a
toolmessage containing the result.
import json
tool_call = message.tool_calls[0]
args = json.loads(tool_call.function.arguments)
result = get_order(**args)
messages.append(message)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result),
})
final = client.chat.completions.create(
model="deepseek-v4-pro",
messages=messages,
tools=tools,
)
print(final.choices[0].message.content)
# Your order ORD-10442 shipped with DHL and is estimated to arrive
# by August 15, 2026. Tracking number: 4281337005.
The tool_call_id mapping is strict. Every tool_calls entry must receive a matching tool response before you make the next model request.
Step 5: Build the full agent loop
Production agents often chain calls: retrieve an order, check a refund policy, create a support ticket, then draft a response.
The core loop is simple:
- Call the model.
- Check for tool calls.
- Execute each requested tool.
- Return each result.
- Repeat until there are no more tool calls.
import json
TOOLS_BY_NAME = {
"get_order": get_order,
}
def run_agent(client, messages, tools, max_rounds=10):
"""Run the model until it produces a final answer or hits the cap."""
for _ in range(max_rounds):
response = client.chat.completions.create(
model="deepseek-v4-pro",
messages=messages,
tools=tools,
)
message = response.choices[0].message
messages.append(message)
if not message.tool_calls:
return message.content
for tool_call in message.tool_calls:
fn = TOOLS_BY_NAME.get(tool_call.function.name)
try:
if fn is None:
raise ValueError(f"Unknown tool: {tool_call.function.name}")
args = json.loads(tool_call.function.arguments)
result = fn(**args)
except Exception as exc:
result = {"error": str(exc)}
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result),
})
raise RuntimeError(f"Agent did not finish within {max_rounds} rounds")
Use a max_rounds limit. It turns a stuck agent—such as one repeatedly calling a failing tool—into a bounded failure instead of an open-ended bill.
Parallel tool calls
For a request such as “Compare the status of ORD-10442 and ORD-10587,” V4 Pro can return multiple calls in a single response:
"tool_calls": [
{
"id": "call_0_a7d1",
"type": "function",
"function": {
"name": "get_order",
"arguments": "{\"order_id\": \"ORD-10442\"}"
}
},
{
"id": "call_1_b3e9",
"type": "function",
"function": {
"name": "get_order",
"arguments": "{\"order_id\": \"ORD-10587\"}"
}
}
]
The run_agent implementation already handles this because it iterates over every tool_call and returns a result for each tool_call_id.
If calls are independent, execute them concurrently in your runtime to reduce latency. Regardless of execution order, return one tool message for every requested call before sending the next model turn.
This differs from GPT-5.6’s programmatic tool calling, where the model writes orchestration code in a sandbox. With DeepSeek function calling, tool execution and the trust boundary remain in your application runtime.
Thinking mode plus tools
V4 Pro includes three thinking modes. Use additional reasoning for planning-heavy turns and skip it for routine retrieval. Refer to the official docs for mode names and defaults.
With thinking enabled, the API returns reasoning_content alongside the tool calls:
response = client.chat.completions.create(
model="deepseek-v4-pro",
messages=messages,
tools=tools,
extra_body={"thinking": {"type": "enabled"}},
)
message = response.choices[0].message
print(message.reasoning_content)
print(message.tool_calls)
The reasoning trace can help diagnose tool selection and schema problems. Before appending the assistant turn to persistent history, strip reasoning_content. Also use thinking selectively: reasoning is billed as output at $0.87/M tokens.
Error handling: when the model gets a call wrong
Malformed calls may be uncommon, but an agent loop amplifies failures. Do not crash the entire workflow on invalid arguments. Instead:
- Parse the JSON.
- Validate it against the same schema you sent to the model.
- Return a structured error as the tool result.
- Let the model retry with corrected arguments.
import json
from jsonschema import ValidationError, validate
schema = tools[0]["function"]["parameters"]
try:
args = json.loads(tool_call.function.arguments)
validate(instance=args, schema=schema)
result = get_order(**args)
except (json.JSONDecodeError, ValidationError) as exc:
result = {
"error": f"Invalid arguments: {exc}",
"hint": "Call get_order again with an order_id string like 'ORD-10442'.",
}
The hint makes retries more likely to succeed because it gives the model a concrete correction.
Treat tool errors as security concerns, too. If an attacker can influence a model into calling a destructive function, the impact depends on the credentials available to that function. Apply least-privilege API keys for AI agents so an incorrect or malicious call cannot become an incident.
Test and debug tool calls with Apidog before you ship
Every tool is a wrapper around an API, which means the model becomes another consumer of that API. Ambiguous, inconsistent, or flaky endpoints produce unreliable agent behavior.
Use Apidog to test the API and the agent workflow together:
Design the backing API first. Define
GET /orders/{order_id}in Apidog’s visual designer. Build the tool JSON Schema from the API contract so the API and tool definition do not drift apart.Mock before the backend exists. Apidog’s smart mock can return realistic schema-based responses, letting you test
get_orderand the agent loop while the production service is still being implemented.Inspect raw model payloads. Send the same
messagesandtoolsbody tohttps://api.deepseek.comfrom Apidog. Inspect the rawtool_callsJSON to catch issues such as incorrectly nestedpropertiesor double-encoded arguments.Turn conversations into regression tests. Assert on
finish_reason, selected tool names, and argument shapes. Run those scenarios whenever schemas change. Given the harness sensitivity reported on Hacker News, tests based on your real tools are a more useful production signal than generic benchmarks.
For a deeper implementation pattern, see wiring an AI agent into an Apidog test harness.
Download Apidog free to follow along; the mock server and test scenarios are included in the free tier.
What agent loops cost—and why caching decides it
Agent loops resend conversation history on every round. By round 10, the system prompt, tool schemas, and prior tool results have all been included repeatedly.
V4 Pro’s automatic prefix caching changes the economics. Each turn generally contains the previous turn’s content plus new messages, so the repeated prefix can be billed at $0.003625/M tokens instead of $0.435/M tokens.
For example, re-reading a 100K-token conversation costs approximately:
- Uncached: $0.0435
- Cached: $0.0004
Check prompt_cache_hit_tokens in the usage block to measure actual cache reuse.
To maximize cache hits:
- Do not mutate earlier conversation messages.
- Keep the
toolsarray byte-stable across rounds. - Avoid changing system prompts or tool descriptions during a run.
- Append new messages instead of rewriting prior history.
For more background, see what prompt caching is.
If deepseek-v4-flash at $0.14/$0.28 looks appealing, it can work for one-shot tool routing. However, for loops that chain 10 or more calls, retries can eliminate the apparent savings. Pro is the safer default for multi-step agents.
FAQ
Do tool definitions cost tokens?
Yes. The tools array is part of the input for every request. Keep it stable, and it becomes part of the cached prefix after the first round.
Can I combine function calling with structured outputs?
Yes. A common architecture is:
- Tools fetch or mutate intermediate data.
- The model produces the final response using a structured output schema.
- Downstream application code consumes validated data rather than parsing prose.
Wrapping up
Function calling on DeepSeek V4 Pro is intentionally straightforward: OpenAI-compatible schemas, a tool_calls array, and tool messages linked by call IDs.
The agent loop in Step 5 is the core architecture. Add validation, bounded retries, least-privilege credentials, and regression tests around it. Prefix caching makes repeated tool turns substantially cheaper, but your schemas and harness still determine real-world reliability.
Design backing APIs deliberately, mock them early, and maintain a tool-call test suite in Apidog so schema changes do not silently break your agent.
Top comments (0)