# Tool calling (function calling)

URL: https://bridge.wekoya.tech/docs/tools
Updated: 2026-11-08

> Let the model use your code: look up an order, check a balance, book a slot, search your database. You describe the functions you have, the model asks for…

Let the model use your code: look up an order, check a balance, book a slot, search your database. You describe the functions you have, the model asks for one when it needs it, you run it and send the result back, and the model answers with it. Bridge uses OpenAI's format, so any OpenAI SDK works as is, on every model except the preview N-ATLAS.

- 1. Describe your tools Send tools: a name, what it does, and its parameters.
- 2. The model asks The reply has tool_calls instead of an answer.
- 3. You run them Your code runs each one. The model never runs anything.
- 4. Send the results back One tool message per call; repeat until it answers.

```
import json, os
from openai import OpenAI

client = OpenAI(
    base_url="https://wekoya-api.wekoya.tech/v1",
    api_key=os.environ["WEKOYA_API_KEY"],
)

# 1. Describe what your code can do.
tools = [{
    "type": "function",
    "function": {
        "name": "get_order_status",
        "description": "Look up a customer's order by its id",
        "parameters": {
            "type": "object",
            "properties": {"order_id": {"type": "string", "description": "e.g. 4021"}},
            "required": ["order_id"],
        },
    },
}]

def get_order_status(order_id):
    # Your real lookup goes here.
    return {"order_id": order_id, "status": "shipped", "eta": "Thursday"}

functions = {"get_order_status": get_order_status}
messages = [{"role": "user", "content": "Where is order 4021?"}]

for _ in range(5):  # a cap, so a confused model can't loop forever
    reply = client.chat.completions.create(model="gpt-6-luna", messages=messages, tools=tools)
    message = reply.choices[0].message
    if not message.tool_calls:
        print(message.content)  # 4. No more tools wanted: this is the answer.
        break
    # 2. The model asked for one or more tools. Keep its request in the conversation.
    messages.append(message.model_dump(exclude_none=True))
    for call in message.tool_calls:
        try:
            args = json.loads(call.function.arguments)
            result = functions[call.function.name](**args)  # 3. Run it.
        except Exception as error:
            result = {"error": str(error)}  # the model can recover from this
        messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
```

#### The rules that matter

- Keep the model's message with `tool_calls` in the conversation, then add one `{"role": "tool", "tool_call_id": ..., "content": ...}` message for every call, with the same id. A missing or mismatched id is the most common error.
- `arguments` is a JSON string, not an object: parse it, and check it before you act on it. Treat it like any input from outside your app.
- A tool's `content` is text. Send results as JSON text. If your function fails, send the error as the result (`{"error": "order not found"}`): the model can explain it or try another way.
- Loop until the reply has no `tool_calls` (`finish_reason` is `stop`), and cap the loop. A model may ask for more tools after seeing a result.
- Write clear names and descriptions, and describe each parameter. The model chooses tools from these words alone.

#### Several tools at once
A reply can ask for more than one tool, for example an order status and the weather. Run each, and answer each with its own tool message before asking again. They can run at the same time.

#### Choosing when tools are used

- `"tool_choice": "auto"` (the default): the model decides.
- `"required"`: it must call at least one tool.
- `{"type": "function", "function": {"name": "get_order_status"}}`: it must call that one.
- `"none"`: no tools this time, a plain answer.

#### Streaming
With `"stream": true`, tool calls arrive in pieces. Join them by `index`: the first piece carries the `id` and name, the rest add to `arguments`. Parse the arguments only when the stream ends.

```
import json, os
from openai import OpenAI

client = OpenAI(base_url="https://wekoya-api.wekoya.tech/v1", api_key=os.environ["WEKOYA_API_KEY"])
tools = [{"type": "function", "function": {"name": "get_order_status", "description": "Look up an order", "parameters": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]}}}]

stream = client.chat.completions.create(
    model="gpt-6-luna",
    messages=[{"role": "user", "content": "Where is order 4021?"}],
    tools=tools,
    stream=True,
)

calls = {}  # tool calls arrive in pieces, matched by index
for chunk in stream:
    if not chunk.choices:
        continue
    delta = chunk.choices[0].delta
    if delta.content:
        print(delta.content, end="", flush=True)
    for piece in delta.tool_calls or []:
        call = calls.setdefault(piece.index, {"id": "", "name": "", "arguments": ""})
        call["id"] = piece.id or call["id"]
        if piece.function and piece.function.name:
            call["name"] += piece.function.name
        if piece.function and piece.function.arguments:
            call["arguments"] += piece.function.arguments

for call in calls.values():
    print(call["name"], json.loads(call["arguments"]))
```

#### Which models, limits and cost

- Every model takes tools, except the preview N-ATLAS, which refuses them with `tools_not_supported`. Each model in `GET /v1/models` says so in `wekoya.tools`. `"model": "auto"` picks one that takes tools; name a model for the same behaviour on every request.
- At most 256 tools, and 2 MB of tool definitions, per request.
- Tool definitions and results are input tokens, sent with every request. Keep descriptions short, send only the tools a task needs, and keep them first and in the same order: [prompt caching](https://bridge.wekoya.tech/docs/prompt-caching) then reads them at the cached rate.
- The model only asks. Nothing runs unless your code runs it, so you decide what each tool may do.
