Guides

Tool calling

Let deepseek-flash call your functions, using the OpenAI tools format, with a complete multi-turn example.

On this page

Tool calling (function calling) lets the model ask your code to run a function and then use the result in its answer. It uses the OpenAI tools format unchanged: you describe functions with JSON Schema, the model replies with tool_calls, you run them and send the results back as tool messages.

The loop

  1. Send the conversation with a tools list.
  2. If the model wants a tool, the response has finish_reason: "tool_calls" and message.tool_calls.
  3. Run each call in your code.
  4. Append the assistant message (with its tool_calls) and one tool message per call, each with the matching tool_call_id.
  5. Send the conversation again. Repeat until finish_reason is "stop".

Each round trip is a separate request and is billed separately.

Complete example

This example gives the model one function, get_order_status, and runs the loop until the model answers in plain text.

import json
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.larasrouter.com/v1",
    api_key=os.environ["LARAS_API_KEY"],
)

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_order_status",
            "description": "Look up the shipping status of an order by its ID.",
            "parameters": {
                "type": "object",
                "properties": {
                    "order_id": {"type": "string", "description": "Order ID, e.g. INV-1042"}
                },
                "required": ["order_id"],
            },
        },
    }
]

def get_order_status(order_id: str) -> dict:
    # Your real lookup goes here.
    return {"order_id": order_id, "status": "shipped", "courier": "JNE", "eta_days": 2}

messages = [
    {"role": "system", "content": "You are a helpful support agent for an online shop."},
    {"role": "user", "content": "Where is my order INV-1042?"},
]

while True:
    response = client.chat.completions.create(
        model="deepseek-flash",
        messages=messages,
        tools=tools,
    )
    message = response.choices[0].message

    if not message.tool_calls:
        print(message.content)
        break

    # Keep the assistant turn that asked for the tools, then answer each call.
    messages.append(
        {
            "role": "assistant",
            "content": message.content,
            "tool_calls": [call.model_dump() for call in message.tool_calls],
        }
    )
    for call in message.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_order_status(**args)
        messages.append(
            {"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)}
        )

What goes over the wire

The first response asks for the tool:

Response 1 — the model calls a tool
{
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "",
        "tool_calls": [
          {
            "id": "call_0",
            "type": "function",
            "function": { "name": "get_order_status", "arguments": "{\"order_id\": \"INV-1042\"}" }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}

The second request sends the whole conversation back, including the tool result:

Request 2 — messages
[
  { "role": "system", "content": "You are a helpful support agent for an online shop." },
  { "role": "user", "content": "Where is my order INV-1042?" },
  {
    "role": "assistant",
    "content": "",
    "tool_calls": [
      {
        "id": "call_0",
        "type": "function",
        "function": { "name": "get_order_status", "arguments": "{\"order_id\": \"INV-1042\"}" }
      }
    ]
  },
  {
    "role": "tool",
    "tool_call_id": "call_0",
    "content": "{\"order_id\": \"INV-1042\", \"status\": \"shipped\", \"courier\": \"JNE\", \"eta_days\": 2}"
  }
]

And the model answers: "Your order INV-1042 has shipped with JNE and should arrive in about 2 days."

Controlling tool use

tool_choiceBehaviour
"auto" (default when tools are present)The model decides whether to call a tool.
"none"The model answers in text and calls no tool.
"required"The model must call at least one tool.
{"type": "function", "function": {"name": "get_order_status"}}The model must call that function.

Tips

  • Validate arguments. function.arguments is a JSON string written by the model. Parse it defensively and check it against your schema before acting on it.
  • Several calls at once. A response can contain more than one entry in tool_calls. Answer every one, each with its own tool_call_id, before the next request.
  • Streaming. With "stream": true, tool calls arrive as delta.tool_calls fragments; concatenate function.arguments by index until finish_reason is "tool_calls".
  • Token cost. Tool definitions are part of the prompt and count as input tokens on every request. Long, stable prefixes — system prompt, tools — are often served from cache at the lower price.