{"version":1,"count":28,"entries":[{"code":"missing_authorization","type":"authentication_error","http_status":401,"title":"Authorization header is missing","description":"All `/v1/*` and `/c1/*` endpoints require a Bearer token.","remediation":"Add `Authorization: Bearer <RUST_API_BEARER>` to the request.","typical_param":"headers.authorization"},{"code":"invalid_authorization","type":"authentication_error","http_status":401,"title":"Authorization header is invalid","description":"The Bearer token did not match any token in the multi-tenant catalog (`auth/tokens.yaml`). The comparison is constant-time (timing-attack safe). The same code is also returned when an `X-Tenant-Id` header is presented that the matched token is not bound to.","remediation":"Check the bearer matches a `secrets[].value` of some token in `auth/tokens.yaml`, and that any `X-Tenant-Id` header matches that token's `bound_tenant`.","typical_param":"headers.authorization"},{"code":"forbidden","type":"permission_denied","http_status":403,"title":"Token lacks the required scope","description":"The bearer is valid, but the operation needs a scope the token does not have. Permissions in this stack follow `docs/adr/0003-api-key-permissions-model.md`: scope-strings (`resource:action[:qualifier]`) are the source of truth, optional roles seed the default set, and `extra_scopes`/`deny_scopes` adjust per token.","remediation":"Either grant the token the needed scope (edit `auth/tokens.yaml::tokens[].extra_scopes` or assign a richer `role`, then restart the chat-gateway container), or perform the operation with a token that already has it. The server log line on rejection includes the missing scope name.","typical_param":null},{"code":"rate_limit_exceeded","type":"rate_limit_exceeded","http_status":429,"title":"Rate limit exceeded","description":"Per-token rate limits are enforced (default 1 request/sec, 30 burst). The bucket is keyed on the SHA-256 hash of the bearer token, not the IP.","remediation":"Back off and retry. Tune `RATE_LIMIT_PER_SECOND` and `RATE_LIMIT_BURST` env vars on the server if your workload needs more.","typical_param":null},{"code":"body_too_large","type":"invalid_request_error","http_status":413,"title":"Request body exceeds size limit","description":"Bodies are capped at `MAX_BODY_BYTES` (default 32 MB). Triggered most often by very large base64-encoded image inputs.","remediation":"Resize the image, switch to a smaller model, or split the conversation. For very large media, consider `gpt-image` workflow patterns.","typical_param":null},{"code":"invalid_json","type":"invalid_request_error","http_status":400,"title":"Request body is not valid JSON","description":"The body could not be parsed as JSON or did not match the expected schema.","remediation":"Validate your JSON; check the OpenAPI schema at `/openapi.json` for the endpoint.","typical_param":null},{"code":"missing_field","type":"invalid_request_error","http_status":400,"title":"Required field is missing","description":"The endpoint schema requires a field that was absent.","remediation":"See `param` for which field; consult `/openapi.json`.","typical_param":"<varies>"},{"code":"invalid_field","type":"invalid_request_error","http_status":400,"title":"Field value is invalid","description":"A field's value did not match the expected type, range, or enum.","remediation":"See `param` for which field; check `/openapi.json` for valid values.","typical_param":"<varies>"},{"code":"model_not_in_allowlist","type":"invalid_request_error","http_status":400,"title":"Requested model is not allowed","description":"The `model` field references an alias that the server does not whitelist. The allowlist is hard-coded in `rust-api/src/config.rs::ALLOWED_MODELS`.","remediation":"Use one of the models from `/v1/info` or `/v1/models`.","typical_param":"model"},{"code":"max_tokens_below_minimum","type":"invalid_request_error","http_status":400,"title":"max_tokens is below the model's minimum","description":"Some upstream cloud paths reject low values (`max_output_tokens >= 16`); reasoning-capable models need `>= 200` so they have room to think before producing visible output.\n\nThe rust-api auto-floors `max_tokens` to the model's documented minimum *silently* and adds a response header `spass-applied: max_tokens_floored=<N>`. Only when even the floor would still be invalid does this error surface.","remediation":"Set `max_tokens` to at least the value listed under `constraints.min_max_tokens` for the chosen model in `/v1/info`. For reasoning models (gpt-5.5-pro, gemini-3.1-pro, flagship), `200` is a safe default.","typical_param":"max_tokens"},{"code":"image_url_not_supported","type":"invalid_request_error","http_status":400,"title":"This model does not accept image URLs (use base64 data URI)","description":"Cloud-side multimodal providers refuse `image_url.url` values that are `http://` or `https://` URLs. They block on anti-bot, TLS probing, or size limits.\n\nThe rust-api validates against `constraints.accepts_image_url` from the model catalog and rejects up-front rather than letting the upstream reject with an opaque 400.\n\nLocal inference backends behave the same way.","remediation":"Encode your image as a base64 data URI: `data:image/jpeg;base64,/9j/4AAQ...`. Example with `curl`:\n```sh\nB64=$(base64 -w 0 image.jpg)\ncurl ... -d \"{\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":[{\\\"type\\\":\\\"text\\\",\\\"text\\\":\\\"Describe this\\\"},{\\\"type\\\":\\\"image_url\\\",\\\"image_url\\\":{\\\"url\\\":\\\"data:image/jpeg;base64,$B64\\\"}}]}]}\"\n```","typical_param":"messages[].content[].image_url.url"},{"code":"image_decode_error","type":"invalid_request_error","http_status":400,"title":"Could not decode the supplied image data","description":"The base64-encoded data URI could not be parsed, the MIME type was missing, or the decoded bytes were not a valid image.","remediation":"Verify the data URI format `data:image/<jpeg|png|webp|gif>;base64,<data>`. Re-encode with `base64 -w 0` (no line wrapping).","typical_param":"messages[].content[].image_url.url"},{"code":"memory_bucket_full","type":"invalid_request_error","http_status":409,"title":"Memory bucket would exceed its cap","description":"Each `(scope_id, visibility, owner)` bucket has a 16 KB cap (sum of `LENGTH(key)+LENGTH(value)` over all rows). The proposed write would push the total over that cap. The cap is per-bucket, not per-tenant or per-token.","remediation":"Delete or shorten existing entries in the bucket via `DELETE /v1/memory?visibility=...&key=...` or by calling the `memory_forget` tool. `GET /v1/memory/usage?visibility=...` shows current bucket utilisation.","typical_param":"value"},{"code":"memory_confirm_required","type":"invalid_request_error","http_status":400,"title":"Two-step-confirm required for public memory write via tool","description":"Public-memory writes through the LLM tool path require an explicit second tool-call from the model with a confirm token (defense in depth against prompt-injection that flips a private write to public). The first call returns `confirm_required: true` plus a `confirm_token` and a 60 s TTL.","remediation":"Re-invoke `memory_remember` with the same `key`/`value`/`visibility`/`scope` arguments and the returned `confirm_token`. Public writes via the REST API (`POST /v1/memory`) are explicit human action and do NOT need the confirm dance — only the tool path does.","typical_param":"confirm_token"},{"code":"system_prompt_too_large","type":"invalid_request_error","http_status":400,"title":"System-prompt content exceeds the per-level cap","description":"Each system-prompt level (tenant / scope / user) has an 8 KB content cap by default. Override via env `MAX_SYSTEM_PROMPT_BYTES_PER_LEVEL`. Walkthrough decision 5.6-B.","remediation":"Shorten the content; the limit is per-level, three levels stack to ≈ 24 KB. Existing versions that were written before a cap-tightening remain valid (no retroactive break).","typical_param":"content"},{"code":"system_prompt_version_not_found","type":"not_found","http_status":404,"title":"System-prompt version does not exist","description":"The addressed `(level, scope_id?, user_id?, version)` tuple is unknown.","remediation":"List existing versions via `GET /v1/system-prompts/{level}/versions`.","typical_param":"version"},{"code":"system_prompt_current_locked","type":"invalid_request_error","http_status":409,"title":"Refusing to delete the currently active system-prompt version","description":"Hard-delete (`DELETE /v1/system-prompts/{level}/v/{n}`) refuses to remove the version that has `is_current=1`. This protects against accidental loss of the active stack content.","remediation":"First rollback to a different version via `PUT /v1/system-prompts/{level}/current/{n}`, or unset the current version entirely via `DELETE /v1/system-prompts/{level}` (soft-delete). Then the version can be hard-deleted.","typical_param":"version"},{"code":"conversation_not_found","type":"not_found","http_status":404,"title":"Conversation does not exist","description":"Used by `/c1/*`. The supplied `conversation_id` was never persisted (or was deleted, or belongs to a different `user_id`).","remediation":"Omit `conversation_id` to start a fresh conversation (the server returns the new id in the `x-conversation-id` response header). Or list existing conversations via `GET /c1/conversations`.","typical_param":"conversation_id"},{"code":"route_not_found","type":"not_found","http_status":404,"title":"Route not found","description":"Path/method combination does not exist on this server.","remediation":"Check `/openapi.json` for the list of routes.","typical_param":null},{"code":"model_quota_exhausted","type":"rate_limit_exceeded","http_status":429,"title":"Model temporarily out of quota","description":"The model is temporarily unavailable for this tenant due to a token / cost / rate budget that has been used up. Identical shape to other rate-limit responses — caller can show a generic 'try a different model or try again later' message. Per ADR 0006 v2 the body intentionally does not name the upstream provider or surface raw token counts.","remediation":"Pick a different model alias (Claude / Gemini / a local model), or wait until the budget window resets (typically the start of the next UTC day for daily budgets). Operators can raise the per-tenant budget in `tokens.yaml` or with the upstream provider directly.","typical_param":null},{"code":"openrouter_daily_quota_exhausted","type":"rate_limit_exceeded","http_status":429,"title":"openrouter daily quota exhausted","description":"Cut 2.32 / CR-0005. A specific openrouter-backed alias hit its daily limit (distinct from cross-vendor `model_quota_exhausted`). Body includes `available_fallbacks: [...]` (tenant-allowed alternative aliases) plus `Retry-After`-Header (seconds until reset, typically next 00:00 UTC).","remediation":"Switch to one of the `available_fallbacks` aliases (e.g. claude/gemini variants of the same family), or wait until `Retry-After` seconds have passed.","typical_param":"model"},{"code":"tool_loop_max_iterations","type":"invalid_request_error","http_status":200,"title":"Tool-loop reached MAX_TOOL_ITERATIONS","description":"Cut 2.32 / CR-0001. Server-side tool-loop ran MAX_TOOL_ITERATIONS (=10) iterations without the model reaching a non-tool-calling final answer. Stack auto-emitted an extra synthesis-LLM-call with `tools: []` to produce a narrative response from the collected tool-results. Caller may show a UI hint that the answer might be incomplete.","remediation":"Inspect the response content (it contains the synthesised narrative). If the answer is not satisfying, the user should re-phrase or split the question into smaller turns. In stream mode the `spass.tool-cap` SSE event signals the same.","typical_param":null},{"code":"tool_loop_anti_loop_synthesised","type":"invalid_request_error","http_status":200,"title":"Anti-loop guard fired, synthesis emitted","description":"Cut 2.32 / CR-0001. Model emitted the same tool-call signature as in a previous iteration of the same conversation turn (Llama tendency to re-explore). Stack auto-emitted an extra synthesis-LLM-call (analog `tool_loop_max_iterations`).","remediation":"Same as `tool_loop_max_iterations`. SSE event in stream mode: `spass.tool-anti-loop`.","typical_param":null},{"code":"upstream_error","type":"upstream_error","http_status":502,"title":"Upstream provider returned an error","description":"An upstream provider (gateway, local inference, or cloud) answered with a non-2xx response. The server includes the upstream message inline in `message` for debugging.","remediation":"Read the inline upstream message. Common causes: model rejected an oversize prompt, content moderation flag, provider-side outage. Retry with adjusted input or wait.","typical_param":null},{"code":"upstream_timeout","type":"upstream_error","http_status":504,"title":"Upstream provider timed out","description":"Reqwest hit the configured `HTTP_TOTAL_TIMEOUT_SECS` (default 600). Most likely cause is a slow image-generation model — `gpt-image` regularly takes 100-180 s.","remediation":"Increase your client timeout; check `constraints.typical_response_seconds` per model in `/v1/info`. For `gpt-image`, set client timeout ≥ 240 s.","typical_param":null},{"code":"upstream_unavailable","type":"upstream_error","http_status":503,"title":"Upstream provider is unavailable","description":"Could not establish a TCP/TLS connection to the routing gateway or local inference backend.","remediation":"Check `/readyz` to see which backend is unreachable, then `docker ps`/`docker logs` on the failing service.","typical_param":null},{"code":"internal_error","type":"internal_error","http_status":500,"title":"An internal error occurred","description":"Server-side bug, panic, or unexpected condition. The server logs the full detail with `tracing::error!`; the response intentionally omits internals.","remediation":"Retry. If reproducible, capture request id from `x-request-id` and check server logs.","typical_param":null},{"code":"storage_error","type":"internal_error","http_status":500,"title":"Conversation storage error","description":"SQLite read/write failed for `/c1/*` endpoints. Most common cause: the volume mount `data/sqlite/` is owned by the wrong UID (rust-api runs as 65532).","remediation":"On the server: `sudo chown -R 65532:65532 /home/dietmar/dgx-llm/data/sqlite && docker compose restart rust-api`.","typical_param":null}]}