Build with the AI
Chat completions
An OpenAI-compatible endpoint for chat, tools, and streaming — the flagship of the API.
/api/v1/chat/completions speaks the OpenAI Chat Completions wire format, so it is a drop-in
target for OpenAI SDKs and coding agents such as opencode, Aider,
Continue, and nanocoder. Point any OpenAI-compatible client at base URL https://exe.krallo.com/api/v1, send a
krallo_live_… bearer key, and POST the usual messages[] payload. Krallo relays the
request to its model gateway, meters it by token on your wallet, and returns a standard
chat.completion object.
Endpoint
/api/v1/chat/completions
Requires the ai:invoke scope and a valid bearer key (see
Authentication). A CORS preflight
(OPTIONS) returns 204 No Content; any method other than
POST returns 405 method_not_allowed.
This endpoint is stateless. Unlike the chat surface in the Krallo app, it never reads or
writes the human owner's chat memory and does not inherit any stored temperature — every field you want
applied must be sent on the request. The request body is capped at 4 MiB (coding
contexts get large); a body that is not valid JSON returns 400 invalid_request_error.
Models
The model field takes one of four curated public ids. Raw provider or gateway model strings are
never exposed, and the mapping behind each id can change without notice as Krallo tunes its routing — code
against the curated id, not a specific underlying model.
| Model id | Best for |
|---|---|
krallo-code default | Coding and agentic tool use. Used when model is omitted or blank. |
krallo-chat | General-purpose conversation. |
krallo-reasoning | Deeper, multi-step reasoning tasks. |
krallo-fast | Low-latency, lower-cost replies. |
A model value that is not one of these ids returns 400 with type
model_not_found and a message listing the valid ids. You can also enumerate them
programmatically at GET /api/v1/models, which returns an
OpenAI-shaped model list.
Request parameters
The body is a JSON object. Only the fields below are read; any other OpenAI parameters you send are ignored.
| Field | Type | Required | Description |
|---|---|---|---|
messages | array | Yes | A non-empty array of message objects in OpenAI format (role + content, plus tool_calls / tool_call_id for the tool loop). An empty or missing array returns 400 invalid_request_error. |
model | string | No | A curated model id (see above). Omitted or blank falls back to krallo-code. |
temperature | number | No | Sampling temperature passed through to the gateway. A non-numeric value is silently ignored. |
max_tokens | integer | No | Upper bound on generated tokens. A non-integer value is silently ignored. |
tools | array | No | OpenAI tool definitions the model may call. Krallo relays tool-call requests and never executes them — see Tools. |
stream | boolean | No | When true, responds with a Server-Sent Events stream of chat.completion.chunk frames. Defaults to false. |
persona | integer | No | Id of one of your personas to steer the reply. Overrides the app's pinned persona for this request. |
Request example
A minimal, non-streaming completion with curl:
curl https://exe.krallo.com/api/v1/chat/completions \
-H "Authorization: Bearer krallo_live_..." \
-H "Content-Type: application/json" \
-d '{
"model": "krallo-code",
"messages": [
{"role": "system", "content": "You are a terse assistant."},
{"role": "user", "content": "Reverse the string \"krallo\"."}
],
"temperature": 0.2,
"max_tokens": 128
}'
The same call with the official OpenAI SDK — set baseURL and apiKey, nothing else changes:
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://exe.krallo.com/api/v1",
apiKey: process.env.KRALLO_API_KEY, // your krallo_live_... key
});
const completion = await client.chat.completions.create({
model: "krallo-code",
messages: [{ role: "user", content: "Say hello in one sentence." }],
});
console.log(completion.choices[0].message.content);
The response object
A non-streaming request returns a standard OpenAI chat.completion object. Krallo re-serializes
the provider's response through a whitelist, so only the fields below are ever present — the reply is not a
verbatim passthrough. The id is minted by Krallo and the model field echoes the
curated id you requested (never the underlying provider model).
{
"id": "chatcmpl-2f1c9e4a7b0d4e2f8a1b6c3d5e7f9a0b",
"object": "chat.completion",
"created": 1731619200,
"model": "krallo-code",
"choices": [
{
"index": 0,
"finish_reason": "stop",
"message": {
"role": "assistant",
"content": "ollark"
}
}
],
"usage": {
"prompt_tokens": 24,
"completion_tokens": 4,
"total_tokens": 28
}
}
| Field | Description |
|---|---|
choices[].finish_reason | stop for a normal completion, or tool_calls when the model wants you to run tools. |
choices[].message.content | The assistant text. May be null on a turn that only emits tool_calls. |
choices[].message.tool_calls | Present only when the model requests tools (see below). Each entry is { id, type: "function", function: { name, arguments } }. |
usage | Provider-reported token counts. When the provider omits usage these fields are 0, but billing still meters the call from an estimate — see Billing. |
Any system prompt Krallo attaches for its own configuration, and any persona you attach, are sent only to the model — they are never echoed back in choices or usage.
Tool calling
Pass OpenAI-format tool definitions in tools and the model may respond with a
tool_calls array and finish_reason: "tool_calls". Krallo relays the
tool-call request to you and never executes the tool itself — your client runs the tool, appends a
tool-role message with the result, and calls the endpoint again to continue the loop. Only tool
entries that carry a function object with a non-blank name are accepted; malformed
entries are dropped, and a tool with no parameters defaults to an empty object schema.
{
"model": "krallo-code",
"messages": [{"role": "user", "content": "What is the weather in Paris?"}],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city.",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
}
]
}
A tool-requesting reply looks like this — note content is null and finish_reason is tool_calls:
{
"choices": [
{
"index": 0,
"finish_reason": "tool_calls",
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_a1b2c3",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Paris\"}"
}
}
]
}
}
]
}
Tools are your code, not Krallo's. The server never runs a tool, so it cannot leak your data through one — but your client must execute the requested call and validate the model-supplied arguments before acting on them. See Tools for the full loop.
Streaming
Set stream: true to receive a Server-Sent Events stream. The response uses
Content-Type: text/event-stream and emits OpenAI chat.completion.chunk frames — an
opening role delta, incremental content deltas, a final frame carrying
finish_reason, then a literal data: [DONE] sentinel.
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1731619200,"model":"krallo-code","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1731619200,"model":"krallo-code","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1731619200,"model":"krallo-code","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
When you send both stream: true and tools, the turn is computed in full and then
replayed as a valid SSE stream that ends with a tool_calls delta and
finish_reason: "tool_calls" — so your client always receives a well-formed stream, just not
token-by-token while tools are present. If the upstream stream fails mid-flight, the connection carries a
final error frame (data: {"error":{...,"type":"upstream_error"}}) before closing; because
headers were already sent as 200, watch for that frame rather than a non-200 status. Full
details, including client parsing, are on Streaming.
Attaching a persona
A persona prepends your own instructions to the model. Set persona to the integer id of one of
your personas to apply it for a single request; this overrides any persona pinned on the app. The persona
must be one you can use — your own, or a system/global persona. An explicitly requested persona that does
not exist or does not belong to you returns 400 invalid_request_error. A stale
app-pinned persona (for example one you later deleted) is silently skipped so it cannot break every
call. See Personas to create and manage them.
Billing and token metering
This endpoint is token-metered against your owner wallet, not charged per call. Each app
gets a free daily allowance of weighted credit units that resets at UTC midnight; only once you exceed it
(and account-level billing is enabled) do calls debit your prepaid balance. Krallo reserves a small amount
of credit before running inference and reconciles it to the real token count afterward — so an
over-quota request with an empty balance returns 402 insufficient_credits (with
required_units and balance) without ever invoking the model, and a call that fails
upstream is fully refunded. Weighted units are computed from prompt and completion tokens, with output
tokens weighted more heavily. See Billing for the exact formula and
configuration.
Errors
This endpoint returns different error shapes depending on which layer rejects the request. Authentication,
scope, and "API disabled" failures use Krallo's native envelope with a code field and a
meta.request_id:
{ "error": { "code": "insufficient_scope", "message": "Missing required scope.", "required_scope": "ai:invoke" },
"meta": { "request_id": "..." } }
Errors raised by the completion handler itself use the OpenAI error shape with a type field (and no meta), which OpenAI SDKs parse natively:
{ "error": { "message": "Unknown model 'gpt-4'. Valid models: krallo-code, krallo-chat, krallo-reasoning, krallo-fast.",
"type": "model_not_found" } }
| Status | Type / code | When |
|---|---|---|
| 400 | invalid_request_error | Body is not valid JSON, messages is empty or missing, or an explicitly requested persona is unusable. |
| 400 | model_not_found | model is not one of the curated ids. |
| 401 | unauthorized · invalid_token · token_revoked · token_expired | Missing, unrecognized, revoked, or expired bearer key. |
| 402 | insufficient_credits | Over the free quota with billing on and the wallet cannot cover the reserve. Includes required_units and balance. |
| 403 | insufficient_scope · app_disabled · owner_suspended | Key lacks ai:invoke, the app is disabled, or the owner account is suspended. |
| 404 | not_found | The developer API is currently disabled for the platform. |
| 405 | method_not_allowed | A method other than POST (or the OPTIONS preflight). |
| 429 | rate limited | Per-key rate limit exceeded. Carries Retry-After and X-RateLimit-* headers; the body is a simpler flat shape ({"error":"rate_limited", …}). |
| 502 | upstream_error | The model provider was unavailable or the stream failed. The call is not charged. |
See Errors for the full catalog, rate-limit headers, and retry guidance.
Related
- Chat completions reference — the exhaustive field-by-field API reference.
- Tools — the client-driven tool-calling loop in full.
- Streaming — parsing the SSE stream, including tool calls.
- Use with opencode — zero-config editor integration.
- Personas — attach your own instructions.
- Billing — quota, token weighting, and the wallet.