API: Errors, Status Codes & Limits
Complete reference of HTTP status codes, the OpenAI-compatible error shape, and the budget and size limits of the public Kasimir API.
The public Kasimir API is OpenAI-compatible: errors are returned in the same JSON shape as OpenAI, so off-the-shelf OpenAI SDKs surface them as a normal exception or error object. This chapter describes exactly which status codes the /api/v1/* endpoints throw, what they mean, how the error object is structured, and which hard limits (budget, payload sizes) apply — all derived directly from the source code.
Error shape
Every error from the OpenAI-compatible endpoints (POST /v1/chat/completions, POST /v1/embeddings, GET /v1/models) is translated into the OpenAI schema by server/utils/apiError.ts (toOpenaiError):
{
"error": {
"message": "Model \"foo\" not found",
"type": "not_found_error",
"code": null
}
}Field | Content |
|---|---|
| Plain text from the thrown |
| Derived from the status code (see table below). |
| Always |
The type value is mapped strictly from the status code:
Status |
|
|---|---|
|
|
|
|
|
|
|
|
|
|
anything else (e.g. |
|
The HTTP status code and the body's type are always consistent — toOpenaiError sets both from the same source (setResponseStatus + mapping).
Streaming edge case: if an error occurs after the SSE stream has started (started = true in chat/completions.post.ts), it can no longer be reshaped into an OpenAI error object. The stream simply aborts. Treat an unexpectedly truncated stream as an error on the client.
Status codes in detail
The following table lists every status code thrown by the /api/v1/* surface, with the triggering code location and its meaning.
Status |
| When | Source |
|---|---|---|---|
|
| Body violates the Zod schema ( |
|
|
| No |
|
|
| Company monthly budget reached/exceeded |
|
|
| Key lacks the required scope (e.g. |
|
|
| Model slug unknown/inactive/wrong purpose; model blocked via |
|
|
| Key management only: name already taken ( |
|
|
| Upstream (LiteLLM) returns no success and no own status — fallback ` | |
|
| Upstream error with its own status ( |
|
|
| Any unclassified error (default in |
|
402 does not mean "too expensive for this request", it means "company monthly budget used up". Spend sums both API and in-app chat usage for the current calendar month (RPC company_month_spend_eur).
401 – Authentication
authenticateApiKey (server/utils/apiKeyAuth.ts) throws 401 in three cases:
No bearer token —
message: "Missing bearer token"Invalid key — hash not found in
company_api_keys—message: "Invalid API key"Revoked key —
revoked_atis set —message: "API key revoked"
Keys have the format kasi_<32 chars> and are stored only as a SHA-256 hash; the plaintext is shown once at creation.
402 – Budget
assertCompanyBudget (server/utils/apiUsage.ts) checks before forwarding to the upstream:
companies.monthly_budget_eurisnullor≤ 0→ unlimited, no cap.Otherwise the monthly spend is fetched via
company_month_spend_eur. Ifused ≥ budget, a402is returned, e.g.Monthly budget of €100.00 reached (used €100.42).
Fail-open on infrastructure errors: if the budget lookup itself fails (DB glitch), the request is let through rather than blocked — only a genuine 402 (budget exceeded) is propagated. A flaky lookup therefore never locks a customer out.
403 – Permission
Two independent causes:
Missing scope —
authenticateApiKey(event, ['chat:write'])throwsMissing scope: chat:writeif the key lacks the required scope.GET /v1/modelsrequires no special scope;chat/completionsandembeddingsrequirechat:write.Non-EU gate —
resolveApiModelthrows403when the model haseu_hosted = falseandcompanies.allow_non_eu_modelsis nottrue:Model "…" is not enabled for your organization (non-EU model).
404 – Not found
resolveApiModel (chat/embeddings) and the assistant handler throw 404 when:
the slug does not exist in
models, is inactive, or has a different purpose (chatvsembedding),the slug is listed in
companies.disabled_model_slugs(deliberately masked as "not found"),an assistant (
/v1/assistants/{id}/run) does not exist or belongs to a foreign company (tenant isolation),the assistant's model does not exist/is inactive —
Model "…" not found or inactive.
502 / passed-through 5xx – Upstream
If the LiteLLM layer does not respond with 2xx (or the body is missing for streaming), Kasimir throws res.status || 502 with Upstream error: <first 200 chars of the upstream response>. A 502 typically means the upstream returned no usable status at all.
Limits
In Phase 1 there are no rate limits and no 429 responses — per-key spend caps and per-key rate limits are explicitly deferred to Phase 2. The only enforced "quantity" limit is the company monthly budget (see 402). Beyond that, payload schema bounds apply:
Limit | Value | Endpoint | Source |
|---|---|---|---|
Monthly budget (company) |
| chat, embeddings |
|
| at least 1 message |
| Zod |
Assistant | 1 to 50 messages |
| Zod |
Assistant | 0 to 2 |
| Zod |
Assistant | 1 to 32000 (integer) |
| Zod |
Rate limit / | none (Phase 2) | – | Spec §Non-Goals |
For chat/completions and embeddings, temperature, max_tokens, tools, response_format etc. are not validated by Kasimir but passed verbatim to the upstream (.passthrough()). Invalid values therefore surface as a passed-through upstream error (e.g. 400/5xx), not a Kasimir 400.
Handling errors (recommendations)
Status | Action |
|---|---|
| Fix the request body; do not retry (deterministic). |
| Check/rotate the key; recreate revoked keys in the app. |
| Raise the budget in Administration → … or wait for the month rollover; retrying is pointless. |
| Check the key's scope or clarify the EU gate ( |
| Reconcile the model slug against |
| Does not occur currently — no handling needed (arrives only in Phase 2). |
| Transient upstream error — retry with backoff. |
Because the error shape is OpenAI-conformant, every OpenAI SDK catches these cases automatically — e.g. the Python SDK raises AuthenticationError (401), PermissionDeniedError (403), NotFoundError (404) and APIStatusError (5xx). Read error.message for the specific reason.