Function Calling
Give the model a set of tools it can call. Nimbus supports the OpenAI tools/tool_calls pattern and the Anthropic tool_use/tool_result block pattern — both work on either endpoint.
The two shapes side by side
Same tool, two idiomatic definitions:
{
"type": "function",
"function": {
"name": "search_orders",
"description": "Search the customer's order history. Returns up to 20 matches, most recent first.",
"parameters": {
"type": "object",
"properties": {
"customer_id": { "type": "string", "description": "Stable customer identifier." },
"status": {
"type": "string",
"enum": ["pending", "shipped", "delivered", "cancelled"]
},
"since": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp. Only orders after this instant."
}
},
"required": ["customer_id"],
"additionalProperties": false
}
}
}Both use JSON Schema for the parameter definition. The wire framing differs:
| concept | OpenAI | Anthropic |
|---|---|---|
| Tool definition wrapper | { type: 'function', function: { name, description, parameters } } | { name, description, input_schema } |
| JSON Schema location | function.parameters | input_schema |
| Model calls a tool | message.tool_calls[] with id, function.name, function.arguments (string) | content_block of type tool_use with id, name, input (object) |
| You return the result | role: 'tool' message with tool_call_id + content (string) | role: 'user' content block of type tool_result with tool_use_id + content (string or blocks) |
| Stop reason | finish_reason: 'tool_calls' | stop_reason: 'tool_use' |
| Force a tool | tool_choice: { type: 'function', function: { name } } | tool_choice: { type: 'tool', name } |
| Force any tool | tool_choice: 'required' | tool_choice: { type: 'any' } |
| Disable tools this turn | tool_choice: 'none' | Omit tool_choice, or set { type: 'auto', disable_parallel_tool_use: true } |
| Parallel tool calls | Multiple entries in message.tool_calls | Multiple tool_use blocks in content |
OpenAI: tools + tool_calls
First turn — send the user question with the tool definitions:
{
"model": "openai/gpt-5.1",
"messages": [
{ "role": "user", "content": "Where is order for customer c_419 that shipped last week?" }
],
"tools": [ /* one or more function definitions */ ],
"tool_choice": "auto"
}The model returns a tool_calls array. Each call has an id, function name, and a JSON-encoded arguments string:
{
"id": "chatcmpl_01H8...",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "search_orders",
"arguments": "{\"customer_id\":\"c_419\",\"status\":\"shipped\"}"
}
}]
},
"finish_reason": "tool_calls"
}]
}Execute the function locally, then send a follow-up with a role: "tool" message that echoes the tool_call_id:
{
"model": "openai/gpt-5.1",
"messages": [
{ "role": "user", "content": "Where is order for customer c_419 that shipped last week?" },
{
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "search_orders",
"arguments": "{\"customer_id\":\"c_419\",\"status\":\"shipped\"}"
}
}]
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "[{\"order_id\":\"o_88121\",\"status\":\"shipped\",\"tracking\":\"1Z999\"}]"
}
],
"tools": [ /* same tools */ ]
}Anthropic: tool_use + tool_result blocks
First turn:
{
"model": "anthropic/claude-opus-4.5",
"max_tokens": 1024,
"tools": [ /* one or more tool definitions */ ],
"tool_choice": { "type": "auto" },
"messages": [
{ "role": "user", "content": "Where is order for customer c_419 that shipped last week?" }
]
}The assistant returns a content block of type tool_use. Note input is a full object, not a string:
{
"id": "msg_01H8...",
"type": "message",
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_01A2B3",
"name": "search_orders",
"input": { "customer_id": "c_419", "status": "shipped" }
}
],
"stop_reason": "tool_use"
}Second turn — echo the assistant message and append a user message whose content is a tool_result block:
{
"model": "anthropic/claude-opus-4.5",
"max_tokens": 1024,
"tools": [ /* same tools */ ],
"messages": [
{ "role": "user", "content": "Where is order for customer c_419 that shipped last week?" },
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_01A2B3",
"name": "search_orders",
"input": { "customer_id": "c_419", "status": "shipped" }
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A2B3",
"content": "[{\"order_id\":\"o_88121\",\"status\":\"shipped\",\"tracking\":\"1Z999\"}]"
}
]
}
]
}Parallel tool calls
Both formats support the model emitting multiple calls in a single turn. Execute them concurrently and return all results before the next turn.
{
"choices": [{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc",
"type": "function",
"function": { "name": "search_orders", "arguments": "{\"customer_id\":\"c_419\"}" }
},
{
"id": "call_def",
"type": "function",
"function": { "name": "search_orders", "arguments": "{\"customer_id\":\"c_802\"}" }
}
]
},
"finish_reason": "tool_calls"
}]
}To disable parallelism (some workflows want strict serialization):
- OpenAI:
parallel_tool_calls: falsein the request body. - Anthropic:
tool_choice: { type: "auto", disable_parallel_tool_use: true }.
Full loop example
A minimal agent loop: call the model, execute any tool calls it emits, feed the results back, repeat until it returns a text answer.
from openai import OpenAI
import json
client = OpenAI(base_url="https://llm.nimbusapi.net/v1", api_key="sk-nim-YOUR_KEY")
TOOLS = [{
"type": "function",
"function": {
"name": "search_orders",
"description": "Search order history.",
"parameters": {
"type": "object",
"properties": {"customer_id": {"type": "string"}},
"required": ["customer_id"],
},
},
}]
def search_orders(customer_id: str) -> str:
# your real backend call here
return json.dumps([{"order_id": "o_88121", "status": "shipped"}])
messages = [{"role": "user", "content": "Where is c_419's last order?"}]
while True:
resp = client.chat.completions.create(
model="openai/gpt-5.1",
messages=messages,
tools=TOOLS,
tool_choice="auto",
)
msg = resp.choices[0].message
messages.append(msg.model_dump(exclude_none=True))
if not msg.tool_calls:
print(msg.content)
break
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
if call.function.name == "search_orders":
result = search_orders(args["customer_id"])
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result,
})Edge cases
- Malformed arguments JSON. OpenAI-family models occasionally emit invalid JSON in
arguments. Nimbus returns400 tool_call_parse_errorwhen it can, but you should defensivelytry/catcharoundJSON.parseand, on failure, return atoolmessage explaining the parse error so the model retries. - tool_use_id must match tool_use.id. On
/v1/messages, everytool_resultblock must reference an id emitted in the immediately prior assistant turn. Mismatches return400 tool_use_id_mismatch. - Return type for tool_result. Anthropic accepts either a plain string or an array of content blocks (useful for returning images from a tool). OpenAI
role: "tool"messages must be strings — serialize with JSON if you have structured data. - Description quality drives calls. The model decides when to call a tool almost entirely from
descriptionand parameter descriptions. Terse, precise descriptions (>= 1 sentence, no filler) outperform verbose ones. - Streaming tool calls. Arguments (OpenAI) and input JSON (Anthropic) stream as string fragments. Only parse after the terminal event for the tool call. See /docs/streaming.
Error codes
400 invalid_request—code: invalid_tool_schemawhen a tool'sparameters/input_schemais not valid JSON Schema.400 invalid_request—code: tool_call_parse_errorwhen the model emits invalid JSON in a tool call.400 invalid_request—code: tool_use_id_mismatchon/v1/messages.400 invalid_request—code: unsupported_parameterwhen tools are attached to a model that does not support function calling.