OpenAI-compatible REST API

Arona exposes an OpenAI-compatible REST surface under /v1/* for LLM chat, embeddings, model listing, health probing and async video generation. Any OpenAI SDK pointed at the base URL works for chat and embeddings; the video endpoints follow OpenAI's task-style submit/poll convention.

All request and response bodies are JSON. Errors use a uniform shape (see Errors); authentication failures at the middleware layer are the one exception and are returned as plain text (see Authentication).

Endpoints at a glance

MethodPathDescription
POST/v1/chat/completionsChat turn, streaming or non-streaming.
POST/v1/embeddingsEmbedding vectors for one or many inputs.
GET/v1/modelsRouter models merged with quick-start models.
GET/v1/health{"status": "ok", "version", "build_hash", "models", "providers"}.
POST/v1/video/generationsSubmit an async video generation task.
GET/v1/video/generations/{id}Poll a video task's status / result.

/api/health, /healthz and /readyz are additional readiness probes (Kubernetes-style aliases of /v1/health).

Authentication

Chat, embeddings and video endpoints authenticate with an API key in the Authorization: Bearer header. API keys are created through the management plane (keys.create, see the JSON-RPC API) and look like arona-<uuid>. They are stored server-side as SHA-256 hashes.

text
1
Authorization: Bearer arona-CHANGE_ME

Middleware-level rejections are plain-text bodies, not the JSON error shape described in Errors — the JSON shape is produced only once a request reaches a handler.

Every authenticated /v1 request also passes an in-memory per-key rate limiter (default 60 RPM, 60-second window, configurable via ARONA_API_RATE_LIMIT_RPM). Exceeding it returns 429 plain text: Rate limit exceeded. Try again later. Tier-level quota and rate limits are enforced separately and return JSON 429s with a Retry-After header (see 429 and Retry-After).

Managing API keys, projects and their scoping is covered in Authentication & Security.

POST /v1/chat/completions

The core OpenAI-compatible chat endpoint, with streaming support and arona-specific extensions (conversation_id, memory, extra, provider).

Request body

FieldTypeRequiredNotes
modelstringyesModel id as listed by GET /v1/models.
messagesarrayyesChat turns, see below.
streambooleannoDefault false. When true the response is an SSE stream (see Streaming).
temperaturenumbernoSampling temperature, forwarded upstream.
max_tokensintegernoCompletion token cap, forwarded upstream.
conversation_idstringnoSession affinity + persistence. The conversation must exist and belong to the API-key user (403 conversation_forbidden otherwise, 404 conversation_not_found if it does not exist). The user turn is persisted at send time and the assistant reply when the turn completes; routing pins the conversation to the backend that first served it.
memorybooleannoMemory gateway override. Defaults to true (memory recall is injected when the memory gateway is enabled); false disables recall injection for this request.
extraobjectnoFree-form passthrough merged into the upstream payload top level (see below).
toolsarraynoOpenAI-style function-call definitions, passed through verbatim to the upstream.
providerstringnoExplicit backend selection hint matching a backend name (or kind) case-insensitively. When set, only backends matching the hint are candidates.

messages entries are { "role": "user" | "assistant" | "system", "content": "..." }. Two extensions are forwarded upstream for multimodal / agent workloads:

extra merge rules: every extra key is merged into the upstream request payload at the top level, with two hard guarantees — the reserved keys model, messages, stream, temperature, max_tokens and options are never overridden, and neither is any key the gateway itself already set. Non-object extra values are ignored.

tools entries are { "type": "function", "function": { "name", "description"?, "parameters"? } } and are forwarded verbatim.

Non-streaming response

json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "id": "chatcmpl-...",
  "object": "chat.completion",
  "created": 1720000000,
  "model": "Qwen/Qwen3-1.7B",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "Hello!" },
      "finish_reason": "stop"
    }
  ],
  "usage": { "prompt_tokens": 12, "completion_tokens": 2, "total_tokens": 14 }
}

Streaming

Set "stream": true. The response is a text/event-stream SSE stream — one data: line per chunk, each carrying a single JSON ChatChunk:

json
1
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1720000000,"model":"Qwen/Qwen3-1.7B","choices":[{"index":0,"delta":{"role":"assistant","content":"Hel"},"finish_reason":null}]}

Example

bash
1
2
3
4
5
6
7
8
curl http://192.0.2.10:8080/v1/chat/completions \
  -H "Authorization: Bearer arona-CHANGE_ME" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Qwen/Qwen3-1.7B",
    "messages": [{"role": "user", "content": "Hello!"}],
    "stream": true
  }'

POST /v1/embeddings

FieldTypeRequiredNotes
modelstringyesEmbedding model id (e.g. nomic-embed-text — a bare name also matches a :latest tag).
inputstring or string[]yesOne input, or many.

Response: { "object": "list", "data": [ { "object": "embedding", "embedding": [...], "index": 0 } ], "model": "...", "usage": { "prompt_tokens", "completion_tokens", "total_tokens" } }.

GET /v1/models

Lists the models routable today: every healthy registered backend's model listing, merged with the built-in quick-start models (always advertised, even before a backend is registered): Qwen/Qwen3-0.6B, Qwen/Qwen3-1.7B, HuggingFaceTB/SmolLM2-1.7B-Instruct, google/gemma-3-1b-it, microsoft/Phi-4-mini-instruct, deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B.

json
1
2
3
4
5
6
{
  "object": "list",
  "data": [
    { "id": "Qwen/Qwen3-0.6B", "object": "model", "owned_by": "huggingface" }
  ]
}

Quick-start models appear with owned_by set to their provider; router models carry the owning backend's name.

Video generation

Task-style video endpoints for video-capable backends (e.g. minimax-cloud, see Backends). Jobs progress asynchronously; poll the status endpoint until done.

POST /v1/video/generations

FieldTypeRequiredNotes
modelstringyesVideo model id registered on a video-capable backend.
promptstringyesGeneration prompt.
negative_promptstringnoNegative prompt.
imagesarraynoConditioning/reference images as an array of { "data_base64": "...", "mime_type": "image/png" } objects.
duration_secondsintegernoRequested duration.
width / heightintegernoOutput resolution.
providerstringnoExplicit backend selection hint (backend name).
extraobjectnoBackend-specific workflow overrides (seed, steps, cfg, ...).

Success → 200:

json
1
2
3
4
5
6
7
{
  "id": "3f2d...-uuid",
  "object": "video.generation",
  "model": "minimax-h3",
  "status": "queued",
  "created_at": 1720000000
}

Errors: 400 missing_fields when model or prompt is absent; 503 video_backend_error / no_backend when no healthy video-capable backend serves the model; 429 quota_error / quota_exceeded when the monthly quota is exhausted.

GET /v1/video/generations/{id}

Returns the task status:

json
1
2
3
4
5
6
7
8
9
10
11
{
  "id": "3f2d...-uuid",
  "object": "video.generation",
  "model": "minimax-h3",
  "status": "running",
  "progress": 40,
  "result": null,
  "error": null,
  "cost": 0.0,
  "created_at": 1720000000
}

Video jobs also fan progress out over the RPC SSE sidecar (video.progress / video.done / video.failed, see Events & Notifications).

Errors

Gateway-level errors use one shape (json_error_response):

json
1
2
3
{
  "error": { "message": "...", "type": "...", "code": "..." }
}
Statustype / codeWhen
400invalid_request / missing_fields, missing_index, bad_id, ...Malformed or missing request fields.
403auth_error / conversation_forbiddenconversation_id belongs to another user.
404invalid_request_error / model_not_foundNo backend serves the requested model. Message: No backend available for model: <model>.
404invalid_request / conversation_not_foundConversation not found.
404not_found / no_jobVideo job not found.
502server_error / bad_gatewayUpstream non-2xx: message upstream <status>: <detail> (detail from the upstream error body, bounded to 4 KB). Transport failures (connect/read/timeout) also map to 502 with the error string.
500server_error / backend_errorOther backend failures (e.g. backend does not support the operation).
500server_error / internal_errorAny remaining gateway internal error.
429see belowQuota / rate-limit rejections with Retry-After.

429 and Retry-After

429 responses include a Retry-After header (seconds) so OpenAI-compatible clients back off:

TriggerStatus bodyRetry-After
Monthly quota exceeded{"error":{"message":"Monthly quota exceeded for your billing tier. ...","type":"quota_error","code":"quota_exceeded"}}Seconds until the next month.
Tier per-minute rate limit{"error":{"message":"Rate limit exceeded for your billing tier. Retry later.","type":"rate_limit_error","code":"rate_limit_exceeded"}}60.
In-memory per-key limiter (60 RPM default)plain text Rate limit exceeded. Try again later.none (middleware rejection).

Tiers, quota scoping and usage accounting are described in Billing & Usage.

Usage recording

Every /v1 request records a usage row under the API-key prefix (arona-XX) when it completes (non-streaming chat, streaming chat at the terminal chunk, embeddings, and video jobs on completion with their computed cost). See Billing & Usage for the recording model and how quota is enforced.