Chat completions
Messages, models, conversation history, OpenAI SDKs, prompt caching, model loading, retries and limits on POST /v1/chat/completions.
A chat completion is a model's reply to a list of messages.
Send messages
messages is the conversation so far, oldest first. Each message has a role and text content.
| Role | Content |
|---|---|
system | Instructions for the whole conversation; first in the list |
user | The caller's turn |
assistant | An earlier reply from the model |
tool | A tool result, with the tool_call_id of the call (Tools and structured output) |
curl
curl https://api.albastov.com/v1/chat/completions \
-H "Authorization: Bearer $ALBASTAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3.8-27b",
"max_tokens": 100,
"messages": [
{"role": "system", "content": "Answer in one line."},
{"role": "user", "content": "Give me three names for a grey cat."}
],
"chat_template_kwargs": {"enable_thinking": false}
}'Python
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.albastov.com/v1",
api_key=os.environ["ALBASTAR_API_KEY"],
)
reply = client.chat.completions.create(
model="qwen3.8-27b",
max_tokens=100,
messages=[
{"role": "system", "content": "Answer in one line."},
{"role": "user", "content": "Give me three names for a grey cat."},
],
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
)
print(reply.choices[0].message.content)JavaScript
const res = await fetch("https://api.albastov.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ALBASTAR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "qwen3.8-27b",
max_tokens: 100,
messages: [
{ role: "system", content: "Answer in one line." },
{ role: "user", content: "Give me three names for a grey cat." },
],
chat_template_kwargs: { enable_thinking: false },
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const reply = await res.json();
console.log(reply.choices[0].message.content);The answer is choices[0].message.content. finish_reason is stop for a complete answer, length when
max_tokens cut the answer, and tool_calls for a tool call. usage counts prompt and output tokens.
Models
model names one chat model. Without model, the request runs on qwen3.8-27b. GET /v1/models lists every model
with its state. Context length, parallel requests, thinking switch and tool support of each model:
Chat models.
Conversation history
The API keeps no conversation state. Send the whole conversation on every request: append the model's reply as an
assistant message, then the next user message.
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.albastov.com/v1",
api_key=os.environ["ALBASTAR_API_KEY"],
)
no_thinking = {"chat_template_kwargs": {"enable_thinking": False}}
history = [
{"role": "user", "content": "Explain prefix caching in two sentences."},
]
reply = client.chat.completions.create(
model="qwen3.8-27b", messages=history, max_tokens=120,
extra_body=no_thinking,
)
print(reply.choices[0].message.content)
history += [
{"role": "assistant", "content": reply.choices[0].message.content},
{"role": "user", "content": "Now in one."},
]
reply = client.chat.completions.create(
model="qwen3.8-27b", messages=history, max_tokens=60,
extra_body=no_thinking,
)
print(reply.choices[0].message.content)A conversation longer than the model's context length fails. Context lengths: Chat models.
OpenAI SDKs
Set base_url to https://api.albastov.com/v1 and api_key to an Albastar API key with the scope llm.
chat_template_kwargs is not an OpenAI parameter: the Python SDK sends the field through extra_body; curl,
fetch and the JavaScript SDK send the field at the top level of the body.
| Feature | Albastar API |
|---|---|
| Chat completions | POST /v1/chat/completions |
| Model list | GET /v1/models |
| Streaming | "stream": true, server-sent events |
| Tool calls | tools, tool_choice |
| JSON output | response_format with json_object or json_schema |
| Thinking | chat_template_kwargs.enable_thinking |
| Embeddings, image input, audio, legacy completions | Not supported |
Python
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.albastov.com/v1",
api_key=os.environ["ALBASTAR_API_KEY"],
)
reply = client.chat.completions.create(
model="qwen3.8-27b",
messages=[{"role": "user", "content": "Name the largest moon of Saturn."}],
max_tokens=50,
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
)
print(reply.choices[0].message.content)JavaScript
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.albastov.com/v1",
apiKey: process.env.ALBASTAR_API_KEY,
});
const reply = await client.chat.completions.create({
model: "qwen3.8-27b",
messages: [{ role: "user", content: "Name the largest moon of Saturn." }],
max_tokens: 50,
chat_template_kwargs: { enable_thinking: false },
});
console.log(reply.choices[0].message.content);Prompt caching
A request whose messages start with the same text as an earlier request reuses the processed prefix, and the first
token arrives sooner. usage.prompt_tokens_details.cached_tokens counts the reused prompt tokens.
- Resend earlier messages unchanged: same order, same text. Append new turns at the end.
- Put the fixed system prompt first, and per-request content (dates, retrieved documents) after the fixed part.
- Send earlier assistant turns with
contentonly, withoutreasoning. - Loading another model clears the cache. The cache is shared by all callers; the oldest entries leave first.
Parallel requests
An account runs up to capabilities.max_parallel requests at once on a model (64 on qwen3.8-27b), counted across
the account's keys. A request above the limit fails with 400 too_many_parallel, or 400 parallel_unsupported on a
model with max_parallel 1; both errors carry max_parallel. A retry succeeds once one of the account's running
requests on that model ends. The example sends eight conversations at once.
import asyncio, os
from openai import AsyncOpenAI
client = AsyncOpenAI(
base_url="https://api.albastov.com/v1",
api_key=os.environ["ALBASTAR_API_KEY"],
)
topics = ["rivers", "volcanoes", "glaciers", "deserts",
"tides", "comets", "auroras", "monsoons"]
async def ask(topic):
reply = await client.chat.completions.create(
model="qwen3.8-27b",
messages=[
{"role": "user", "content": f"In two sentences, what are {topic}?"},
],
max_tokens=120,
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
)
return topic, reply.choices[0].message.content
async def main():
for topic, answer in await asyncio.gather(*(ask(t) for t in topics)):
print(topic, "-", answer)
asyncio.run(main())Model loading
One chat model is loaded at a time across the API. A request for a model that is not loaded loads that model: requests already running on the previous model finish first, up to 120 s, then the requested model starts. Requests for the same model share one load. A loaded model unloads after 30 minutes without requests.
| Request | Result while the model loads |
|---|---|
Streamed, no X-LLM-Wait | Waits up to 1800 s; the stream sends a : loading <model> comment after 10 s, then every 15 s |
Not streamed, no X-LLM-Wait | Waits up to 80 s, then 503 model_loading |
X-LLM-Wait: N | Waits up to N seconds (at most 1800), then 503 model_loading; keep N below 90, because a response with no bytes for 100 s ends with 524, a gateway timeout |
X-LLM-Wait: 0 | 503 model_loading at once; the load continues |
503 model_loading carries Retry-After in seconds. Send the same request again after Retry-After, or poll
GET /v1/models until state is loaded, then send a batch.
While other work holds the model service, a load waits in a queue. A request whose wait ends while the load is queued
gets 503 queued with position, the number of loads ahead. A retry joins the same queued load. A load that waits
900 s in the queue fails, and its requests get 503 gpu-busy.
Retries
Every 503 below carries Retry-After in seconds. Wait Retry-After seconds and send the same request again.
Branch on error.code; error.message is for people.
error.code | Meaning | error.message, for example |
|---|---|---|
model_loading | The model is loading | Loading qwen3.8-27b; retry in 30 s |
queued | The load waits in the queue | Queued (1 ahead); retry in 30 s |
gpu-busy | The load waited 900 s in the queue | The service is busy; retry after Retry-After. |
device_busy | Too many chat requests are open across all callers; Retry-After is 1 | The model service is at capacity; retry in a second |
A 429 daily limit resets at 00:00 UTC; retrying earlier fails again. The OpenAI Python SDK retries a 503 twice by
default: the example sets max_retries=0 and retries in its own loop. Both examples send X-LLM-Wait: 0, so each
503 arrives at once and the loop does the waiting.
Python
import os, time
from openai import OpenAI, APIStatusError
client = OpenAI(
base_url="https://api.albastov.com/v1",
api_key=os.environ["ALBASTAR_API_KEY"],
max_retries=0,
default_headers={"X-LLM-Wait": "0"}, # 503 at once; the loop waits
)
RETRY = {"model_loading", "queued", "gpu-busy", "device_busy"}
def create(deadline_s=900, **kwargs):
start = time.monotonic()
while True:
try:
return client.chat.completions.create(**kwargs)
except APIStatusError as e:
body = e.body if isinstance(e.body, dict) else {}
code = body.get("code")
wait = int(e.response.headers.get("Retry-After", "10"))
late = time.monotonic() - start + wait > deadline_s
if e.status_code != 503 or code not in RETRY or late:
raise
print(f"503 {code}, retrying in {wait} s")
time.sleep(wait)
reply = create(
model="qwen3.8-27b",
max_tokens=60,
messages=[{"role": "user", "content": "One sentence on patience."}],
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
)
print(reply.choices[0].message.content)JavaScript
const RETRY = new Set(["model_loading", "queued", "gpu-busy", "device_busy"]);
async function create(body, deadlineMs = 900_000) {
const start = Date.now();
for (;;) {
const res = await fetch("https://api.albastov.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ALBASTAR_API_KEY}`,
"Content-Type": "application/json",
"X-LLM-Wait": "0", // 503 at once; the loop waits
},
body: JSON.stringify(body),
});
if (res.ok) return res.json();
const err = (await res.json().catch(() => ({}))).error ?? {};
const waitS = Number(res.headers.get("Retry-After") ?? 10);
const late = Date.now() - start + waitS * 1000 > deadlineMs;
if (res.status !== 503 || !RETRY.has(err.code) || late) {
throw new Error(`${res.status} ${err.code ?? ""} ${err.message ?? ""}`);
}
console.log(`503 ${err.code}, retrying in ${waitS} s`);
await new Promise((r) => setTimeout(r, waitS * 1000));
}
}
const reply = await create({
model: "qwen3.8-27b",
max_tokens: 60,
messages: [{ role: "user", content: "One sentence on patience." }],
chat_template_kwargs: { enable_thinking: false },
});
console.log(reply.choices[0].message.content);Limits
| Limit | Value |
|---|---|
| Models loaded at once | 1 |
| Context length | Per model, prompt plus output: context_length in GET /v1/models |
| Parallel requests per account | Per model: capabilities.max_parallel in GET /v1/models |
| Non-streamed response time | 100 s without a response byte, then 524, a gateway timeout; stream longer requests |
| Request body | 32 MB |
| Conversation state | None; send the full conversation on every request |
| Prompt and reply storage | None; token counts only |
| Input and output | Text only |
Calls and tokens per day: Rate limits. Every parameter, header, response field and error: Chat completions reference.