# Tool calling

A tool-capable model can request that your application run a function. SimpleLLM returns the model's request; your application validates it, executes the permitted action, and returns the result.

> The example below requires a model that supports OpenAI-style function calling. The public model list does not expose a verified tool-capability flag; validate your chosen model before relying on this workflow.

## The request/result loop

1. Describe the available functions in `tools` and send the user's message.
2. Inspect the assistant's `tool_calls`.
3. Validate the function name and arguments against your allowlist and schema.
4. Append the assistant message and one `tool` result for each call.
5. Send the updated messages so the model can answer using the result.

## Complete Python example

This example uses a read-only local demo function, with a maximum of four model rounds. Install `openai`, set `SIMPLELLM_API_KEY`, and set `SIMPLELLM_MODEL` to your validated tool-capable model.

~~~python
import json
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.simplellm.eu/v1",
    api_key=os.environ["SIMPLELLM_API_KEY"],
    max_retries=0,
)
tools = [{
    "type": "function",
    "function": {
        "name": "lookup_stock",
        "description": "Read demo stock for one product SKU.",
        "parameters": {
            "type": "object",
            "properties": {"sku": {"type": "string"}},
            "required": ["sku"],
            "additionalProperties": False,
        },
    },
}]
messages = [{"role": "user", "content": "How many DEMO-1 items are in stock?"}]

for _ in range(4):
    response = client.chat.completions.create(
        model=os.environ["SIMPLELLM_MODEL"],
        messages=messages,
        tools=tools,
        tool_choice="auto",
        max_tokens=2048,
    )
    answer = response.choices[0].message
    if not answer.tool_calls:
        print(answer.content or "")
        break

    messages.append(answer.model_dump(exclude_none=True))
    for call in answer.tool_calls:
        try:
            if call.function.name != "lookup_stock":
                raise ValueError("Unknown function")
            args = json.loads(call.function.arguments)
            if not isinstance(args, dict) or set(args) != {"sku"}:
                raise ValueError("Expected only sku")
            if not isinstance(args["sku"], str) or len(args["sku"]) > 64:
                raise ValueError("Invalid sku")
            result = {"sku": args["sku"], "stock": {"DEMO-1": 12}.get(args["sku"], 0)}
        except (ValueError, TypeError) as error:
            result = {"error": str(error)}
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result),
        })
else:
    raise RuntimeError("Tool round limit reached")
~~~

## Streamed tool calls

With streaming, the tool name and JSON arguments may arrive in separate deltas. Accumulate calls by their choice and tool-call index; concatenate argument fragments and parse the JSON only once the call is complete.

Do not execute an incomplete argument fragment or assume each event contains a complete function call. Multiple tool calls can occur in one assistant response.

## Safety and reliability

Treat generated tool arguments as untrusted input. Never dispatch a model-supplied name directly to arbitrary code, SQL, or a shell.

Require user approval for destructive or externally visible operations where appropriate. Use your own action IDs and idempotency rules so retries cannot repeat a payment, email, or deletion.

Limit tool rounds, execution time, and response size. Tool results become new model input and can contribute to both context size and cost.

## Reference

See [Chat completions API](/docs/reference/createChatCompletion.html) for fields and [streaming](/docs/streaming.html) for event handling.
