Kasimir Docs DEEN To the app →

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/v1

All 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_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Keys 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

chat:write

POST /chat/completions, POST /embeddings, POST /assistants/{id}/run

(none)

GET /models — any valid, non-revoked key suffices

A missing required scope returns 403. An invalid or revoked key returns 401.

Metering & Governance Proxy One bearer key in. EU-gated model routing out. Script / SDK client = Kasimir( api_key= kasi_a9f3…7b2 ) Authorization: Bearer Metering + Governance Kasimir Proxy Key isolation EU gate Model namespace Usage ledger Budget LiteLLM proxy routing · fanout EU models non-EU gated opt-in Non-EU branch is locked behind a per-tenant sovereignty gate — disabled by default.
Kasimir sits as a metering and governance proxy between your code and the models — you only hold a kasi_ key.

Endpoints at a glance

Method

Path

Purpose

Scope

GET

/models

List the company's allowed chat and embedding models

any valid

POST

/chat/completions

Chat completion (streaming + non-streaming)

chat:write

POST

/embeddings

Generate embedding vectors

chat:write

POST

/assistants/{id}/run

Run a stored assistant as a one-shot function

chat:write

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

model

string

yes

Model slug from GET /models

messages

array

yes

At least 1 message in OpenAI format (role + content)

stream

boolean

no

true for Server-Sent-Events streaming (default: false)

temperature

number

no

Passed through

max_tokens

number

no

Passed through

tools, response_format, …

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

model

string

yes

Embedding model slug (purpose='embedding')

input

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

messages

array

yes

1–50 messages with role (system/user/assistant) + content (string)

model_slug

string

no

Overrides the assistant's default model

temperature

number (0–2)

no

Overrides the assistant setting

max_tokens

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

type

Meaning

400

invalid_request_error

Invalid request body (e.g. missing model/messages)

401

authentication_error

Missing, invalid, or revoked key

402

insufficient_quota

Company monthly budget reached

403

permission_error

Missing scope or non-EU model blocked by the EU gate

404

not_found_error

Model/assistant not found or disabled for the company

502

api_error

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_output

For 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.