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

POST/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 idBest for
krallo-code defaultCoding and agentic tool use. Used when model is omitted or blank.
krallo-chatGeneral-purpose conversation.
krallo-reasoningDeeper, multi-step reasoning tasks.
krallo-fastLow-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.

FieldTypeRequiredDescription
messagesarrayYesA 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.
modelstringNoA curated model id (see above). Omitted or blank falls back to krallo-code.
temperaturenumberNoSampling temperature passed through to the gateway. A non-numeric value is silently ignored.
max_tokensintegerNoUpper bound on generated tokens. A non-integer value is silently ignored.
toolsarrayNoOpenAI tool definitions the model may call. Krallo relays tool-call requests and never executes them — see Tools.
streambooleanNoWhen true, responds with a Server-Sent Events stream of chat.completion.chunk frames. Defaults to false.
personaintegerNoId 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:

Node.js
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
  }
}
FieldDescription
choices[].finish_reasonstop for a normal completion, or tool_calls when the model wants you to run tools.
choices[].message.contentThe assistant text. May be null on a turn that only emits tool_calls.
choices[].message.tool_callsPresent only when the model requests tools (see below). Each entry is { id, type: "function", function: { name, arguments } }.
usageProvider-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.

JSON
{
  "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" } }
StatusType / codeWhen
400invalid_request_errorBody is not valid JSON, messages is empty or missing, or an explicitly requested persona is unusable.
400model_not_foundmodel is not one of the curated ids.
401unauthorized · invalid_token · token_revoked · token_expiredMissing, unrecognized, revoked, or expired bearer key.
402insufficient_creditsOver the free quota with billing on and the wallet cannot cover the reserve. Includes required_units and balance.
403insufficient_scope · app_disabled · owner_suspendedKey lacks ai:invoke, the app is disabled, or the owner account is suspended.
404not_foundThe developer API is currently disabled for the platform.
405method_not_allowedA method other than POST (or the OPTIONS preflight).
429rate limitedPer-key rate limit exceeded. Carries Retry-After and X-RateLimit-* headers; the body is a simpler flat shape ({"error":"rate_limited", …}).
502upstream_errorThe 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