API reference
POST /chat/completions
Create an OpenAI-compatible chat completion.
This endpoint is a drop-in for the OpenAI Chat Completions wire format. Point any OpenAI-compatible
client (opencode, Aider, Continue, nanocoder, the @ai-sdk/openai-compatible provider, the official
OpenAI SDKs) at base URL https://exe.krallo.com/api/v1 with a krallo_live_…
bearer key and it will POST /chat/completions unchanged. The call is billed by tokens on
the owning developer’s wallet, not per request. For a narrative walkthrough see the
Chat completions guide; this page is the exhaustive
reference.
Krallo relays tool-call requests to your client and never executes tools server-side. Your client drives the agent loop, exactly as it would against OpenAI. See Tools & function calling.
Endpoint
/api/v1/chat/completions
Requires a bearer key with the ai:invoke scope. The
request body is JSON (up to 4 MiB — coding contexts get large). A pre-flight
OPTIONS returns 204 No Content with CORS headers; any method other than
POST returns 405 method_not_allowed. See
Authentication for how keys are issued and sent.
Like the other OpenAI-compatible endpoints, responses are a bare JSON object — there
is no { "data": …, "meta": … } envelope here. Errors raised by this endpoint use the
OpenAI shape { "error": { "message": …, "type": … } }; errors raised by the shared
auth/scope layer use a different shape (see Errors).
Request parameters
All parameters live in the JSON request body. Fields not listed here are ignored.
| Name | Type | Required | Description |
|---|---|---|---|
messages |
array | required | A non-empty ordered array of message objects (the conversation so far). Missing, non-array, or empty returns 400 invalid_request_error. See The messages array. |
model |
string | optional | One of the curated model ids below. Omitted or blank defaults to krallo-code. Any other value returns 400 model_not_found. The value you send is echoed back verbatim in the response model field. |
temperature |
number | optional | Sampling temperature. Applied when present and numeric; a non-numeric value is silently ignored and the model default is used. |
max_tokens |
integer | optional | Upper bound on generated tokens. Applied when present and integral; otherwise the model default is used. |
tools |
array | optional | OpenAI-style tool definitions ({ "type": "function", "function": { … } }). Passed through to the model; the model may respond with tool_calls that you execute. See Tools and function calling. |
stream |
boolean | optional | When true, the response is a Server-Sent Events stream of chat.completion.chunk frames instead of a single object. Default false. See Streaming. |
persona |
integer | optional | Id of a persona to apply for this request, overriding the app’s pinned persona. Must be a persona you own (or a system/global one), or the call returns 400 invalid_request_error. See Persona override. |
Curated models
Krallo exposes stable, curated model ids — never raw provider or model strings. The same list is returned by GET /api/v1/models.
| Model id | Intended for |
|---|---|
krallo-code | Coding and agent loops. default |
krallo-chat | General conversational use. |
krallo-reasoning | Deeper, multi-step reasoning. |
krallo-fast | Lowest latency and cost. |
Sampling controls beyond temperature and max_tokens — such as top_p, n, stop, presence_penalty, response_format, tool_choice, and stream_options — are not read in v1. Send them if your client emits them; they are simply ignored. Requesting more than one choice (n > 1) still returns a single choice.
The messages array
Each entry is an object with a role and content, in standard OpenAI form.
Roles are system, user, assistant, and tool. When
you continue an agent loop, append the assistant’s previous tool_calls message and
one tool message per result, then POST the whole array again — the endpoint is stateless
and keeps no server-side conversation history.
[
{ "role": "system", "content": "You are a terse coding assistant." },
{ "role": "user", "content": "Write a haiku about pointers." }
]
Your messages are forwarded to the model as sent. If you supply a persona (or the app has
a pinned persona), that persona’s instructions are applied ahead of your messages; see
Persona override.
Example request
cURLcurl 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 coding assistant." },
{ "role": "user", "content": "Write a haiku about pointers." }
],
"temperature": 0.3,
"max_tokens": 512
}'
Node.js
const res = await fetch("https://exe.krallo.com/api/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KRALLO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "krallo-code",
messages: [
{ role: "system", content: "You are a terse coding assistant." },
{ role: "user", content: "Write a haiku about pointers." },
],
temperature: 0.3,
max_tokens: 512,
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(`${res.status} ${err.error?.type}: ${err.error?.message}`);
}
const data = await res.json();
console.log(data.choices[0].message.content);
With the official OpenAI SDK, set baseURL to https://exe.krallo.com/api/v1
and apiKey to your krallo_live_… key; the SDK POSTs this endpoint unchanged.
The chat completion object
A non-streaming request returns 200 OK with a chat.completion object. The
response is re-serialized from a whitelist, so it always has exactly the fields below and no provider
internals. id and model are appended last.
{
"object": "chat.completion",
"created": 1752537600,
"choices": [
{
"index": 0,
"finish_reason": "stop",
"message": {
"role": "assistant",
"content": "Silent addresses,\npointing where the value sleeps—\nderef, and it wakes."
}
}
],
"usage": {
"prompt_tokens": 24,
"completion_tokens": 19,
"total_tokens": 43
},
"id": "chatcmpl-4f9c2a1b8e7d4c3fa0b1c2d3e4f5a6b7",
"model": "krallo-code"
}
| Field | Type | Description |
|---|---|---|
object | string | Always chat.completion. |
id | string | A per-response id, chatcmpl- followed by 32 hex characters. Freshly generated; not a handle you can fetch later. |
created | integer | Unix timestamp (seconds) when the response was serialized. |
model | string | The curated model id you requested (or krallo-code when omitted). The internal model/provider is never exposed. |
choices | array | Always exactly one element (index 0). |
choices[].index | integer | Choice index, always 0. |
choices[].finish_reason | string | Provider finish reason, e.g. stop, length, or tool_calls. Defaults to stop when the provider omits it. |
choices[].message | object | The assistant message: role, content (string, possibly empty), and optionally tool_calls. |
usage | object | prompt_tokens, completion_tokens, and total_tokens (their sum). See the note below. |
Token usage and billing
The usage object reflects the counts the upstream provider reported. When a provider (or a
streamed turn) does not return usage, the response can show zeros even though the call did real work.
Do not treat the response usage as your bill. Billing settles on effective tokens: the provider’s reported usage when present, otherwise an internal estimate from the request and response text. A call metered off an estimate can therefore be billed while usage reads 0. Reconcile spend against GET /api/v1/usage, not this field.
AI calls are priced by weighted tokens, not per request. Each app has a free daily allowance (in credit
units) that resets at UTC midnight; once it is exhausted and billing is enabled, calls debit
the owner’s prepaid wallet. A fixed reserve is pre-authorized before inference runs and reconciled
to the real token count afterward — so an over-quota caller who cannot cover the reserve gets
402 without ever running the model, and a failed call is fully refunded. See
Billing & quotas for the credit model and the weighting formula.
Streaming
Set "stream": true to receive Server-Sent Events. The response is
Content-Type: text/event-stream with status 200, and each frame is a
data: line carrying a chat.completion.chunk object: an opening frame with
delta.role, then content frames with delta.content, a final frame with an
empty delta and a finish_reason, and finally the literal data: [DONE].
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":1752537600,"model":"krallo-code","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":1752537600,"model":"krallo-code","choices":[{"index":0,"delta":{"content":"Silent"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":1752537600,"model":"krallo-code","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
A streamed request that also sends tools is computed on the buffered path and then replayed as a single burst of SSE frames (role, then content, then a tool_calls delta, then finish_reason: "tool_calls", then [DONE]). The stream is valid but not incremental when tools are present. Because headers are already sent with 200, a mid-stream provider failure arrives as an SSE {"error":{…}} frame, not an HTTP error status.
See the Streaming guide for parsing patterns and reconnection advice.
Tools and function calling
Pass OpenAI-style tool definitions in tools. Each definition needs a
function object with a non-blank name; description and
parameters (a JSON Schema object) are optional and default to an empty object schema.
Definitions without a usable function.name are dropped.
{
"model": "krallo-code",
"messages": [
{ "role": "user", "content": "What's the weather in Paris?" }
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Look up current weather for a city.",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
}
]
}
When the model decides to call a tool, the choice comes back with
finish_reason: "tool_calls" and a tool_calls array on the message. Krallo does
not run the tool — you execute it and continue the loop by appending a
tool message with the result and POSTing again.
{
"object": "chat.completion",
"created": 1752537600,
"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": 58, "completion_tokens": 12, "total_tokens": 70 },
"id": "chatcmpl-…",
"model": "krallo-code"
}
Each tool call is normalized to { id, type: "function", function: { name, arguments } },
with arguments a JSON-encoded string (defaulting to "{}"). Note that
tool_choice is not read — the model decides whether to call a tool. Full guidance is in
Tools & function calling.
Persona override
A persona is a reusable instruction set you attach to a call. Pass its integer id in
persona to apply it for this request, overriding whatever persona the app has pinned. The
persona must be usable by you — one you own, or a shared system/global persona — checked across your
linked identities. The ai:invoke scope is sufficient; no separate personas scope is needed
to reference one here.
{
"model": "krallo-chat",
"persona": 42,
"messages": [
{ "role": "user", "content": "Summarize this changelog for a release note." }
]
}
| Situation | Behavior |
|---|---|
You pass a persona you cannot use, or that does not exist | 400 invalid_request_error — "Persona '…' was not found or does not belong to you." |
The app has a pinned persona that was since deleted, and you pass no persona | Silently skipped, so a stale default cannot break every call. The request proceeds with no persona. |
| No persona pinned and none passed | The model runs with your messages alone. |
Manage and list personas via the Personas endpoints.
Rate limits
Every request counts against your key’s per-minute rate limit, driven by the app’s tier
(or a per-app override). Every authenticated response carries advisory X-RateLimit-Limit,
X-RateLimit-Remaining, and X-RateLimit-Reset headers; a 429
additionally sets Retry-After (60 seconds). A separate, coarser per-IP limit guards the
auth path against invalid-token floods. See Rate limits for the
per-tier ceilings and back-off guidance.
Errors
This endpoint uses two error shapes. Errors from its own request handling (bad JSON, invalid
parameters, credits, upstream failures) use the OpenAI shape
{ "error": { "message": …, "type": … } }, sometimes with extra fields. Errors from the
shared auth/scope layer use the standard API envelope
{ "error": { "code": …, "message": … }, "meta": { "request_id": … } }. The
429 rate-limit body is a third, simpler shape:
{ "error": "rate_limited", "hint": …, "retryAfterSeconds": … }. See
Errors for the platform-wide model and correlation ids.
| Status | Type / code | Shape | When |
|---|---|---|---|
| 400 | invalid_request_error | OpenAI | Body is not a JSON object, messages is missing / non-array / empty, or a persona you passed is not found or not usable by you (your own, or a shared system/global one). |
| 400 | model_not_found | OpenAI | model is not one of the curated ids. The message lists the valid ids. |
| 401 | unauthorized / invalid_token / token_expired / token_revoked | envelope | Missing, malformed, expired, or revoked bearer key. See Authentication. |
| 402 | insufficient_credits | OpenAI | Over the free allowance and the wallet cannot cover the pre-authorized reserve. The error body includes required_units and balance. No model runs. |
| 403 | insufficient_scope | envelope | Key lacks the ai:invoke scope. Body includes required_scope. |
| 403 | app_disabled / owner_suspended | envelope | The owning app is deactivated, or the owner account is suspended (checked across linked identities). |
| 404 | not_found | envelope | The developer API is not enabled on this deployment. |
| 405 | method_not_allowed | OpenAI | Any method other than POST (or the pre-flight OPTIONS, which returns 204). |
| 429 | rate_limited | rate-limit | Per-key route limit or per-IP auth limit exceeded. Honor Retry-After. |
| 502 | upstream_error | OpenAI | The model provider was unavailable or the stream failed. The call is refunded; retry. |
A 502 upstream_error reverses the reservation, so retrying is safe from a billing standpoint — the failed attempt was already refunded.
Related
- Chat completions guide — end-to-end usage and patterns.
- Streaming — parsing the SSE chunk format.
- Tools & function calling — the client-driven agent loop.
- GET /api/v1/models — list the curated model ids.
- Personas — create and manage personas.
- Billing & quotas — credits, free quota, and wallet debits.
- Errors — the shared error model and correlation ids.