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:
/api/v1/chat/completionsRequires 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:
- You POST
messagesplus atoolsarray. - The model either answers directly (
finish_reason: "stop") or asks to call one or more tools (finish_reason: "tool_calls", with atool_callsarray on the assistant message). - You run each requested call in your own code and produce a result.
- You append the assistant message and one
toolmessage per call (carrying the result), then POST again. - 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"]
}
}
}
| Field | Type | Required | Description |
|---|---|---|---|
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:
function.argumentsis a JSON-encoded string, not an object (it may even be"{}"). Parse it before use, and never assume it is well-formed or complete — wrap the parse in a try/catch and validate the result against your schema.- The model may return multiple calls in one turn — iterate the entire
tool_callsarray. It may also include somecontentalongside the calls, though it is usually empty when the turn is purely a tool request.
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.jsconst 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:
- a role frame —
delta: {"role":"assistant"}; - a content frame if the message has any text (skipped when empty);
- a single frame carrying the whole
tool_callsarray as a delta — each entry gets anindexso SDKs that reassemble streamed calls still work; - a final frame with
finish_reason: "tool_calls"; - the terminating
data: [DONE]line.
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.
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:
| Status | type | When |
|---|---|---|
| 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
- Chat completions — the endpoint reference and every parameter.
- Streaming — the SSE frame format in full.
- Personas — attach your own system behavior to tool-using turns.
- Billing & quotas — how each loop turn is metered.