SimpleLLMDocs
Build with SimpleLLM
Markdown ↗

Tool calling

Define a function, validate arguments, and return its result.

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 for fields and streaming for event handling.

Search the complete documentation.