Skip to content

Guides

Tool calling

Describe functions with a JSON Schema and the model answers with calls to them instead of text. Your code runs the call, sends back the result, and the model carries on. It's the loop every coding agent runs on.

The loop#

  1. Send tools with your request.
  2. If the answer has finish_reason: "tool_calls", run each call in message.tool_calls. function.arguments is a JSON string.
  3. Send the conversation back with the assistant's message and one tool message per call, matched by tool_call_id.
  4. Repeat until the model answers in text.
import jsonimport osfrom openai import OpenAIclient = OpenAI(base_url="https://fullsendai.com/v1", api_key=os.environ["FULLSEND_API_KEY"])tools = [{    "type": "function",    "function": {        "name": "get_weather",        "description": "Current weather for a city",        "parameters": {            "type": "object",            "properties": {"city": {"type": "string"}},            "required": ["city"],        },    },}]def get_weather(city):  # your code    return {"city": city, "temp_c": 18, "sky": "clear"}messages = [{"role": "user", "content": "What's the weather in Paris?"}]while True:    reply = client.chat.completions.create(model="deepseek-v4.1-flash", messages=messages, tools=tools)    message = reply.choices[0].message    messages.append(message)    if not message.tool_calls:        break    for call in message.tool_calls:        result = get_weather(**json.loads(call.function.arguments))        messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})print(message.content)
Response with a tool call
{  "choices": [    {      "message": {        "role": "assistant",        "content": null,        "tool_calls": [          { "id": "call_34087531920d0de5cb6cc3d1", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\": \"Paris\"}" } }        ]      },      "finish_reason": "tool_calls"    }  ]}

Tool call IDs are FullsendAI's own (call_…). Send them back exactly as you got them.

Controlling tool use#

tool_choiceThe model…
"auto"decides (the default when tools are sent)
"required"must call at least one tool
"none"answers in text
{"type": "function", "function": {"name": "get_weather"}}must call that tool

parallel_tool_calls is passed to the model; models that support it can return several calls at once, so always loop over tool_calls.

Streaming tool calls#

With stream: true, calls arrive in pieces in delta.tool_calls. The first piece of each call has its index, id and function.name; later pieces add to function.arguments. Join them by index:

Python
calls = {}for chunk in client.chat.completions.create(model="deepseek-v4.1-flash", messages=messages, tools=tools, stream=True):    if not chunk.choices:        continue    for piece in chunk.choices[0].delta.tool_calls or []:        call = calls.setdefault(piece.index, {"id": piece.id, "name": "", "arguments": ""})        call["name"] += piece.function.name or ""        call["arguments"] += piece.function.arguments or ""

Anthropic API#

Define tools with name, description and input_schema. The model answers with tool_use blocks and stop_reason: "tool_use"; send each result back as a tool_result block in the next user message. tool_choice takes auto, any, none or a named tool. A complete example is on the Anthropic API reference. Anthropic's server tools (web search, code execution) aren't run.

Tips#

  • Tool definitions count as input tokens on every request, so keep descriptions short and the list to what the task needs.
  • Give max_tokens room: a call cut off by the limit can arrive with incomplete or empty arguments.
  • Validate arguments before running anything with side effects; the model can send values your schema doesn't allow.
  • For one structured answer rather than an action, structured output is simpler than a tool.

Stuck? Email [email protected]System statusChangelog