API Overview
Kasimir's OpenAI-compatible, EU-sovereign LLM API: authentication, endpoints, models, streaming, errors and metering.
The Kasimir Public API exposes governed, EU-sovereign LLM access programmatically. It is drop-in compatible with the OpenAI API — your existing tools and SDKs work by changing only the base_url and the API key. Kasimir acts as a thin metering and governance proxy in front of the internal LiteLLM proxy: identical wire format, plus per-company key isolation, a branded model namespace, the EU/non-EU gate, usage accounting, and company-budget enforcement.
Base URL
https://kasimir.ai/api/v1All endpoints are relative to this base URL. It is exactly what you set as base_url in an OpenAI SDK.
Authentication
Every request carries a company API key in the Authorization header as a bearer token:
Authorization: Bearer kasi_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxKeys use the prefix kasi_ followed by 32 characters (base32 alphabet, ~160 bits of entropy). They are created in /administration/access, shown in plaintext only once at creation, and stored server-side solely as a SHA-256 digest. For identification the UI permanently shows only the prefix (first 8 characters).
Treat the key like a password. It is never shown in plaintext again after creation. If lost, revoke it and create a new one. The internal LiteLLM master key never leaves the server — you only ever hold kasi_ keys.
Scopes
Keys carry scopes, enforced server-side:
Scope | Required for |
|---|---|
|
|
(none) |
|
A missing required scope returns 403. An invalid or revoked key returns 401.
Endpoints at a glance
Method | Path | Purpose | Scope |
|---|---|---|---|
|
| List the company's allowed chat and embedding models | any valid |
|
| Chat completion (streaming + non-streaming) |
|
|
| Generate embedding vectors |
|
|
| Run a stored assistant as a one-shot function |
|
The first three are OpenAI-shaped. assistants/{id}/run is a Kasimir-specific convenience endpoint (see below).
GET /models
Returns the models allowed for your company, OpenAI-shaped. The list already applies your company filters: disabled models (disabled_model_slugs) are hidden, and non-EU models appear only if your organization has enabled allow_non_eu_models.
curl https://kasimir.ai/api/v1/models \
-H "Authorization: Bearer kasi_..."Response:
{
"object": "list",
"data": [
{
"id": "self-hosted-llama-3.3-70b",
"object": "model",
"created": 1745971200,
"owned_by": "kasimir"
}
]
}The id field is the model slug — this exact value goes into the model field of completion and embedding calls. Internal model_id values and upstream aliases are never exposed.
Model slugs are company- and configuration-specific. Don't rely on hard-coded names — query GET /models to get the slugs valid for your key.
POST /chat/completions
Accepts an OpenAI chat body. Required fields are model and messages; all other fields (temperature, max_tokens, tools, response_format, …) are passed through transparently, so function calling, structured outputs, finish_reason, and multimodal content work as the upstream supports them.
Parameters
Field | Type | Required | Description |
|---|---|---|---|
| string | yes | Model slug from |
| array | yes | At least 1 message in OpenAI format ( |
| boolean | no |
|
| number | no | Passed through |
| number | no | Passed through |
| – | no | Passed through (LiteLLM parity) |
Non-streaming
curl https://kasimir.ai/api/v1/chat/completions \
-H "Authorization: Bearer kasi_..." \
-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": "Explain GDPR in one sentence."}
]
}'Response (OpenAI-shaped; the model field carries the slug back):
{
"id": "chatcmpl-...",
"object": "chat.completion",
"created": 1745971200,
"model": "self-hosted-llama-3.3-70b",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "GDPR is..." },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 24, "completion_tokens": 18, "total_tokens": 42 }
}Streaming
With "stream": true the API returns a text/event-stream. The upstream SSE bytes are passed through transparently; Kasimir only rewrites the model field to the slug and taps the final usage chunk for metering. The end is the usual data: [DONE] line.
curl -N https://kasimir.ai/api/v1/chat/completions \
-H "Authorization: Bearer kasi_..." \
-H "Content-Type: application/json" \
-d '{
"model": "self-hosted-llama-3.3-70b",
"messages": [{"role": "user", "content": "Count to three."}],
"stream": true
}'data: {"id":"chatcmpl-...","object":"chat.completion.chunk","model":"self-hosted-llama-3.3-70b","choices":[{"index":0,"delta":{"content":"One"},"finish_reason":null}]}
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":12,"completion_tokens":5,"total_tokens":17}}
data: [DONE]POST /embeddings
OpenAI-shaped embeddings. Required: model (an embedding slug) and input (string or string array).
Field | Type | Required | Description |
|---|---|---|---|
| string | yes | Embedding model slug ( |
| string | string[] | yes | Text(s) to embed |
curl https://kasimir.ai/api/v1/embeddings \
-H "Authorization: Bearer kasi_..." \
-H "Content-Type: application/json" \
-d '{
"model": "self-hosted-bge-m3",
"input": "Sample text to embed"
}'{
"object": "list",
"data": [
{ "object": "embedding", "index": 0, "embedding": [0.0123, -0.045, "..."] }
],
"model": "self-hosted-bge-m3",
"usage": { "prompt_tokens": 6, "total_tokens": 6 }
}The default embedding model's vector dimension is fixed at 1024 (BAAI/bge-m3).
POST /assistants/{id}/run
A convenience endpoint (not an OpenAI standard) that runs an assistant stored in Kasimir as a one-shot function. The assistant's system prompt is prepended automatically (plus the current date); no SSE streaming, no conversation persistence, no tool-calling round-trip.
Field | Type | Required | Description |
|---|---|---|---|
| array | yes | 1–50 messages with |
| string | no | Overrides the assistant's default model |
| number (0–2) | no | Overrides the assistant setting |
| integer (1–32000) | no | Overrides the assistant setting |
curl https://kasimir.ai/api/v1/assistants/<assistant-id>/run \
-H "Authorization: Bearer kasi_..." \
-H "Content-Type: application/json" \
-d '{ "messages": [{"role": "user", "content": "Summarize the quarterly report."}] }'{
"text": "The quarterly report shows...",
"model": "self-hosted-llama-3.3-70b",
"usage": { "prompt_tokens": 312, "completion_tokens": 88 }
}If neither model_slug nor a default model on the assistant is set, the endpoint returns 400. An unknown or cross-tenant assistant returns 404.
Using the OpenAI Python SDK
Because the endpoints are OpenAI-shaped, you only set base_url and api_key:
from openai import OpenAI
client = OpenAI(
base_url="https://kasimir.ai/api/v1",
api_key="kasi_...",
)
# List models
for m in client.models.list().data:
print(m.id)
# Chat completion
resp = client.chat.completions.create(
model="self-hosted-llama-3.3-70b",
messages=[{"role": "user", "content": "Explain GDPR in one sentence."}],
)
print(resp.choices[0].message.content)
# Streaming
stream = client.chat.completions.create(
model="self-hosted-llama-3.3-70b",
messages=[{"role": "user", "content": "Count to three."}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)
# Embeddings
emb = client.embeddings.create(
model="self-hosted-bge-m3",
input="Sample text to embed",
)
print(len(emb.data[0].embedding))Errors
Errors are OpenAI-shaped, with the correct HTTP status code, so standard SDKs surface them normally:
{ "error": { "message": "Model \"foo\" not found", "type": "not_found_error", "code": null } }HTTP |
| Meaning |
|---|---|---|
|
| Invalid request body (e.g. missing |
|
| Missing, invalid, or revoked key |
|
| Company monthly budget reached |
|
| Missing scope or non-EU model blocked by the EU gate |
|
| Model/assistant not found or disabled for the company |
|
| Upstream error (LiteLLM/model) |
EU governance & sovereignty
Every API call passes server-side through the same EU/non-EU gate as the in-app model picker. Models with eu_hosted=false are reachable only if the organization has explicitly enabled allow_non_eu_models — otherwise 403. The branded model namespace (slug ↔ internal model_id) and the LiteLLM master key stay entirely server-side. The API therefore carries the same GDPR guarantee as the application.
Metering & budget
Each successful completion or embedding call is recorded in the api_usage ledger (model slug, endpoint, prompt/completion tokens, cost in EUR). Cost follows the standard formula:
cost = (prompt_tokens / 1000) * cost_eur_per_1k_input
+ (completion_tokens / 1000) * cost_eur_per_1k_outputFor streaming, usage comes from the tapped final usage chunk (stream_options.include_usage is set automatically). Metering is best-effort — a recording failure never aborts the already-sent response.
Budget enforcement: before each forward the API checks the company monthly budget (companies.monthly_budget_eur). Monthly spend includes API and in-app chat cost for the current calendar month. When the budget is reached, the API returns 402. A budget of null/0 means unlimited. Owners/admins find a usage summary (requests, tokens, EUR this month) under /administration/access.
Set a monthly budget in Administration to enforce cost control — without a budget, API calls run unlimited.