# Tool-calling agent

> Build a minimal agent loop on MirAPI: define tools, let a model request them, execute locally, and feed results back using OpenAI-style tool calling.

Tool calling lets a model ask your code to run a function and return the result, instead of answering from training data alone. This tutorial builds a minimal agent loop on MirAPI's OpenAI-compatible endpoint at `https://api.mirapi.ai/v1`: you define a tool, the model requests it, your code executes it, and the result goes back to the model for a final answer.

Tool calling on MirAPI uses the standard OpenAI `tools` and `tool_choice` format, so the example below runs with the OpenAI SDK or any OpenAI-compatible client.

## Prerequisites

- Python with the OpenAI SDK: `pip install openai`
- An API key exported as `MIRAPI_API_KEY` (see [Authentication](/docs/api-reference/authentication))
- A model from `GET /v1/models` whose `supported_parameters` list tool calling — check the model page in the console catalogue or the [Models & pricing](/docs/models) page

## The tool-calling loop

```python
import json
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.mirapi.ai/v1",
    api_key=os.environ["MIRAPI_API_KEY"],
)

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "City name"}
                },
                "required": ["city"],
            },
        },
    }
]


def get_weather(city: str) -> str:
    # Replace with a real weather API in production.
    return json.dumps({"city": city, "temp_c": 24, "condition": "sunny"})


messages = [
    {"role": "user", "content": "What is the weather in Shanghai? Use the tool."}
]

# 1. Model decides to call the tool
response = client.chat.completions.create(
    model="deepseek-chat",
    messages=messages,
    tools=TOOLS,
)
msg = response.choices[0].message
messages.append(msg)

# 2. Execute every requested call
for call in msg.tool_calls or []:
    result = get_weather(json.loads(call.function.arguments)["city"])
    messages.append(
        {
            "role": "tool",
            "tool_call_id": call.id,
            "content": result,
        }
    )

# 3. Model produces the final answer with the tool output in context
final = client.chat.completions.create(
    model="deepseek-chat",
    messages=messages,
    tools=TOOLS,
)
print(final.choices[0].message.content)
```

## How tool calls work

1. **Define** a tool schema in `tools` and implement the matching function locally.
2. **Read** `message.tool_calls` — each entry has `id`, `function.name`, and `function.arguments` (a JSON string).
3. **Execute** the local function and append a `role: "tool"` message whose `tool_call_id` matches the request.
4. **Repeat** until the model returns a message with no `tool_calls`; that `content` is the final answer.

If the model requests several tools in one turn, execute all of them before calling the API again. The model does not run your function itself — it only returns the function name and arguments for your code to dispatch.

## Tool schema

The `tools` array uses the OpenAI function-calling shape:

| Field | Type | Purpose |
| --- | --- | --- |
| `tools[].type` | string | Always `"function"` for function calling. |
| `tools[].function.name` | string | The identifier the model uses when it requests the tool. |
| `tools[].function.description` | string | When and how to use the tool; a clearer description improves call accuracy. |
| `tools[].function.parameters` | object | A JSON Schema describing the arguments the model may pass. |

## Control tool choice

`tool_choice` overrides the default behaviour:

| Value | Effect |
| --- | --- |
| `"auto"` (default) | The model decides whether to call a tool. |
| `"none"` | Disables tool calls. |
| `"required"` | Forces the model to call a tool. |
| `{"type": "function", "function": {"name": "..."}}` | Forces a specific function. |

Include `tools` in **every** request in the loop, including the final call, so the model can keep calling tools when it needs to.

## Error handling

- **403 with a valid key**: the model is not on your key's whitelist, or the balance is empty. Top up and retry — the same key works again immediately after a top-up.
- **Model answers without calling the tool**: the model may not support tool calling, or `tools` is missing from a request. Verify the model's `supported_parameters` and send `tools` on every turn.
- **Broken JSON in `function.arguments`**: parse it inside `try/except` and, on failure, return the error text as the tool result so the model can correct itself.
- **429 rate limit**: retry with jittered exponential backoff (1s → 2s → 4s, capped around 30s); MirAPI does not send a `Retry-After` header and does not retry for you.

Every error message ends with a request ID — include it when you open a support ticket.

## Cost

A tool-calling loop resends the tool schema and the full message history on every turn, so each extra turn adds input tokens. A short loop of a few turns is typically a few thousand tokens — well under a cent for most models, but check the `pricing` field in `GET /v1/models` for exact per-model rates.

Repeated prefixes in the history are eligible for prompt caching, which bills at a lower cached-read rate than normal input. Reasoning tokens produced between tool calls are billed as output.

## Related links

- [Models & pricing](/docs/models) — find models that support tool calling
- [OpenAI compatibility](/docs/openai-compat) — endpoint and request format
- [Errors](/docs/api-reference/errors) — status codes and retry guidance
- [Prompt caching](/docs/guides/prompt-caching) — lower cost for repeated context