Kasimir Docs DEEN To the app →

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

POST

URL

https://kasimir.ai/api/v1/chat/completions

Auth

Authorization: Bearer kasi_…

Required scope

chat:write

Content-Type

application/json

Response

application/json (sync) or text/event-stream (streaming)

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_abcdef0123456789abcdef0123456789
ℹ️

Each 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

model

string

yes

Kasimir model slug (see "Models"). Not the OpenAI model name.

messages

array

yes

At least 1 message in OpenAI chat format ({ role, content, … }).

stream

boolean

no

true → Server-Sent-Events stream. Defaults to false.

temperature, top_p, max_tokens, stop, tools, tool_choice, response_format, seed, …

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_models enabled → otherwise 403.

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]
Chat Completions request flow POST /api/v1/chat/completions · governance gateway Client curl / OpenAI SDK POST /chat/ completions Bearer kasi_… request Kasimir gateway governance gates 1 Authenticate API key verify scope chat:write 2 Resolve model slug EU sovereignty gate 3 Assert monthly budget spend cap not exceeded 4 Proxy to LiteLLM forward upstream call proxy LiteLLM proxy → upstream LLM usage metered (tokens, EUR) Usage ledger writes tokens · cost (EUR) response JSON (sync) { "choices": [ … ], "usage": {…} } SSE stream stream: true data: { "delta": "…" } data: [DONE] Every request passes all four gates before a token leaves the gateway; usage is metered on the return path.
Request flow: auth and governance gates before the proxy, with usage metering after the response.

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

type

Meaning

400

invalid_request_error

Invalid body (missing model/messages, empty messages array).

401

authentication_error

Bearer token missing, key unknown or revoked.

402

insufficient_quota

Company monthly budget reached.

403

permission_error

Missing chat:write scope or non-EU model not enabled.

404

not_found_error

Model slug unknown, inactive, or workspace-disabled.

5xx

api_error

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_tokens

Python — 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:
        raise

The same authentication, governance, and metering apply to:

Endpoint

Purpose

GET /api/v1/models

OpenAI-compatible list of the chat and embedding models allowed for your company ({ object: "list", data: [{ id, object, created, owned_by }] }). No scope required (just a valid key).

POST /api/v1/embeddings

OpenAI-compatible embeddings (model + input). Requires the chat:write scope.

POST /api/v1/assistants/{id}/run

Runs a configured Kasimir assistant via API.

curl https://kasimir.ai/api/v1/models \
  -H "Authorization: Bearer $KASIMIR_API_KEY"