DEV Community

Cover image for How to Use Function Calling with DeepSeek V4 Pro API
Hassann
Hassann

Posted on Originally published at apidog.com

How to Use Function Calling with DeepSeek V4 Pro API

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.

Try Apidog today

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 a tools array, receive tool_calls, and return results as tool messages. The standard openai SDK works with https://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\"}"
}
Enter fullscreen mode Exit fullscreen mode

Your application executes the function, sends the result back, and lets the model continue with real data.

The request cycle is:

  1. Send messages and a tools array describing available functions with JSON Schema.
  2. Receive tool_calls with finish_reason: "tool_calls" when the model needs a tool.
  3. Parse arguments and execute the corresponding function in your runtime.
  4. Append the tool result as a role: "tool" message using the call ID.
  5. 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-..."
Enter fullscreen mode Exit fullscreen mode

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",
)
Enter fullscreen mode Exit fullscreen mode

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"],
            },
        },
    }
]
Enter fullscreen mode Exit fullscreen mode

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}"})
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

Three details are important:

  1. finish_reason is "tool_calls", which tells your runtime to execute requested functions.
  2. Every call has an id. You must return that ID in the tool result.
  3. arguments is 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:

  1. Parse the function arguments.
  2. Execute the local function.
  3. Append the assistant message containing tool_calls.
  4. Append a tool message 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.
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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\"}"
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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:

  1. Parse the JSON.
  2. Validate it against the same schema you sent to the model.
  3. Return a structured error as the tool result.
  4. 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'.",
    }
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.

  2. Mock before the backend exists. Apidog’s smart mock can return realistic schema-based responses, letting you test get_order and the agent loop while the production service is still being implemented.

  3. Inspect raw model payloads. Send the same messages and tools body to https://api.deepseek.com from Apidog. Inspect the raw tool_calls JSON to catch issues such as incorrectly nested properties or double-encoded arguments.

  4. 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 tools array 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:

  1. Tools fetch or mutate intermediate data.
  2. The model produces the final response using a structured output schema.
  3. 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)