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

# Function calling

> Execute model-requested functions and continue the conversation, with complete Python and streaming examples.

This guide uses the Perceptron Python SDK with Perceptron's public API. The model selects a function and generates its arguments; your Python code runs it and returns the result.

## Run a complete example

Install the client and set `PERCEPTRON_API_KEY` to your API key:

```bash theme={null}
pip install "perceptron>=0.4.0"
export PERCEPTRON_API_KEY="your-api-key"
```

Save the following as `inventory.py` and run `python inventory.py`. The inventory is a local example dataset, so only the model request needs network access. Replace `lookup_inventory` with your own database or service once the loop is working.

```python theme={null}
import json

from perceptron import Client, function_tool


client = Client(timeout=60.0)

INVENTORY = {
    "BALL-RED": {"name": "Red ball", "quantity": 12},
    "BALL-BLUE": {"name": "Blue ball", "quantity": 0},
}

TOOLS = [function_tool(
    "lookup_inventory",
    description="Look up the current stock for one product SKU.",
    parameters={
        "type": "object",
        "properties": {
            "sku": {"type": "string", "description": "The exact product SKU."},
        },
        "required": ["sku"],
        "additionalProperties": False,
    },
)]


def execute_tool(name, arguments):
    try:
        if name != "lookup_inventory":
            raise ValueError("Unknown tool. Use lookup_inventory.")
        args = json.loads(arguments)
        if not isinstance(args, dict) or set(args) != {"sku"}:
            raise ValueError("Arguments must contain only sku.")
        if not isinstance(args["sku"], str):
            raise ValueError("sku must be a string.")
        item = INVENTORY.get(args["sku"])
        return json.dumps({"sku": args["sku"], "found": item is not None, "item": item})
    except (TypeError, ValueError) as error:
        return json.dumps({"error": str(error)})


def complete_turn(messages, tool_choice="auto"):
    response = client.chat.completions.create(
        model="perceptron-mk1.5",
        messages=messages,
        tools=TOOLS,
        tool_choice=tool_choice,
        parallel_tool_calls=True,
        reasoning_effort="high",
        max_completion_tokens=2048,
    )
    choice = response.choices[0]
    message = choice.message.to_dict()
    return message, choice.finish_reason, response.usage


def run():
    messages = [{
        "role": "user",
        "content": (
            "Use inventory lookup to compare BALL-RED and BALL-BLUE. "
            "Which is in stock, and how many are available?"
        ),
    }]
    calls_used = 0
    for _ in range(4):
        message, finish_reason, usage = complete_turn(messages)
        if usage is not None:
            print(f"Tokens this request: {usage.total_tokens}")

        calls = message.get("tool_calls") or []
        if finish_reason == "stop" and not calls:
            messages.append(message)
            return messages
        if finish_reason != "tool_calls" or not calls:
            raise RuntimeError(f"Incomplete response: {finish_reason}")
        if calls_used + len(calls) > 6:
            raise RuntimeError("Tool call budget exhausted.")

        messages.append(message)
        for call in calls:
            function = call["function"]
            result = execute_tool(function["name"], function["arguments"])
            messages.append({
                "role": "tool",
                "tool_call_id": call["id"],
                "content": result,
            })
        calls_used += len(calls)

    raise RuntimeError("Model round budget exhausted.")


if __name__ == "__main__":
    history = run()
    print(history[-1].get("content") or "")
```

The example allows at most four model requests and six tool executions. It executes calls sequentially for clarity, even when the model requests them in the same turn. A successful answer should report 12 red balls and no blue balls in this example inventory; the wording and number of calls can vary. `run()` returns the completed conversation, including the final assistant answer, so another request can reuse the tool results.

## Preserve the conversation

Append the whole assistant message before its tool results. The SDK accepts the returned `response.message` directly in `messages`; use its `to_dict()` method when storing history as dictionaries, as in the example. Both retain `content`, `tool_calls`, and any returned `reasoning_content`.

The model's next input does not necessarily include every saved field. Historical `reasoning_content` is forwarded when the request includes function tools or tool-call/result history; it is ignored on ordinary requests without either. When an assistant message contains tool calls, its accompanying prose in `content` is omitted from the next model input. Keep the full message for the tool round trip, and put information needed for subsequent reasoning in the user messages or tool results.

Each call needs one result with the matching `tool_call_id` before you continue. Results may be supplied in a different order from the calls. Keep `function.arguments` as the returned string in assistant history, even after parsing it for execution.

Continue sending the same function declarations on subsequent requests, including when you want a final answer. For a [constrained JSON or regex answer](/perceptron-mk1.5/capabilities/structured-outputs#combine-tools-with-a-constrained-final-answer), complete the tool loop first, then omit the tool declarations and options from that separate request. Assistant text and tool arguments can be absent or empty while a response is streaming.

## Stream a tool call

Replace `complete_turn` in the example with this version. The execution loop stays the same. The SDK assembles calls by `index` and accumulates argument fragments. The example waits for the complete stream and checks the result before returning anything that can be executed.

```python theme={null}
def complete_turn(messages, tool_choice="auto"):
    with client.chat.completions.create(
        model="perceptron-mk1.5",
        messages=messages,
        tools=TOOLS,
        tool_choice=tool_choice,
        parallel_tool_calls=True,
        reasoning_effort="high",
        max_completion_tokens=2048,
        stream=True,
        stream_options={"include_usage": True},
    ) as stream:
        for chunk in stream:
            for choice in chunk.choices:
                print(choice.delta.content or "", end="", flush=True)
        response = stream.get_final_completion()

    if not response.complete or response.finish_reason not in {"stop", "tool_calls"}:
        raise RuntimeError(f"Incomplete stream: {response.finish_reason}")
    calls = response.tool_calls or []
    ids = [call.id for call in calls]
    if calls and (not all(ids) or len(set(ids)) != len(ids)):
        raise RuntimeError("Missing or duplicate tool call IDs.")
    return response.message.to_dict(), response.finish_reason, response.usage
```

The SDK requests usage by default for Perceptron streams; the example sets `stream_options.include_usage: true` explicitly. Usage can arrive in a separate trailing chunk with `choices: []`. `get_final_completion()` consumes any remaining chunks and returns the assembled message and usage. If you read chunks yourself, consume usage independently of choice deltas and keep reading after a finish reason.

`finish_reason: "tool_calls"` means a model turn is complete and ready for your application to handle. `"stop"` is a completed answer. Treat `"length"`, a missing finish reason, or an API error as an incomplete turn; do not execute accumulated arguments. The SDK raises an `SDKError` subclass for API or transport failures, and `IncompleteStreamError` when the stream ends without `[DONE]`. Its error may carry a `.partial` completion for diagnostics; do not execute those partial calls. A raw SSE client must also handle `{"error": ...}` events, which end the stream without `[DONE]`.

## Supported controls

The API behaviors below apply when the request declares non-empty `tools`. The SDK validates controls before sending a request: unsupported `tool_choice` values and incompatible tool/output combinations raise `BadRequestError` locally. At the HTTP API layer, validly shaped `tool_choice` and `parallel_tool_calls` values are ignored when `tools` is omitted or empty.

| Control                                             | Behavior                                                                                                                               |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `tool_choice: "auto"`                               | Default. The model chooses whether to call a function.                                                                                 |
| `tool_choice: "none"`                               | Best effort request for an answer without calls. Your application must still handle returned calls.                                    |
| `tool_choice: "required"` or a named function       | Unsupported; the SDK raises `BadRequestError`. Direct API requests with non-empty `tools` receive HTTP 400.                            |
| `parallel_tool_calls`                               | Defaults to `true`. Always accept an array of calls, including when set to `false`; prior parallel history can preserve this behavior. |
| Function `strict: true`                             | Accepted, but argument-schema enforcement is not implemented. Validate arguments yourself.                                             |
| `n`                                                 | Chat completions supports `1`. Several tool calls can still appear within that one completion.                                         |
| JSON-schema `response_format` or `regex` with tools | Unsupported in the same request.                                                                                                       |
| Multilook                                           | Does not support tool declarations, tool calls, or tool-result history. Use chat completions.                                          |

For a machine-readable final report, finish the tool workflow and make a separate [structured-output request](/perceptron-mk1.5/capabilities/structured-outputs) without tool declarations. Completed tool history can remain in the conversation. For execution budgets, image results, and retrieval patterns, continue to [Building a tool agent](/perceptron-mk1.5/guides/tool-agents).
