How to Use Function Calling with DeepSeek V4 Pro API

Hands-on guide to DeepSeek V4 Pro function calling: tool schemas, the full Python agent loop, parallel tool calls, thinking mode, error handling, caching costs, and testing tool calls in Apidog.

INEZA Felin-Michel

INEZA Felin-Michel

13 August 2026

How to Use Function Calling with DeepSeek V4 Pro API

Apidog for Enterprise

On-Premises Deploy

SSO & RBAC

SOC 2 Compliant

Explore Apidog Enterprise

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.

button

TL;DR

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:

  1. You send messages plus a tools array describing each function in JSON Schema.
  2. The model decides a tool is needed and responds with tool_calls and finish_reason: "tool_calls".
  3. Your code parses the arguments and runs the actual function.
  4. You append the result as a role: "tool" message tied to the call’s ID.
  5. 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:

  1. 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.
  2. Mock it before the backend exists. Apidog’s smart mock serves realistic responses from the schema, so the agent loop runs against get_order while the real service is still being built.
  3. Inspect the raw payloads. Send the same messages + tools body to https://api.deepseek.com from Apidog and read the raw tool_calls JSON directly, a mis-nested properties or double-encoded arguments surface in one inspection.
  4. Turn conversations into test scenarios. Assert on finish_reason and 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.

button

Explore more

How to Remove the Claude Watermark?

How to Remove the Claude Watermark?

Claude now embeds an invisible watermark in every text output. Here's what it actually is, what survives editing, and how to strip it with the open-source watermarks-remover tool.

13 August 2026

How to Use DeepSeek V4 Pro 0813 API ?

How to Use DeepSeek V4 Pro 0813 API ?

DeepSeek V4 Pro is GA as build 0813. Call the deepseek-v4-pro API with Python: setup, thinking modes with reasoning_content, streaming, tool calling, and 120x prompt-caching savings.

13 August 2026

How to Test and Debug Grok 4.6 API Requests (Streaming, Tool Calls, and Errors)

How to Test and Debug Grok 4.6 API Requests (Streaming, Tool Calls, and Errors)

A practical workflow for testing Grok 4.6 API integrations: debug SSE streaming stalls, validate tool-call payloads, handle 429s and retries, and mock Grok responses for fast, free CI.

13 August 2026

Practice API Design-first in Apidog

Discover an easier way to build and use APIs

How to Use Function Calling with DeepSeek V4 Pro API