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 one API feature matter more than any other, function calling, and it’s the one feature launch-week guides haven’t touched. Every tutorial so far stops at chat completions.
This one goes further: define a tool schema, make your first tool call with the standard Python openai SDK, build the full agent loop, then test the whole thing in Apidog before your agent ships. If you don’t have a DeepSeek API key yet, set one up with our guide to how to use the DeepSeek V4 API, then come back.
TL;DR
deepseek-v4-pro(GA build DeepSeek-V4-Pro-0813) supports OpenAI-style function calling: send atoolsarray, receivetool_calls, return results astoolmessages. The standardopenaiSDK works againsthttps://api.deepseek.com.- The full agent loop is about 30 lines of Python: call, execute, append, repeat until the model stops requesting tools. Parallel 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 miss. That’s what makes deep agent loops affordable.
- Tool-calling quality varies with your harness and schemas. Test your actual tools against the live model, not benchmarks.
Why tool calling is V4 Pro’s headline use case
DeepSeek built V4 Pro for agents, and the spec sheet reads like an agent-runtime checklist:
| 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 |
Each line maps to an agent problem: the 1M-token window carries a long agent’s full tool-result history, the 384K output ceiling leaves room for big structured payloads, and prefix caching makes loop economics work. The model is listed on OpenRouter as deepseek-v4-pro-0813 for provider comparisons.
One caveat before the code. In the Hacker News launch discussion, developers reported tool-calling performance is highly sensitive to the harness: the same model scored better or worse depending on framework, prompt scaffolding, and schema style. Benchmarks won’t tell you how it handles your tool schemas. Test with your real definitions.
How DeepSeek function calling works
Function calling doesn’t mean the model executes anything. It responds with a structured request, “call get_order with {"order_id": "ORD-10442"}”, instead of prose. Your code runs the function, returns the result, and the model continues with real data. The cycle:
- You send
messagesplus atoolsarray describing each function in JSON Schema. - The model decides a tool is needed and responds with
tool_callsandfinish_reason: "tool_calls". - Your code parses the arguments and runs the actual function.
- You append the result as a
role: "tool"message tied to the call’s ID. - The model either requests another tool or produces its final answer.
If you’ve worked with OpenAI function calling, this is the same wire format; most agent code ports by changing the base URL and model name. The official DeepSeek docs also cover an Anthropic-compatible Messages endpoint and a Responses API, but this guide sticks to the OpenAI-compatible surface.
Step 1: Set up the client
Install the SDK and point it at DeepSeek:
pip install openai
export DEEPSEEK_API_KEY="sk-..."
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="https://api.deepseek.com",
)
That’s the entire setup. Every example uses model="deepseek-v4-pro", which resolves to the GA build DeepSeek-V4-Pro-0813.
Step 2: Define a tool schema
We’ll build a support agent for an online store. Its first tool looks up orders. A tool definition has three parts: a name, a description, and a JSON Schema for the 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 not decoration: the model decides when to call a tool by reading it. Vague descriptions are the top reason a model ignores a tool or picks the wrong one.
The local function the schema describes, stubbed in for a real order service:
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 can’t answer without the tool:
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"}
Instead of answering, the model asks you to run get_order. The raw response payload 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 matter. finish_reason is "tool_calls", which tells your loop the model wants execution. Each call carries an id you must echo back with the result. And arguments is a JSON string you parse yourself, so expect it to occasionally be malformed.
Step 4: Execute the function and return the result
Run the function, then append two messages: the assistant turn containing tool_calls, and a tool message carrying your result.
import json
tool_call = message.tool_calls[0]
args = json.loads(tool_call.function.arguments)
result = get_order(args)
messages.append(message) # the assistant turn containing tool_calls
messages.append({
"role": "tool",
"tool_call_id": tool_call.id, # must match the id from the response
"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 link is strict: every tool_calls entry needs a matching tool message before the next model turn, or the request fails.
Step 5: The full agent loop
Real agents chain calls: look up an order, check a refund policy, draft an email, each step depending on the last. The pattern: keep calling the model and executing whatever it requests until it returns a normal answer.
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: # no tool requests: we're done
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)} # feed failures back to the model
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")
Frameworks and agent SDKs are elaborations on this loop. The max_rounds cap converts a model stuck re-calling a failing tool into a clean failure instead of an open-ended bill.
Parallel tool calls
Ask for two lookups, “compare the status of ORD-10442 and ORD-10587”, and V4 Pro will often batch both into one turn:
"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 loop already handles this: the inner for answers each call with its own tool_call_id (every call needs a matching result before the next turn), and you’re free to execute the batch concurrently. It’s a different philosophy from GPT-5.6’s programmatic tool calling, where the model writes orchestration code in a sandbox; DeepSeek keeps execution, and the trust boundary, in your runtime.
Thinking mode plus tools
V4 Pro ships with three thinking modes, so you can raise reasoning effort for hard planning turns and skip it for routine lookups (see the official docs for mode names and defaults). With thinking enabled, the API returns the model’s trace as reasoning_content alongside any 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) # the planning trace
print(message.tool_calls) # the calls it settled on
The trace shows why the model picked a tool, which is usually where a bad schema reveals itself. Strip reasoning_content before appending the assistant turn to history, and reserve thinking for planning-heavy turns, reasoning bills as output at $0.87/M.
Error handling: when the model gets a call wrong
Malformed tool calls are rare, but an agent loop amplifies every failure mode. The essential pattern: never crash on a bad call, return the problem as the tool result, and let the model retry. That covers arguments that fail json.loads as well as values that break your business rules:
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 field matters: a one-line correction usually produces a fixed retry on the next round. Treat agent errors as security events too. A model talked into calling delete_order with attacker-supplied arguments is only as dangerous as the key behind it, the case for least-privilege API keys for AI agents. Scope credentials so a wrong call can’t become an incident.
Test and debug tool calls with Apidog before you ship
Every tool is a thin wrapper around an API, and the model is now a consumer of that API. If the backing endpoint is ambiguous or flaky, the model inherits all of it. This is where Apidog earns its place in the loop:
- Design the backing API first. Define
GET /orders/{order_id}as a spec in Apidog’s visual designer; your tool’s JSON Schema falls straight out of the spec, so the two can’t silently drift apart. - Mock it before the backend exists. Apidog’s smart mock serves realistic responses from the schema, so the agent loop runs against
get_orderwhile the real service is still being built. - Inspect the raw payloads. Send the same
messages+toolsbody tohttps://api.deepseek.comfrom Apidog and read the rawtool_callsJSON directly, a mis-nestedpropertiesor double-encoded arguments surface in one inspection. - Turn conversations into test scenarios. Assert on
finish_reasonand argument shapes, and run the suite on every schema change; given the harness sensitivity reported on Hacker News, a regression suite over your real schemas is the benchmark that predicts production. See wiring an AI agent into an Apidog test harness for a deeper pattern.
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 re-read the entire conversation every round: by round ten, your system prompt, tool schemas, and nine rounds of results are billed for the tenth time. V4 Pro’s automatic prefix caching breaks that curve, each round’s input is the previous round’s plus a little more, so almost the whole prefix bills at $0.003625/M instead of $0.435/M. Re-reading a 100K-token conversation costs about $0.0435 uncached but about $0.0004 cached; prompt_cache_hit_tokens in the usage block shows your actual hit rate.
To keep that rate high, never mutate earlier messages, and keep the tools array byte-stable across rounds. Our primer on what prompt caching is covers the mechanics. And if deepseek-v4-flash at $0.14/$0.28 looks tempting: it’s fine for single-shot tool routing, but it regresses on loops chaining 10+ calls, so retries eat the savings, Pro is the safer default for agents.
FAQ
Do tool definitions cost tokens?
Yes, the tools array is input on every request. Keep it stable and it joins the cached prefix after round one, billed at the cache-hit rate from then on.
Can I combine function calling with structured outputs?
Yes. A common pattern: tools fetch the intermediate data, a structured output schema formats the final answer, so downstream code never parses prose.
Wrapping up
Function calling on DeepSeek V4 Pro is deliberately unexciting to implement: OpenAI-compatible schemas, a tool_calls array, a tool message with an ID. The loop in Step 5 is the whole architecture, and cache-hit pricing makes it cheaper than most teams expect. What benchmarks can’t tell you is how the model behaves against your schemas, design the backing APIs deliberately, mock them early, and keep a regression suite of tool-call scenarios in Apidog so schema changes can’t silently break your agent.



