API: Chat Completions
Kasimir's OpenAI-compatible chat completions endpoint with bearer API keys, streaming, model governance, and usage metering.
The Kasimir Public API exposes an OpenAI-compatible surface at https://kasimir.ai/api/v1. The endpoint POST /api/v1/chat/completions accepts the same request body as the OpenAI Chat Completions API, proxies it transparently to the backing LLM infrastructure (a LiteLLM proxy), and layers Kasimir-specific governance on top: company API-key authentication, a per-model EU-hosting gate, monthly-budget enforcement, and token-accurate usage metering. Because the wire format and error schema match OpenAI, existing OpenAI SDKs work unchanged — just point base_url and api_key at Kasimir.
Endpoint
Method |
|
URL |
|
Auth |
|
Required scope |
|
Content-Type |
|
Response |
|
Authentication
Every call is authenticated with a company API key passed in the Authorization: Bearer <secret> header. Keys use the format kasi_<32-chars> and are created in the Kasimir UI under Administration → Access by workspace owners or admins.
The plaintext key is shown exactly once at creation time. Kasimir stores only a SHA-256 hash; a lost key cannot be recovered, only revoked and re-created.
Each key carries scopes. Newly created keys default to chat:read and chat:write. Chat completions require chat:write — without it the API returns 403.
# Fails with 401 if the header is missing or the key is invalid/revoked.
Authorization: Bearer kasi_abcdef0123456789abcdef0123456789Each successful call updates the key's last_used_at (non-blocking), so unused keys are easy to spot in the admin UI.
Request body
The body matches the OpenAI schema exactly. Required fields are validated; every other field is passed through verbatim to the upstream infrastructure (full parity: temperature, top_p, tools, tool_choice, response_format, stop, seed, max_tokens, …).
Field | Type | Required | Description |
|---|---|---|---|
|
| yes | Kasimir model slug (see "Models"). Not the OpenAI model name. |
|
| yes | At least 1 message in OpenAI chat format ( |
|
| no |
|
| various | no | Passed through 1:1. |
In streaming mode Kasimir automatically sets stream_options: { include_usage: true } server-side so a usage chunk is emitted at the end of the stream and metering is recorded correctly — you don't need to send it yourself.
Models
model expects a Kasimir slug, not an upstream model name. Slugs are subject to the same governance as the in-app model picker:
The model must be active for your company and of type
chat.Models disabled at the workspace level (
disabled_model_slugs) are not callable →404.Non-EU-hosted models are only available if your organization has
allow_non_eu_modelsenabled → otherwise403.
List the slugs valid for your key via GET /api/v1/models (see below). The response always returns the Kasimir slug in the model field, never the internal upstream ID.
Response (synchronous)
With stream: false (or omitted), the response is a complete OpenAI chat-completion object. The model field is normalized to the Kasimir slug.
{
"id": "chatcmpl-…",
"object": "chat.completion",
"created": 1750000000,
"model": "self-hosted-llama-3.3-70b",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Hi! How can I help?" },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 18, "completion_tokens": 9, "total_tokens": 27 }
}Response (streaming)
With stream: true the endpoint responds with Content-Type: text/event-stream and transparently re-emits the upstream SSE stream — identical to OpenAI's format (data: {…} lines, terminated by data: [DONE]). Kasimir only rewrites each chunk's model field to the slug and reads the final usage chunk for metering.
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","model":"self-hosted-llama-3.3-70b","choices":[{"index":0,"delta":{"role":"assistant","content":"Hi"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","model":"self-hosted-llama-3.3-70b","choices":[{"index":0,"delta":{"content":"!"},"finish_reason":null}]}
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":18,"completion_tokens":9,"total_tokens":27}}
data: [DONE]Budget & usage metering
Every call is checked against the company's monthly budget before it is proxied. If a monthly_budget_eur is set and the aggregated spend for the current calendar month (API and in-app chat combined) reaches/exceeds it, the API returns 402. A budget of null or ≤ 0 means unlimited.
After each call an entry is written to the usage ledger: model slug, endpoint, prompt/completion tokens, and the computed EUR cost. For streaming this happens once the stream ends (even on early abort, the tokens counted up to that point are recorded). Metering is best-effort and never aborts a response.
Cost = (prompt_tokens / 1000) × price_input + (completion_tokens / 1000) × price_output, rounded to 6 decimals, in EUR. Prices come from the model configuration.
Errors
Errors are returned in the OpenAI error schema so off-the-shelf SDKs surface them normally:
{ "error": { "message": "Missing scope: chat:write", "type": "permission_error", "code": null } }HTTP status |
| Meaning |
|---|---|---|
|
| Invalid body (missing |
|
| Bearer token missing, key unknown or revoked. |
|
| Company monthly budget reached. |
|
| Missing |
|
| Model slug unknown, inactive, or workspace-disabled. |
|
| Upstream / infrastructure error. |
If an error occurs after a stream has already started, it can no longer be reshaped into the JSON error schema — the connection simply drops. Handle stream interruptions on the client side.
Examples
curl — synchronous
curl https://kasimir.ai/api/v1/chat/completions \
-H "Authorization: Bearer $KASIMIR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "self-hosted-llama-3.3-70b",
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "Write a one-liner about data privacy." }
],
"temperature": 0.7
}'curl — streaming
curl -N https://kasimir.ai/api/v1/chat/completions \
-H "Authorization: Bearer $KASIMIR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "self-hosted-llama-3.3-70b",
"messages": [{ "role": "user", "content": "Count from 1 to 5." }],
"stream": true
}'Python — OpenAI SDK (synchronous)
Since Kasimir is OpenAI-compatible, you only need to set base_url and api_key.
from openai import OpenAI
client = OpenAI(
base_url="https://kasimir.ai/api/v1",
api_key="kasi_…", # your Kasimir API key
)
resp = client.chat.completions.create(
model="self-hosted-llama-3.3-70b",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "List three benefits of EU hosting."},
],
temperature=0.7,
)
print(resp.choices[0].message.content)
print(resp.usage) # prompt_tokens / completion_tokens / total_tokensPython — OpenAI SDK (streaming)
from openai import OpenAI
client = OpenAI(base_url="https://kasimir.ai/api/v1", api_key="kasi_…")
stream = client.chat.completions.create(
model="self-hosted-llama-3.3-70b",
messages=[{"role": "user", "content": "Explain RAG in two sentences."}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content if chunk.choices else None
if delta:
print(delta, end="", flush=True)
print()Error handling (Python)
from openai import OpenAI, APIStatusError
client = OpenAI(base_url="https://kasimir.ai/api/v1", api_key="kasi_…")
try:
client.chat.completions.create(
model="self-hosted-llama-3.3-70b",
messages=[{"role": "user", "content": "Hi"}],
)
except APIStatusError as e:
if e.status_code == 402:
print("Monthly budget reached — please raise the budget.")
elif e.status_code in (401, 403):
print("Auth/scope problem:", e.message)
else:
raiseRelated endpoints
The same authentication, governance, and metering apply to:
Endpoint | Purpose |
|---|---|
| OpenAI-compatible list of the chat and embedding models allowed for your company ( |
| OpenAI-compatible embeddings ( |
| Runs a configured Kasimir assistant via API. |
curl https://kasimir.ai/api/v1/models \
-H "Authorization: Bearer $KASIMIR_API_KEY"