Chat completions
Beta. Updated 2026-10-29.
POST /v1/chat/completions accepts the OpenAI chat completions request.
Example
curl https://citadel.albastov.com/v1/chat/completions \
-H "Authorization: Bearer $CITADEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "qwen3.8-27b", "max_tokens": 50,
"messages": [{"role": "system", "content": "Answer in one line."},
{"role": "user", "content": "What is a KV cache?"}],
"chat_template_kwargs": {"enable_thinking": false}}'Request
Headers
| Header | Required | Value |
|---|---|---|
Authorization | yes | Bearer hlk_... |
Content-Type | yes | application/json |
X-LLM-Wait | no | Seconds to wait for a loading model, 0 to 900 |
X-Request-Id | no | 16 to 64 lowercase hex characters |
X-LLM-Wait behaviour: Model switching.
Body
| Field | Type | Default |
|---|---|---|
model | string | qwen3.8-27b |
messages | array | required |
max_tokens | integer | none |
stream | boolean | false |
stream_options.include_usage | boolean | false |
tools | array | none |
tool_choice | string or object | auto |
response_format | object | none |
chat_template_kwargs.enable_thinking | boolean | true |
model: a model id fromGET /v1/models.messages: rolessystem,user,assistantandtool. Text content only.max_tokens: output token limit, reasoning tokens included.stream_options.include_usage: adds a final chunk withusageto a stream.tool_choice:auto,none,requiredor a function object.
Details: Streaming, Tool calls, JSON output, Thinking.
Citadel reads model, stream and stream_options and passes the body to the model engine unchanged. The default
engine is vLLM 0.31. temperature, top_p, stop, seed, presence_penalty and frequency_penalty have their
OpenAI meaning. The request body is limited to 32 MB.
Note. Parameters on this page are verified on qwen3.8-27b. Tools, response_format and thinking are not verified
on qwen3.8-flash-next-abliterated, deepseek-v4.1-flash or the llama.cpp model.
Response
200, Content-Type: application/json, an OpenAI chat.completion object.
| Field | Value |
|---|---|
model | Requested model id |
choices[0].message.content | Answer text |
choices[0].message.reasoning | Reasoning text, or null |
choices[0].message.tool_calls | Tool calls, if any |
choices[0].finish_reason | stop, length or tool_calls |
usage.prompt_tokens | Prompt tokens |
usage.completion_tokens | Output tokens, reasoning included |
usage.prompt_tokens_details.cached_tokens | Prompt tokens from the prefix cache |
usage.completion_tokens_details.reasoning_tokens | Reasoning tokens |
system_fingerprint | Engine build |
vLLM adds extension fields with null values (refusal, annotations, token_ids, prompt_logprobs). Ignore the
extension fields.
| Header | Value |
|---|---|
Cache-Control | no-store |
X-Request-Id | Request id |
Errors
Errors use the OpenAI format: {"error": {"message", "type", "code", ...}}. Retry-After is in seconds.
| Status | Code | Cause | Retry |
|---|---|---|---|
| 400 | invalid_json | Body is not valid JSON | no |
| 400 | bad_body | Body could not be read | yes |
| 401 | missing_api_key | No Authorization header | no |
| 401 | invalid_api_key | Unknown or revoked key | no |
| 404 | model_not_found | model is not installed | no |
| 413 | too_large | Body over 32 MB | no |
| 429 | daily_limit | Daily calls used | after 00:00 UTC |
| 502 | engine_restarted | Engine restarted (watchdog) | after model load |
| 502 | llm-unavailable | Engine failed to start | after a few minutes |
| 502 | edge_error | Invalid upstream response | yes |
| 503 | model_loading | Model is loading | after Retry-After |
| 503 | queued | Waiting for the GPU | after Retry-After |
| 503 | gpu-busy | GPU unavailable for 900 s | after Retry-After |
| 503 | insufficient_memory | Not enough RAM for the model | after Retry-After |
| 503 | device_offline | Model server offline | after a few minutes |
| 503 | device_busy | 96 open LLM calls | after Retry-After |
| Code | Extra fields |
|---|---|
invalid_json | reason |
model_not_found | model, available |
daily_limit | limit, used |
engine_restarted | model, reason |
llm-unavailable, gpu-busy | reason |
model_loading | model, state, retry_after_s |
queued | model, position, retry_after_s, reason |
insufficient_memory | model, reason |
type equals code for engine errors. Citadel errors use the types invalid_request_error, rate_limit_exceeded,
service_unavailable and edge_error. Branch on code.
503 queued requires X-LLM-Wait: 0; without the header, Citadel waits.
403 error code: 1010 and 524 come from Cloudflare as plain text:
Cloudflare errors.
GET /v1/models
curl https://citadel.albastov.com/v1/models -H "Authorization: Bearer $CITADEL_API_KEY"{
"object": "list",
"data": [
{
"id": "qwen3.8-27b",
"object": "model",
"created": 1793304723,
"owned_by": "hl1",
"engine": "hyperqwen",
"mode": "batch",
"loaded": true,
"state": "loaded",
"context_length": 150000,
"cold_start_s": 150,
"notes": "HyperQwen batch: 64 slots, ~1,035 tok/s aggregate, ~46 tok/s per stream; prefix caching, tool calls, reasoning"
}
]
}| Field | Value |
|---|---|
id | Value for model |
engine | hyperqwen (vLLM), strata or llama (llama.cpp) |
mode | batch (64 requests) or single (8 requests); hyperqwen only |
state | loaded, starting, stopped or unavailable |
loaded | true if state is loaded |
context_length | Prompt plus output token limit; null if set by the engine at start |
cold_start_s | Estimated load time, seconds |
notes | Engine summary; speed figures are engine claims |
created | Router start time, Unix seconds |
GET /v1/models does not start a model and does not count toward the daily limit. Errors: 401,
503 device_offline, 502 edge_error.