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) import OpenAI from "openai";
const client = new OpenAI({ apiKey: "YOUR_API_KEY", baseURL: "https://api.shannon-ai.com/v1" });
const 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"],
},
},
}];
// Your code: ask a weather service here.
function getWeather({ city, unit = "celsius" }) {
return { city, temperature: 18, unit, conditions: "cloudy" };
}
const messages = [{ role: "user", content: "What is the weather in Paris right now?" }];
let answer = null;
// The API sets no limit on rounds: set your own.
for (let round = 0; round < 5; round++) {
const response = await client.chat.completions.create({
model: "Kimi-K3-3BIT-REAP",
messages,
tools,
});
const choice = response.choices[0];
if (choice.finish_reason !== "tool_calls") {
answer = choice.message.content;
break;
}
messages.push(choice.message); // the assistant turn with its tool_calls
for (const call of choice.message.tool_calls) {
const args = JSON.parse(call.function.arguments); // arguments is a JSON string
messages.push({
role: "tool",
tool_call_id: call.id,
content: JSON.stringify(getWeather(args)),
});
}
}
console.log(answer); # 1. Send the conversation with the tools.
curl https://api.shannon-ai.com/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "Kimi-K3-3BIT-REAP",
"messages": [
{"role": "user", "content": "What is the weather in Paris right now?"}
],
"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"]
}
}
}]
}'
# 2. Run the function yourself, then send the assistant turn and the result.
# Use the id of the call you received.
curl https://api.shannon-ai.com/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "Kimi-K3-3BIT-REAP",
"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\"}"}
],
"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"]
}
}
}]
}' The first reply does not answer the question. It asks for a call:
{
"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"]
}
}
} {
"type": "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"]
}
} {
"name": "get_weather",
"description": "Get the current weather for a city.",
"input_schema": {
"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\"}"
}
}
]
}
} {
"output": [
{
"id": "fc_3d2c1b0a9f8e4d7c6b5a4f3e2d1c0b9a",
"type": "function_call",
"call_id": "call_9f1c2ab34d5e4f60a7b8c9d0e1f2a3b4",
"name": "get_weather",
"arguments": "{\"city\":\"Paris\"}",
"status": "completed"
}
]
} {
"stop_reason": "tool_use",
"content": [
{
"type": "tool_use",
"id": "toolu_9f1c2ab34d5e4f60a7b8c9d0e1f2a3b4",
"name": "get_weather",
"input": {"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.
{
"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\"}"
}
]
} {
"input": [
{"role": "user", "content": "What is the weather in Paris right now?"},
{
"type": "function_call",
"call_id": "call_9f1c2ab34d5e4f60a7b8c9d0e1f2a3b4",
"name": "get_weather",
"arguments": "{\"city\":\"Paris\"}"
},
{
"type": "function_call_output",
"call_id": "call_9f1c2ab34d5e4f60a7b8c9d0e1f2a3b4",
"output": "{\"city\":\"Paris\",\"temperature\":18,\"unit\":\"celsius\",\"conditions\":\"cloudy\"}"
}
]
} {
"messages": [
{"role": "user", "content": "What is the weather in Paris right now?"},
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_9f1c2ab34d5e4f60a7b8c9d0e1f2a3b4",
"name": "get_weather",
"input": {"city": "Paris"}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_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_idon/v1/chat/completions,call_idon/v1/responses,tool_use_idon/v1/messages. - A result is text. Serialise an object to a JSON string for
content(outputon/v1/responses). On/v1/messagesthecontentof atool_resultblock is a string or a list of text blocks. - In the assistant turn,
function.argumentsstays 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-liteandshannon-2-prouse a defaulttemperatureof 0.2. Sendtemperatureto 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