Build with the AI

Tools & function calling

Give the model functions to call. Krallo relays the calls to you — your agent runs the loop.

Function calling lets the model request that your code run a function — fetch weather, query a database, hit an internal API — and then continue reasoning with the result. You declare the functions on Chat completions via a tools array in the OpenAI wire format. When the model decides to use one, it returns a tool_calls array on the assistant message. You execute the call, hand the result back as a tool message, and call the endpoint again. The endpoint is the same one you already use:

POST/api/v1/chat/completions

Requires a krallo_live_ bearer key with the ai:invoke scope.

Krallo never executes your tools. The server only relays the model's requested calls back to you. There is no server-side sandbox, no hosted tools, and no hidden loop — your client owns the agent loop. This keeps every side effect (file writes, network calls, spend) on your infrastructure and under your control.

How the loop works

A tool-calling conversation is a loop you drive. The endpoint itself is stateless — it holds no memory of prior turns, so you resend the full messages array (and the tools array) on every request. Each turn goes:

  1. You POST messages plus a tools array.
  2. The model either answers directly (finish_reason: "stop") or asks to call one or more tools (finish_reason: "tool_calls", with a tool_calls array on the assistant message).
  3. You run each requested call in your own code and produce a result.
  4. You append the assistant message and one tool message per call (carrying the result), then POST again.
  5. Repeat until the model returns a final answer with no tool_calls.

Because you own the loop, you also own the stopping condition. Always cap the number of turns so a model that keeps requesting tools cannot spin indefinitely — and remember that every round is a separate metered request against your wallet (see Billing & quotas).

Declaring tools

Pass a tools array. Each entry is an OpenAI function definition — an object with type: "function" and a function object describing the callable:

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get the current weather for a city.",
    "parameters": {
      "type": "object",
      "properties": {
        "city": { "type": "string", "description": "City name, e.g. \"Paris\"" }
      },
      "required": ["city"]
    }
  }
}
FieldTypeRequiredDescription
type string recommended Always "function". Only function tools are supported — there are no hosted or built-in tools. An entry is accepted as long as it carries a function object.
function.name string yes The function identifier the model will echo back in tool_calls. It is trimmed; an entry with a blank or missing name is silently dropped from the request.
function.description string no What the function does and when to use it. Optional but always provide one — it is the model's primary signal for choosing the tool. Omitted → sent as an empty string.
function.parameters object no A JSON Schema object describing the arguments. If omitted or not an object, it defaults to {"type":"object","properties":{}} — i.e. a no-argument tool.

Your parameters schema is passed through to the model unchanged. The server does not validate arguments against it — schemas are a hint to the model, not an enforced contract. Validate every argument object yourself before acting on it.

Reading tool calls

When the model wants to call a function, the response is a normal chat.completion object whose choice has finish_reason: "tool_calls" and whose assistant message carries a tool_calls array. Krallo re-serializes each call through a strict whitelist — only id, type, and function.name / function.arguments survive; any other provider-specific fields are stripped.

{
  "id": "chatcmpl-9f2c1a7b4e8d4f0a",
  "object": "chat.completion",
  "created": 1752600000,
  "model": "krallo-code",
  "choices": [
    {
      "index": 0,
      "finish_reason": "tool_calls",
      "message": {
        "role": "assistant",
        "content": "",
        "tool_calls": [
          {
            "id": "call_abc123",
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"city\":\"Paris\"}"
            }
          }
        ]
      }
    }
  ],
  "usage": { "prompt_tokens": 84, "completion_tokens": 12, "total_tokens": 96 }
}

Two details worth internalizing:

Returning results

To feed a result back, append two things to your messages array before the next POST: the assistant message exactly as you received it (including its tool_calls), then one tool message per call. Each tool message must set tool_call_id to the matching call's id and put your result (as a string) in content.

{
  "model": "krallo-code",
  "tools": [ /* resend the same tools array */ ],
  "messages": [
    { "role": "user", "content": "What is the weather in Paris?" },
    {
      "role": "assistant",
      "content": "",
      "tool_calls": [
        { "id": "call_abc123", "type": "function",
          "function": { "name": "get_weather", "arguments": "{\"city\":\"Paris\"}" } }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_abc123",
      "content": "{\"temp_c\":19,\"condition\":\"Partly cloudy\"}"
    }
  ]
}

With the results in hand, the model typically produces a final answer and finish_reason: "stop":

{
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "It's 19°C and partly cloudy in Paris right now."
      }
    }
  ],
  "usage": { "prompt_tokens": 132, "completion_tokens": 16, "total_tokens": 148 }
}

A full round-trip

Here is the complete loop in Node.js — declare tools, run every requested call, feed results back, and cap the turns. This is the whole agent; there is no server-side counterpart.

Node.js
const BASE = "https://exe.krallo.com/api/v1";
const KEY = process.env.KRALLO_API_KEY;

// Your real implementations, keyed by tool name.
const handlers = {
  get_weather: async ({ city }) => ({ city, temp_c: 19, condition: "Partly cloudy" }),
};

const 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"],
      },
    },
  },
];

async function complete(messages) {
  const res = await fetch(`${BASE}/chat/completions`, {
    method: "POST",
    headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ model: "krallo-code", messages, tools }),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
  return res.json();
}

async function run(prompt) {
  const messages = [{ role: "user", content: prompt }];

  // You own this loop — bound the turns so a chatty model can't spin forever.
  for (let turn = 0; turn < 8; turn++) {
    const data = await complete(messages);
    const msg = data.choices[0].message;
    messages.push(msg); // keep the assistant turn (incl. tool_calls) in history

    if (!msg.tool_calls || msg.tool_calls.length === 0) {
      return msg.content; // final answer — no more tools requested
    }

    // Run every requested call and append one `tool` message per result.
    for (const call of msg.tool_calls) {
      const fn = handlers[call.function.name];
      let args = {};
      try { args = JSON.parse(call.function.arguments || "{}"); } catch { /* malformed */ }
      const result = fn ? await fn(args) : { error: `unknown tool: ${call.function.name}` };
      messages.push({
        role: "tool",
        tool_call_id: call.id,
        content: JSON.stringify(result),
      });
    }
  }
  throw new Error("Tool loop exceeded its turn cap.");
}

run("What is the weather in Paris?").then(console.log);

Streaming with tools

Tools work with streaming (stream: true), but the delivery is buffered, not incremental. When a request carries a tools array, the model runs on the non-streaming path and the completed result is then replayed to you as a valid Server-Sent Events stream. You receive, in order:

The key difference from text-only streaming: the tool_calls are not fragmented across many chunks — they arrive whole in one frame. If you need true token-by-token streaming, use it only for the text turns (omit tools) and fall back to a non-streaming call when tools are in play.

text/event-stream
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1752600000,"model":"krallo-code","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1752600000,"model":"krallo-code","choices":[{"index":0,"delta":{"tool_calls":[{"id":"call_abc123","type":"function","function":{"name":"get_weather","arguments":"{\"city\":\"Paris\"}"},"index":0}]},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1752600000,"model":"krallo-code","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}

data: [DONE]

Tools alongside a persona

Tool calling composes with your own persona. Pin one to the app or pass a per-request persona id (a persona you own, or a shared system/global one), and the model reasons about your tools under that persona's guidance. The persona shapes the model's behavior; it does not change the tool wire format described here.

Errors

Tool calling adds no new error codes — the handler's own failures use the same OpenAI-shaped error body as Chat completions: { "error": { "message": "...", "type": "...", ... } }. As on that page, auth and scope refusals use the native { "error": { "code", "message" }, "meta" } envelope and the 429 is a flat { "error": "rate_limited", … } body. The ones you are most likely to hit:

StatustypeWhen
400 invalid_request_error Body is not valid JSON, or messages is missing/empty. (A malformed tools array is not rejected — unusable entries are dropped silently.)
400 model_not_found The model is not one of the curated ids. Valid: krallo-code, krallo-chat, krallo-reasoning, krallo-fast.
402 insufficient_credits Your wallet cannot cover the request; the model never runs. The error includes required_units and balance. See Billing.
403 scope error The key lacks the ai:invoke scope.
429 rate limited Per-key rate limit exceeded. Back off and retry — see Rate limits.
502 upstream_error The model provider was unavailable or the stream failed. No credits are charged for a failed call — retry.

Authentication failures (missing or invalid bearer key) return 401. See Authentication and the full Errors reference for the complete catalog.

Next steps