> ## Documentation Index
> Fetch the complete documentation index at: https://docs.voidai.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Tool Calling with the VoidAI API

> Build a bounded Chat Completions tool loop with VoidAI: validate arguments, handle multiple tool calls, and return each result with its tool_call_id.

Tool calling lets a model request data or an action from your application. Your application decides which functions may run, executes them, and sends the results back to the model.

This guide uses [Chat Completions](/api-reference/chat/completions) and a small, fictional project's formatting conventions. The example reads an in-memory dictionary; it does not run shell commands, evaluate generated code, or modify files.

## Before you start

* Configure the [OpenAI Python SDK for VoidAI](/guides/openai-sdk) and store your key in `VOIDAI_API_KEY`.
* Choose an exact ID from [models and pricing](https://voidai.app/models) that advertises `/v1/chat/completions` and `supports_tool_calling: true`.
* Confirm your account can use that model. Catalog metadata does not guarantee account access or current availability. Claude-family models also have [additional access and usage checks](/api-reference/messages/create#claude-access-and-usage-checks).

The example defaults to `gpt-4o-mini`, which advertises chat and tool support in the catalog checked on October 1, 2026. Set `VOIDAI_MODEL` to use a different eligible model. Model behavior and optional parameters can differ.

## How the exchange works

1. Send the conversation and a function definition in `tools`.
2. Read the assistant's `tool_calls`. Validate each function name and its JSON arguments in your application.
3. Append the assistant message, then one `role: "tool"` message for **every** call, using that call's `id` as `tool_call_id`.
4. Send the updated conversation again. Stop when the model returns an answer or your loop limit is reached.

A tool definition describes a function; it does not execute it. The function's return value becomes the tool message's string `content`.

## A complete Python example

Install the `openai` package if it is not already part of your application. The key is read from the environment and is never included in the tool definition.

```python theme={null}
import json
import os

from openai import OpenAI

client = OpenAI(
    api_key=os.environ["VOIDAI_API_KEY"],
    base_url="https://api.voidai.app/v1",
    timeout=30.0,
    max_retries=0,
)
model = os.environ.get("VOIDAI_MODEL", "gpt-4o-mini")

# Example data for a fictional project, not universal style rules.
CONVENTIONS = {
    "python": {"indent": "4 spaces", "line_length": 88},
    "javascript": {"indent": "2 spaces", "semicolons": "required"},
}

tools = [{
    "type": "function",
    "function": {
        "name": "get_project_convention",
        "description": "Read this project's formatting convention for a language.",
        "parameters": {
            "type": "object",
            "properties": {
                "language": {
                    "type": "string",
                    "enum": ["python", "javascript"],
                },
            },
            "required": ["language"],
            "additionalProperties": False,
        },
    },
}]


def run_tool(call):
    # Dispatch only the function this application deliberately exposes.
    if call.type != "function" or call.function.name != "get_project_convention":
        return {"error": "Unknown tool"}

    raw = call.function.arguments
    if len(raw) > 4096:
        return {"error": "Arguments are too large"}
    try:
        arguments = json.loads(raw)
    except json.JSONDecodeError:
        return {"error": "Arguments must be valid JSON"}

    if not isinstance(arguments, dict) or set(arguments) != {"language"}:
        return {"error": "Expected only a language argument"}
    language = arguments["language"]
    if not isinstance(language, str) or language not in CONVENTIONS:
        return {"error": "Choose python or javascript"}
    return CONVENTIONS[language]


messages = [{
    "role": "user",
    "content": "Use the project conventions to compare Python and JavaScript indentation.",
}]
request_ids = []
MAX_ROUNDS = 4
MAX_CALLS_PER_ROUND = 8

for _ in range(MAX_ROUNDS):
    response = client.chat.completions.create(
        model=model,
        messages=messages,
        tools=tools,
        tool_choice="auto",
        max_tokens=512,
    )
    request_ids.append(response.id)
    if not response.choices:
        raise RuntimeError("The response contained no choices")

    choice = response.choices[0]
    if choice.finish_reason in {"length", "content_filter"}:
        raise RuntimeError("The response stopped before the example could finish")
    message = choice.message
    calls = message.tool_calls or []
    if not calls:
        print(message.content or "No text was returned.")
        break
    if len(calls) > MAX_CALLS_PER_ROUND:
        raise RuntimeError("Tool-call limit reached")

    messages.append(message.model_dump(exclude_none=True))
    for call in calls:
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(run_tool(call)),
        })
else:
    raise RuntimeError("Round limit reached; inspect the conversation before continuing")
```

The model may request both languages in one response, across separate rounds, or return text without calling a tool. The loop handles all calls in each response before asking the model to continue. It makes at most four SDK requests, with automatic SDK retries disabled.

`max_tokens=512` is an example output cap. If your chosen model requires `max_completion_tokens`, use the supported parameter instead. A round limit and output cap help bound the example; they do not establish a fixed credit cost.

<Note>The example's control flow is checked with offline fixtures. It has not been run against the live VoidAI API.</Note>

## Adapt the example to your application

Keep argument validation in your handler even when you supply a JSON schema. Treat model-selected arguments as untrusted input, and expose only the functions your application intends to offer. A production handler that writes files or changes an external service should apply its own authorization and confirmation rules before the action.

Preserve the assistant message containing `tool_calls`, and return one result per call with the matching `tool_call_id`. Returning only the first result can leave the conversation incomplete. Keep tool results compact: they become input to a later model request and can consume context and credits.

The example uses non-streaming responses so complete JSON arguments are available before execution. With streaming, assemble the tool-call argument fragments before parsing or invoking a function. Do not execute partial arguments. See the [SDK streaming guide](/guides/openai-sdk#streaming) for the basic stream format.

## Check usage and handle errors

Each model request in the loop can be billed. Keep the response IDs in `request_ids` and use [request usage](/api-reference/usage/get) to inspect recorded charges. Use [current catalog prices](https://voidai.app/models) and the [credits guide](/guides/credits) to interpret input, output, and cache costs.

If a model rejects `tools`, check its advertised endpoint and tool support, account eligibility, and the provider's error response. A capability flag is not a promise that every optional tool feature works on every provider. Follow the [error guide](/guides/errors) for authentication, access, and rate-limit responses.

For the same kind of workflow in [Responses](/api-reference/responses/create) or [Messages](/api-reference/messages/create), use that API's own tool-input and tool-result format. The `role: "tool"` exchange above is specifically the Chat Completions format.
