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:

conceptOpenAIAnthropic
Tool definition wrapper{ type: 'function', function: { name, description, parameters } }{ name, description, input_schema }
JSON Schema locationfunction.parametersinput_schema
Model calls a toolmessage.tool_calls[] with id, function.name, function.arguments (string)content_block of type tool_use with id, name, input (object)
You return the resultrole: '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 reasonfinish_reason: 'tool_calls'stop_reason: 'tool_use'
Force a tooltool_choice: { type: 'function', function: { name } }tool_choice: { type: 'tool', name }
Force any tooltool_choice: 'required'tool_choice: { type: 'any' }
Disable tools this turntool_choice: 'none'Omit tool_choice, or set { type: 'auto', disable_parallel_tool_use: true }
Parallel tool callsMultiple entries in message.tool_callsMultiple tool_use blocks in content

OpenAI: tools + tool_calls

First turn — send the user question with the tool definitions:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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: false in 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 returns 400 tool_call_parse_error when it can, but you should defensively try/catch around JSON.parse and, on failure, return a tool message explaining the parse error so the model retries.
  • tool_use_id must match tool_use.id. On /v1/messages, every tool_result block must reference an id emitted in the immediately prior assistant turn. Mismatches return 400 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 description and 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_schema when a tool's parameters /input_schema is not valid JSON Schema.
  • 400 invalid_request code: tool_call_parse_error when the model emits invalid JSON in a tool call.
  • 400 invalid_request code: tool_use_id_mismatch on /v1/messages.
  • 400 invalid_request code: unsupported_parameter when tools are attached to a model that does not support function calling.