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

POST/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.

NameTypeRequiredDescription
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 idIntended for
krallo-codeCoding and agent loops. default
krallo-chatGeneral conversational use.
krallo-reasoningDeeper, multi-step reasoning.
krallo-fastLowest 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

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 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"
}
FieldTypeDescription
objectstringAlways chat.completion.
idstringA per-response id, chatcmpl- followed by 32 hex characters. Freshly generated; not a handle you can fetch later.
createdintegerUnix timestamp (seconds) when the response was serialized.
modelstringThe curated model id you requested (or krallo-code when omitted). The internal model/provider is never exposed.
choicesarrayAlways exactly one element (index 0).
choices[].indexintegerChoice index, always 0.
choices[].finish_reasonstringProvider finish reason, e.g. stop, length, or tool_calls. Defaults to stop when the provider omits it.
choices[].messageobjectThe assistant message: role, content (string, possibly empty), and optionally tool_calls.
usageobjectprompt_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." }
  ]
}
SituationBehavior
You pass a persona you cannot use, or that does not exist400 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 personaSilently skipped, so a stale default cannot break every call. The request proceeds with no persona.
No persona pinned and none passedThe 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.

StatusType / codeShapeWhen
400invalid_request_errorOpenAIBody 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).
400model_not_foundOpenAImodel is not one of the curated ids. The message lists the valid ids.
401unauthorized / invalid_token / token_expired / token_revokedenvelopeMissing, malformed, expired, or revoked bearer key. See Authentication.
402insufficient_creditsOpenAIOver the free allowance and the wallet cannot cover the pre-authorized reserve. The error body includes required_units and balance. No model runs.
403insufficient_scopeenvelopeKey lacks the ai:invoke scope. Body includes required_scope.
403app_disabled / owner_suspendedenvelopeThe owning app is deactivated, or the owner account is suspended (checked across linked identities).
404not_foundenvelopeThe developer API is not enabled on this deployment.
405method_not_allowedOpenAIAny method other than POST (or the pre-flight OPTIONS, which returns 204).
429rate_limitedrate-limitPer-key route limit or per-IP auth limit exceeded. Honor Retry-After.
502upstream_errorOpenAIThe 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