Skip to content
Function Calling

Function Calling

Describe your functions in the request and the model replies with the calls it wants made. Your code runs them and sends the results back: the API returns tool calls and does not run a tool itself.

POST https://api.shannon-ai.com/v1/chat/completions

The samples on this page use /v1/chat/completions. The same loop runs on /v1/responses and /v1/messages; where a format spells something differently, the sections below show all three.

import json
from openai import OpenAI

client = OpenAI(api_key="YOUR_API_KEY", base_url="https://api.shannon-ai.com/v1")

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get the current weather for a city.",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "City name, for example Paris"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
            },
            "required": ["city"],
        },
    },
}]

def get_weather(city, unit="celsius"):
    # Your code: ask a weather service here.
    return {"city": city, "temperature": 18, "unit": unit, "conditions": "cloudy"}

messages = [{"role": "user", "content": "What is the weather in Paris right now?"}]
answer = None

for _ in range(5):  # the API sets no limit on rounds: set your own
    response = client.chat.completions.create(
        model="Kimi-K3-3BIT-REAP",
        messages=messages,
        tools=tools,
    )
    choice = response.choices[0]
    if choice.finish_reason != "tool_calls":
        answer = choice.message.content
        break

    messages.append(choice.message)  # the assistant turn with its tool_calls
    for call in choice.message.tool_calls:
        args = json.loads(call.function.arguments)  # arguments is a JSON string
        result = get_weather(**args)
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result),
        })

print(answer)

The first reply does not answer the question. It asks for a call:

200 JSON
{
  "id": "chatcmpl-5b0c1d8e7f6a4b3c9d2e1f0a8b7c6d5e",
  "object": "chat.completion",
  "created": 1791590400,
  "model": "Kimi-K3-3BIT-REAP",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "reasoning_content": "The user asks for the current weather in Paris. I should call get_weather.",
        "tool_calls": [
          {
            "id": "call_9f1c2ab34d5e4f60a7b8c9d0e1f2a3b4",
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"city\":\"Paris\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ],
  "usage": {
    "prompt_tokens": 312,
    "completion_tokens": 58,
    "total_tokens": 370,
    "prompt_tokens_details": {"cached_tokens": 0},
    "completion_tokens_details": {"reasoning_tokens": 31}
  }
}

After you send the result, the next reply is an ordinary answer: message.content holds the text and finish_reason is stop.

The loop, step by step

Step Who What happens
1 You Send messages together with tools, the list of functions the model may ask for.
2 Model The reply carries tool_calls and finish_reason: "tool_calls". Each call names a function and gives its arguments.
3 You Parse the arguments of each call and run your function.
4 You Append the assistant message and one tool message per call to messages, then send the request again with the same tools.
5 Model The model answers in text with finish_reason: "stop", or asks for more calls. In that case go back to step 3.

One request is one model turn. The API keeps no conversation between requests and sets no number of rounds: your code holds messages and decides when to stop.

Defining tools

tools is an array with one entry per function. Every model of the catalogue accepts it; GET /v1/models reports capabilities.tools: true for every id.

Field Type Description
type string Always function.
function.name string The name the model uses when it asks for this function. Required.
function.description string What the function does, in plain language.
function.parameters object The arguments as a JSON Schema with type: "object" at the top. See Schema keywords below.

Each endpoint has its own spelling of a tool. Send the one that belongs to the endpoint you call:

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get the current weather for a city.",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {
          "type": "string",
          "description": "City name, for example Paris"
        },
        "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
      },
      "required": ["city"]
    }
  }
}

tool_choice

tool_choice says whether the model may, or must, call a tool. It is read only together with tools.

Value Description Applied by
"auto" The model decides whether to answer in text or to call a tool. This is the default. All models
"required" The reply calls at least one tool. Hosted open-weight models
{"type": "function", "function": {"name": "get_weather"}} The reply calls this function. The name must be one of the functions in tools. Calls to other functions are left out of the reply. Hosted open-weight models

The Shannon models (shannon-*) decide in every request, as with auto.

With required or a named function, read finish_reason before you read tool_calls. When the model produces no call, the reply is a text answer with finish_reason: "stop".

Meaning /v1/chat/completions /v1/responses /v1/messages
The model decides "auto" "auto" {"type": "auto"}
Call at least one tool "required" "required" {"type": "any"}
Call one named function {"type": "function", "function": {"name": "get_weather"}} {"type": "function", "name": "get_weather"} {"type": "tool", "name": "get_weather"}

Accepted for compatibility: tool_choice: "none" and parallel_tool_calls. For a turn without tool calls, send the request without tools.

Tool calls in the reply

Field Type Description
finish_reason string tool_calls when the reply carries at least one call.
message.tool_calls array One entry per call. The field is present only when the model asked for a call.
tool_calls[].id string call_ followed by 32 hexadecimal characters. Send it back as tool_call_id with the result.
tool_calls[].type string Always function.
tool_calls[].function.name string The function to run.
tool_calls[].function.arguments string The arguments as a JSON string, "{}" when there are none. Parse it before use.
message.content string | null null when the reply holds only calls. The hosted open-weight models can return text next to the calls; the Shannon models return null.

The same call in each format. On /v1/responses it is a function_call item of output, with arguments as a JSON string. On /v1/messages it is a tool_use content block whose input is a JSON object, its id starts with toolu_, and stop_reason is tool_use.

{
  "finish_reason": "tool_calls",
  "message": {
    "role": "assistant",
    "content": null,
    "tool_calls": [
      {
        "id": "call_9f1c2ab34d5e4f60a7b8c9d0e1f2a3b4",
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{\"city\":\"Paris\"}"
        }
      }
    ]
  }
}

Several calls in one turn

A reply can carry several calls. Run every one of them and send one result per call, each with its own tool_call_id, in the next request.

The hosted open-weight models return each distinct call once: two calls with the same name and the same arguments arrive as one.

Streamed tool calls

With stream: true, a call arrives in one piece: the chunk holds the complete arguments string, so there are no fragments to join. index counts the calls of the turn from 0.

data: one chunk, formatted
{
  "id": "chatcmpl-5b0c1d8e7f6a4b3c9d2e1f0a8b7c6d5e",
  "object": "chat.completion.chunk",
  "created": 1791590400,
  "model": "Kimi-K3-3BIT-REAP",
  "choices": [
    {
      "index": 0,
      "delta": {
        "tool_calls": [
          {
            "index": 0,
            "id": "call_9f1c2ab34d5e4f60a7b8c9d0e1f2a3b4",
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"city\":\"Paris\"}"
            }
          }
        ]
      },
      "finish_reason": null
    }
  ]
}
Endpoint What arrives
/v1/chat/completions One chat.completion.chunk per call, with the call in delta.tool_calls[0]. The last chunk has finish_reason: "tool_calls" and usage.
/v1/responses Per call: response.output_item.added (a function_call item with empty arguments), response.function_call_arguments.delta (the whole string), response.function_call_arguments.done, response.output_item.done.
/v1/messages Per call: content_block_start (a tool_use block with empty input), one content_block_delta whose input_json_delta.partial_json is the whole arguments text, content_block_stop. message_delta then carries stop_reason: "tool_use".

On shannon-1.6-lite, shannon-1.6-pro, shannon-2-lite, shannon-2-pro and shannon-coder-1, a streamed request that carries tools delivers its answer text in one content delta at the end of the turn. Streaming

Sending results back

The second request repeats the conversation and adds two things: the assistant turn that asked for the calls, and one result per call.

{
  "messages": [
    {"role": "user", "content": "What is the weather in Paris right now?"},
    {
      "role": "assistant",
      "content": null,
      "tool_calls": [
        {
          "id": "call_9f1c2ab34d5e4f60a7b8c9d0e1f2a3b4",
          "type": "function",
          "function": {
            "name": "get_weather",
            "arguments": "{\"city\":\"Paris\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_9f1c2ab34d5e4f60a7b8c9d0e1f2a3b4",
      "content": "{\"city\":\"Paris\",\"temperature\":18,\"unit\":\"celsius\",\"conditions\":\"cloudy\"}"
    }
  ]
}
  • Send the assistant turn back as you received it, before the results. Each result is matched to its call by id: tool_call_id on /v1/chat/completions, call_id on /v1/responses, tool_use_id on /v1/messages.
  • A result is text. Serialise an object to a JSON string for content (output on /v1/responses). On /v1/messages the content of a tool_result block is a string or a list of text blocks.
  • In the assistant turn, function.arguments stays a JSON string, exactly as it arrived.
  • The conversation may end with a tool result. No new user message is needed: the model continues from the result.
  • When your function fails, send the failure as the result text. The model can then report it or ask for another call.

Schema keywords

parameters (input_schema on /v1/messages) is a JSON Schema. The Shannon 3 family (shannon-3, shannon-3-pro, shannon-3.1, shannon-3.1-pro) and the hosted open-weight models receive it as you send it.

shannon-1.6-lite, shannon-1.6-pro, shannon-2-lite, shannon-2-pro and shannon-coder-1 receive this subset, on all three endpoints:

Treatment Keywords
Read as sent Every keyword that is not named in the rows below, for example type, properties, required, items, enum, description, minimum, maximum, anyOf and allOf, at every level of the schema.
format Read for the values int32, int64, float, double, byte, date, date-time and enum.
oneOf Read as anyOf.
Not part of the schema these models receive $schema, $id, $ref, $defs, $comment, definitions, additionalProperties, unevaluatedProperties, patternProperties, propertyNames, examples, default, title, const, not, if, then, else, contains, minContains, maxContains, dependentRequired, dependentSchemas, dependencies, readOnly, writeOnly, deprecated, contentEncoding, contentMediaType, contentSchema, multipleOf, uniqueItems; exclusiveMinimum and exclusiveMaximum when they are numbers; other values of format.

Behaviour and limits

  • The API sets no maximum number of tools and no maximum number of rounds.
  • A request body can be up to 32 MiB, and the conversation with its results has to fit the context window of the model.
  • When a request carries tools, shannon-1.6-lite, shannon-1.6-pro, shannon-2-lite and shannon-2-pro use a default temperature of 0.2. Send temperature to set another value.
  • Every round is a request of its own: it draws from your balance and counts toward flood protection like any other request. Limits and balance

Errors

A tool call is a normal reply with status 200. These errors concern the request itself:

Status Type Message When
400 invalid_request_error unknown model: <id> model is not an id of the published list.
422 invalid_request_error Failed to deserialize the JSON body into the target type: … tools is not an array, or another field has the wrong JSON type.
413 invalid_request_error Failed to buffer the request body: … The request body is larger than 32 MiB. The error carries code: "request_too_large".

The model writes the arguments. Check them against your schema before you run the function, as you would check any outside input.

Authentication, balance and server errors are the same as for every request. Error Handling