# Leiolai Developer API

Build with Leiolai through OpenAI-compatible chat completions or Leiolai's continuous generation API, with adjustable reasoning effort and up to 11 million tokens of context.

Machine-readable spec: https://leiolai.com/docs/openapi.yaml

## Quick start

The API authenticates with bearer tokens. The finite endpoint implements the documented subset of OpenAI Chat Completions below, so you can use raw HTTP or an OpenAI SDK. Changing only the SDK base URL is not enough because every request must also choose `mode`. Pass that provider field with the SDK's `extra_body` option.

- Base URL: `https://api.leiolai.com/v1`
- `POST /v1/chat/completions`: send a conversation, get one assistant response.
- `POST /v1/infinite/chat/completions`: open a continuous response that can accept new context while it runs.
- `POST /v1/infinite/inject`: send context, buffer state, or a stop command to a continuous session.
- `GET /v1/models`: returns `leiolai-1`, the currently available model.
- Authorization: send your key on every request: `Authorization: Bearer sb_...`

Never put an API key in frontend JavaScript. Anyone can extract it and spend your balance. Keep the key on a backend you control, then have your frontend call that backend.

```bash
curl https://api.leiolai.com/v1/chat/completions \
  -H "Authorization: Bearer sb_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "leiolai-1",
    "messages": [{"role": "user", "content": "Say hi in five words."}],
    "stream": true,
    "mode": "private"
  }'
```

With the OpenAI Python SDK:

```python
from openai import OpenAI

client = OpenAI(
    api_key="sb_your_key",
    base_url="https://api.leiolai.com/v1",
)

response = client.chat.completions.create(
    model="leiolai-1",
    messages=[{"role": "user", "content": "Say hi in five words."}],
    reasoning_effort="low",
    extra_body={"mode": "private"},
)
print(response.choices[0].message.content)
```

Use Chat Completions, not the Responses API. Tool calls, audio input, embeddings, fine-tuning, and batch endpoints are not part of this compatibility surface.

## Model

One model: Leiolai. There is no model selection. Every request runs Leiolai. Depth is controlled by `reasoning_effort`, not by picking a different model.

- Model ID: `leiolai-1`. Send this canonical value in every request. Compatibility layers may qualify the same ID as `openai/leiolai-1` or `leiolai/leiolai-1`; the suffix still resolves to `leiolai-1` and does not select a different model. Any other non-empty model ID is refused with 404 `model_not_found`.
- Context window: up to 11M tokens (11,000,000). A floor-to-ceiling context window. Bring the whole project.
- Modality: messages accept text, image, and supported inline file parts. Responses are text.

## Request format

`POST /v1/chat/completions` with a JSON body:

```json
{
  "model": "leiolai-1",
  "messages": [
    {"role": "user", "content": "Explain why the sky changes color at sunset."}
  ],
  "stream": true,
  "stream_options": {"include_usage": true},
  "reasoning_effort": "medium",
  "mode": "private"
}
```

Creates a completion for the supplied conversation. The body follows the OpenAI chat completions schema; the supported fields are below.

- `model`: required. Use `leiolai-1`.
- `messages`: role and content pairs. Role is system, user, or assistant. Content is a plain string or an array of typed parts.
- Text part: `{"type":"text","text":"..."}`.
- Image part: `{"type":"image_url","image_url":{"url":"..."}}` with a base64 `data:` image URL. Images work in both modes. Remote image URLs are not accepted.
- File part: `{"type":"file","file":{"filename":"notes.md","file_data":"data:text/markdown;base64,..."}}`. Inline files may contain up to 2 MiB of UTF-8 text, code, CSV, JSON, HTML, XML, YAML, SQL, or Markdown. `file_id` references and binary files such as PDFs are not accepted by this endpoint.
- `stream`: when true, content is returned as server-sent event deltas. When omitted, the response is a single JSON object.
- `stream_options.include_usage`: when true with `stream: true`, the stream includes OpenAI's final usage-only chunk before `data: [DONE]`.
- `reasoning_effort`: optional response depth: low, medium, high, or xhigh. Defaults to low.
- `max_tokens`: optional output cap, in tokens. If omitted, Leiolai derives a ceiling from the available balance and API budget. `max_completion_tokens` is an alias.
- `mode`: required. Send `private` or `non-private`. There is no default because this choice changes both privacy and price. Images and supported inline files work in both modes. Omitting `mode` returns HTTP 400.
This is a Chat Completions-compatible subset, not a promise that every OpenAI field or SDK feature is implemented. Only the fields listed here are part of the contract. You always receive one choice.

## Streaming

When `stream` is true, the response uses OpenAI Chat Completions server-sent events and ends with `data: [DONE]`. Append every `choices[0].delta.content` value to build the reply. Leiolai may hold content until it is final, so do not assume token-by-token timing or a minimum number of content chunks. When `stream` is omitted, the response is a single `chat.completion` object with `choices[0].message.content`.

This example includes `"stream_options":{"include_usage":true}` in the request:

```text
data: {"id":"chatcmpl-8f1c2d3a4b5e6f7a8b9c0d1e","object":"chat.completion.chunk","created":1787000000,"model":"leiolai-1","choices":[{"delta":{"role":"assistant","content":"Hi there"},"index":0,"finish_reason":null}],"usage":null}

data: {"id":"chatcmpl-8f1c2d3a4b5e6f7a8b9c0d1e","object":"chat.completion.chunk","created":1787000000,"model":"leiolai-1","choices":[{"delta":{},"index":0,"finish_reason":"stop"}],"usage":null}

data: {"id":"chatcmpl-8f1c2d3a4b5e6f7a8b9c0d1e","object":"chat.completion.chunk","created":1787000000,"model":"leiolai-1","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":2,"total_tokens":16}}

data: [DONE]
```

Read it:

- `id`: the response ID. It stays the same across every chunk. Include it when reporting a bad answer.
- `choices[0].delta.role`: `assistant`, on the first chunk only.
- `choices[0].delta.content`: append each value to build the reply. A short answer may arrive in one content chunk.
- `data: [DONE]`: the final frame. Close the stream when you see it.
- `usage`: token counts for the request: `prompt_tokens`, `completion_tokens`, and `total_tokens`. The non-streaming object always includes them. A stream includes them only when you send `stream_options.include_usage: true`; OpenAI's usage-only chunk has an empty `choices` array and arrives immediately before `[DONE]`. Earlier chunks carry `usage: null`. These are the counts you are billed for.
- `choices[0].finish_reason`: `stop` when Leiolai completed the answer, `length` when it reached your `max_tokens` cap, and `content_filter` when the content policy stopped the response. Null before the terminal chunk.

## Continuous generation

What's an output limit? Continuous generation keeps a response open and lets you add context while it runs. There is no fixed product output limit. If you omit `max_tokens`, Leiolai derives the session ceiling from your available balance and API budget. The opening endpoint always returns server-sent events. It is a Leiolai extension, not a standard Chat Completions stream, so use an SSE client that preserves every event.

Open a session with `POST /v1/infinite/chat/completions`:

```bash
curl -N https://api.leiolai.com/v1/infinite/chat/completions \
  -H "Authorization: Bearer sb_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "leiolai-1",
    "messages": [{"role": "user", "content": "Teach me astronomy until I stop you."}],
    "reasoning_effort": "medium",
    "mode": "private",
    "continuous_protocol": 2,
    "lookahead": true,
    "consumer_lease": true,
    "realtime_audio": true
  }'
```

Opening fields:

- `model`, `messages`, `reasoning_effort`, and `mode` have the same meaning as on the finite endpoint. Text, image, and supported inline file parts work in both modes.
- `continuous_protocol`: required. Send `2`. Omitting this field or sending another value returns HTTP 400.
- `lookahead`: optional. When true, the stream includes a replaceable preview separate from delivered text.
- `consumer_lease`: optional. When true, send `consumer_heartbeat` while the stream is live. This lets Leiolai close the session if the downstream consumer disconnects but a proxy keeps the HTTP connection open.
- `realtime_audio`: optional. Set this when the client speaks delivered text as it arrives. Leiolai will not rewrite words the user may already have heard. Send buffer reports to pace delivery.
- `max_tokens` or `max_completion_tokens`: optional output-token ceiling for the session. If both are sent, they must be the same positive integer. The stream ends when the ceiling is reached.

The stream begins with the session event:

```text
data: {"session_id":"inf-0123456789abcdef","continuous_protocol":2}
```

Keep that opaque `session_id`. Send it as the `session` query parameter on every inject request. Send the same API key on the opening request and every inject. A session belongs to the API key that opened it.

The continuous stream has four data event shapes:

- `{"choices":[{"delta":{"content":"..."}}]}` is delivered answer text. Append every `content` value. Delivered bytes never change.
- `{"lookahead":"..."}` is the full current preview. Replace the previous preview with this value. An empty string clears it. Never append lookahead to delivered text.
- `{"inject_ack":{"id":"...","seq":1,"anchor":20,"text":"..."}}` confirms a completed user message. `anchor` is the UTF-8 byte position where that message belongs in the delivered answer. It may be ahead of the answer bytes already received. Leiolai sends `inject_ack` before sending answer bytes beyond that position.
- `data: [DONE]` ends the session. A pause in output does not end the session.

SSE comment lines are keepalives and can be ignored. One possible sequence is:

```text
data: {"session_id":"inf-0123456789abcdef","continuous_protocol":2}

data: {"choices":[{"delta":{"content":"The first sentence. "}}]}

data: {"lookahead":"This preview may still change."}

data: {"lookahead":""}

data: {"inject_ack":{"id":"opaque-receipt-id","seq":1,"anchor":20,"text":"Wait, explain that more simply."}}

data: {"choices":[{"delta":{"content":"Here is the simpler version. "}}]}

: keepalive

data: [DONE]
```

### Inject context

Send live context to `POST /v1/infinite/inject?session=<session_id>`. Protocol 2 accepts these fields:

- `text`: the user's completed message.
- `text_partial`: the user's complete message so far. Each update replaces the previous value.
- `image`: the latest image as a base64 data URI. It replaces the prior live image after screening.
- `buffer`: the client's queued-output state. See Buffer control below.
- `consumer_heartbeat`: send only this field with `true` to confirm the downstream consumer is still connected. Use it only when the opening request set `consumer_lease: true`.
- `stop`: send `true` by itself to end the session.

Send inject requests one at a time, in order, within each session. Do not send `text` and `text_partial` together. Send `stop` by itself. Unknown fields return HTTP 400.

As the user interrupts, send the complete transcript so far through `text_partial`. Leiolai clears the old lookahead and pauses delivery. When the user finishes, send the completed message through `text`. Leiolai adds it to the conversation, sends `inject_ack` with the exact insertion point, and resumes from the updated context.

The final-injection POST also returns the receipt for correlation:

```json
{
  "ok": true,
  "inject_ack": {
    "id": "opaque-receipt-id",
    "seq": 1,
    "anchor": 20,
    "text": "Wait, explain that more simply."
  }
}
```

Use the SSE receipt to place the completed user message. If `anchor` is ahead of the delivered answer, wait until you have received that many UTF-8 bytes. Split the answer at `anchor`, insert the user message, then display later answer text after it.

Opening messages accept the same image and inline file parts as finite chat. During a live session, `image` updates the current visual context. A live image must be a base64 data URI with up to 8 MiB of decoded image data; the complete inject body may be up to 12 MiB. Files cannot be added through the inject endpoint after the session opens.

### Buffer control

Buffer control lets a client maintain a target amount of consumable output. It is not voice-specific. Use it for speech, captions, translation, rendering, or any consumer that measures queued work in seconds.

Send:

```json
{
  "buffer": {
    "remaining_seconds": 1.2,
    "drain_rate": 1.0
  }
}
```

- `remaining_seconds` is the non-negative number of seconds currently queued by the client.
- `drain_rate` is the queue's consumption speed relative to real time, from `0` through `4`. Send `0` while paused or unable to consume, `1` at normal speed, and `2` at twice normal speed.

The server updates its queue estimate between reports by subtracting elapsed time multiplied by the last `drain_rate`. Report immediately when queued output changes, consumption pauses, resumes, or fails, or the drain rate changes. While output is draining, report at least every 500 ms. Faster reports are accepted. Send one request at a time so reports arrive in order.

At the current defaults, Leiolai pauses delivery when the client reports more than 2.5 seconds of queued output. Generation continues while delivery is paused, so lookahead may keep changing. Leiolai resumes delivery when the queue reaches 2.5 seconds or less. If the queue drops below 0.8 seconds and more output is ready, Leiolai may add enough to target about 1.5 seconds. These thresholds are controlled by the server. Clients should report their actual state instead of reproducing this logic.

## Errors

Before streaming begins, API-generated errors use the OpenAI-style JSON envelope below:

```json
{
  "error": {
    "message": "mode is required. Send \"private\" or \"non-private\".",
    "type": "invalid_request_error",
    "param": "mode",
    "code": "invalid_value"
  }
}
```

`error.type` is the broad error class. `error.code` is the stable programmatic code, and `error.param` names the request field when one caused the failure. After a stream begins, its HTTP status remains 200. If the content policy stops a finite stream, its final choice has `finish_reason: "content_filter"`. If a continuous stream fails after it starts, it sends an error data frame before `data: [DONE]`.

Every API-generated response includes `X-Request-Id`. Log it when you contact support. You may send your own trace value in `X-Client-Request-Id`; valid values are echoed in the response.

| Status | Code | What it means |
| --- | --- | --- |
| 401 | invalid_api_key | No valid API key was provided. Check the bearer token. |
| 402 | insufficient_credits | Your available balance cannot cover this request. Top up or request less output. |
| 402 | api_budget_exhausted | The key or the account reached its API budget for the period. Raise or clear the budget in your account. |
| 402 | api_budget_required | Auto reload is on for the account, so API use needs a budget. Set one, or turn auto reload off. |
| 400 | invalid_value | A field is invalid, two fields conflict, or `mode` is missing. |
| 400 | invalid_json | The request body is not valid JSON. |
| 403 | content_policy_violation | The content policy blocked the input before streaming began. |
| 403 before stream; 200 after | content_filter | The content policy stopped the response. Before streaming, this is a 403. An active finite stream uses `finish_reason: "content_filter"`; an active continuous stream sends an error data frame before `[DONE]`. |
| 404 | model_not_found | The requested model is not available. |
| 404 | session_not_found | The continuous session ended, does not exist, or belongs to another API key. |
| 405 | method_not_allowed | This route does not accept that HTTP method. |
| 413 | request_too_large | The request body exceeds the accepted size. |
| 429 | rate_limit_exceeded | You exceeded the request rate limit. Wait for the number of seconds in `Retry-After`, then try again. |
| 500 | streaming_unavailable | Streaming is unavailable. Try again. |
| 503 before stream; 200 after | billing_unavailable | API billing is temporarily unavailable. During an active continuous stream, an error data frame arrives before `[DONE]`. |
| 503 | api_budget_unverifiable | Spending records are temporarily unavailable. Retry shortly. |
| 503 | vision_unavailable | Image understanding is temporarily unavailable. Retry shortly. |
| 503 before stream; 200 after | service_busy | The service is at capacity. A 503 sent before streaming includes `Retry-After` when available; an active continuous stream ends with an error data frame. |
| 503 | service_unavailable | The request could not be completed. Retry shortly. |

## Token pricing

Billing is per token, with separate input and output rates. `reasoning_effort` and `mode` determine those rates. Early access pricing currently discounts research rates; it does not apply to xhigh or private requests. Prices in USD per 1M tokens:

| Reasoning effort | Research in | Research out | Private in | Private out |
| --- | --- | --- | --- | --- |
| low | $0.01 | $0.02 | $20 | $90 |
| medium | $0.25 | $0.65 | $40 | $175 |
| high | $6.40 | $21 | $80 | $350 |
| xhigh | $160 | $700 | $160 | $700 |

The request charge is `(input tokens × input rate + output tokens × output rate) / 1,000,000`. The original submitted input is charged once. For continuous generation, that means the opening messages; later inject updates are not charged as a second input request. The rates are fixed when the request is accepted, so an in-flight request cannot be repriced.

## Access and billing

Keys are created and revoked in your Leiolai account. Revoking a key takes effect immediately. API usage draws on your account balance and is billed separately from app subscriptions.

Support: https://leiolai.com/support
Get Leiolai: https://leiolai.com/about/#get
