{"version":1,"count":50,"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 `SPASS-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 `SPASS-Tenant-Id` header matches that token's `bound_tenant`.","typical_param":"headers.authorization"},{"code":"forbidden","type":"permission_error","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_error","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":"request_too_large","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":"image_base64_invalid","type":"invalid_request_error","http_status":400,"title":"Inline image base64 payload is not valid base64","description":"rust-api pre-flight checks every `data:image/...;base64,<payload>` URI by attempting a base64 decode. If the payload contains non-base64 characters (e.g. unicode chars instead of ASCII), is truncated mid-symbol, or has invalid padding, this is caught BEFORE the upstream provider would reject it with an opaque error. Spotted via S-series follow-up 2026-05-02 (Cockpit-side encoding bug produced silent 502 cascades).","remediation":"Re-encode the image with a strict base64 encoder (e.g. `base64 -w 0` on Linux, `btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)))` JS-side after reading as ArrayBuffer). Common JS bug: passing a UTF-8 string through `btoa` corrupts non-ASCII bytes — read as Uint8Array first. Verify output is `[A-Za-z0-9+/]+={0,2}` only.","typical_param":"messages[].content[].image_url.url"},{"code":"model_not_vision_capable","type":"invalid_request_error","http_status":400,"title":"This model cannot read images","description":"The resolved model is text-only — it has no vision encoder. The request carried at least one image, either as an `image_url` content-part inside `messages[]` or as an entry in the top-level `attachments` array.\n\nThe gateway checks `modalities.image_in` from the model catalog (see `GET /v1/info`) and rejects up-front. Both image channels behave identically: sending an image to a text-only model is an error, not a silent drop.\n\nBefore 2026-08-22 the `attachments` channel dropped images silently (HTTP 200 plus a `SPASS-Attachments-Dropped` marker header) while content-parts ran through to the engine and failed there. Callers that relied on the silent drop must now pick a vision model or omit the image.","remediation":"Either drop the image, or route the request to a vision-capable model. `GET /v1/info` lists them — check `modalities.image_in` (equivalently `supports_attachment_input`). Local vision lanes today: `qwen3-6-35b-a3b` and `qwen3-8-27b` (both also as `-local` for the force-local tier).","typical_param":"messages[].content[].image_url"},{"code":"tool_call_arguments_invalid","type":"invalid_request_error","http_status":400,"title":"tool_call.function.arguments is not a valid JSON string","description":"OpenAI's tool-calling spec encodes function arguments as a JSON-string (e.g. `\"arguments\":\"{\\\"key\\\":\\\"value\\\"}\"`). rust-api parses each tool_call argument-string with serde_json before forwarding upstream — non-parseable JSON is caught here so the caller gets a clear diagnostic instead of an opaque upstream Pydantic validation error. Cockpit-followup-6 (S.5) Defense-in-Depth Layer 1.","remediation":"Verify your encoder produces a valid JSON-string. Common bug: passing a Python dict directly without `json.dumps`, or shell-escape interactions. Fix on caller-side, then retry.","typical_param":"messages[].tool_calls[].function.arguments"},{"code":"context_window_exceeded","type":"invalid_request_error","http_status":400,"title":"The prompt exceeds this model's maximum context length","description":"The request (system prompt + conversation history + your message + any attachments) is larger than the model's context window. A long chat history or an image carried along in the history is the usual cause.\n\nThe context window per model is in `GET /v1/info` under `context_window`.","remediation":"Start a new conversation, shorten the input, or pick a model with a larger context window. On `/c1` you can also compact the conversation.","typical_param":"messages"},{"code":"upstream_bad_request","type":"invalid_request_error","http_status":400,"title":"Upstream provider rejected the request as invalid","description":"The upstream model/router returned a 4xx (most often 400 BadRequest) — typically caused by malformed payloads that pre-flight validation didn't catch (corrupt base64, schema-validation error, content moderation flag). rust-api propagates the upstream status-code 1:1 instead of emitting an opaque 502, so the caller can distinguish 'client-side-fixable' from 'upstream-outage'.","remediation":"Read the inline upstream message in `error.message` for the specific reason. Common: image payload corrupt → re-encode (see `image_base64_invalid`); response_format empty → omit the field or set `{type: 'text'}`; oversized prompt → truncate.","typical_param":null},{"code":"embedding_input_too_large","type":"invalid_request_error","http_status":400,"title":"Embedding input exceeds the configured cap","description":"`/v1/embeddings` enforces two caps to protect the pooling-runner: at most 256 elements in the `input` array, and at most 8192 bytes (≈ 2k tokens) per element. Both limits match the configured `max-model-len` of the embedding service.","remediation":"Split very large documents into chunks before embedding (typical chunk size: 512–1024 tokens). For large batches, send multiple requests of ≤ 256 inputs each.","typical_param":"input"},{"code":"agent_not_found","type":"not_found_error","http_status":404,"title":"Agent is not configured","description":"The requested agent name is not present in the file-based agent registry (Phase 2, ADR 0011). Agents are loaded once at process start from the configured agents directory. Adding or renaming an agent currently requires a process restart (hot-reload is a backlog item).","remediation":"List available agents via `GET /a1/agents`. To create a new one, drop a YAML file into the agents directory and restart the service.","typical_param":"name"},{"code":"agent_tool_not_registered","type":"invalid_request_error","http_status":400,"title":"Agent references a tool that is not registered","description":"An agent's `tools[]` lists a name that the runtime tool-registry does not contain. Likely causes: typo in agent.yaml, or a tool was renamed/removed since the agent was authored. Config drift — caught at request-time so the operator can fix the YAML.","remediation":"List available tool names via `POST /v1/tools/execute` with an inspection payload, or check `info::available_tools_for(model)` in the source. Edit the agent.yaml and restart.","typical_param":"tools"},{"code":"recursion_depth_exceeded","type":"invalid_request_error","http_status":400,"title":"Agent self-call recursion limit reached","description":"An /a1/agents/<name>/chat request arrived with SPASS-Caller-Depth header at or above the configured maximum (3). The handler refuses to issue a further self-call because the request would form an /a1 → /v1 → /a1 chain. In normal operation no caller should set this header; if you see it, an upstream component (or a buggy proxy) is propagating depths unintentionally.","remediation":"Drop the SPASS-Caller-Depth header from your request, or check whether an intermediary inserted it.","typical_param":null},{"code":"index_not_found","type":"not_found_error","http_status":404,"title":"RAG index does not exist","description":"The requested RAG index id is not present in the tenant-scoped registry. Indices are created on first `POST /a1/rag/indices/<id>/documents` and persisted per-tenant on disk (Cut 2.4.b restart-survives).","remediation":"List indices via `GET /a1/rag/indices`. To create one, ingest documents via `POST /a1/rag/indices/<id>/documents`.","typical_param":"id"},{"code":"document_not_found","type":"not_found_error","http_status":404,"title":"Document does not exist in this RAG index","description":"The index exists for this tenant, but the requested document id is not present. Documents are addressed via `(index_id, doc_id)` and added via POST /a1/rag/indices/<id>/documents (full-replace) or POST .../documents/append (incremental).","remediation":"List documents in the index via `GET /a1/rag/indices/<id>/documents`. The id you used was probably typed wrong or the doc was deleted.","typical_param":"doc_id"},{"code":"session_not_found","type":"not_found_error","http_status":404,"title":"Agent session does not exist","description":"The requested session id is not present in the tenant-scoped session-store. Sessions are created via `POST /a1/agents/<name>/sessions` and persist in `data/sqlite/agent_sessions/<tenant>.sqlite`.","remediation":"List your tenant's sessions via `GET /a1/agents/<name>/sessions` or create a fresh one with `POST /a1/agents/<name>/sessions`.","typical_param":"session_id"},{"code":"session_agent_mismatch","type":"invalid_request_error","http_status":400,"title":"Session belongs to a different agent","description":"Sessions are pinned to the agent_name they were created with. The URL path's `<name>` does not match the session's stored `agent_name`. Switching agents mid-session would invalidate the conversation context.","remediation":"Either address the session under its original agent path, or create a new session under the desired agent.","typical_param":"session_id"},{"code":"session_compact_conflict","type":"conflict_error","http_status":409,"title":"Compaction cannot proceed","description":"Either the session has too few live messages to compact (≤ compact_keep_last_n), or the session is already archived. Compaction always preserves the most recent N messages of the source session and rolls older turns into the summary; with N or fewer turns there is nothing left to summarize. Archived sessions are read-only — chat with the live successor instead (see `GET /a1/agents/<name>/sessions/<sid>/lineage` to find it).","remediation":"Continue the conversation until enough turns accumulate, or lower `compact_keep_last_n` for this session. For archived sessions, follow the `successor_session_id` in the lineage view.","typical_param":"session_id"},{"code":"runtime_not_runnable","type":"invalid_request_error","http_status":400,"title":"Runtime does not support runs","description":"`POST /a1/runs` requires a `runtime` that supports asynchronous repo-bound runs. See `GET /a1/runtimes` — only entries with `runs: true` are accepted (currently `ramteid-agent`).","remediation":"Pick a run-capable runtime from `GET /a1/runtimes`, or use `/a1/agents/<name>/chat` for conversational runtimes.","typical_param":"body.runtime"},{"code":"ramteid_agent_disabled","type":"permission_error","http_status":403,"title":"Ramteid Agent is not enabled on this gateway","description":"The external coding runtime is deployed fail-closed: it only serves when the operator has enabled it on this gateway AND a provider credential is configured. Default is off (ADR 0033 governance).","remediation":"Operator action: set `A1_RAMTEID_AGENT_ENABLED=1` and a provider key in the gateway environment, then restart the chat-gateway service.","typical_param":null},{"code":"ramteid_agent_repo_not_allowed","type":"permission_error","http_status":403,"title":"Repository is not on this tenant's allowlist","description":"Ramteid-Agent runs are restricted to an operator-curated repository allowlist per tenant (`ramteid_agent_repos` in `auth/tokens.yaml`, Group B). An empty or missing list disables the provider for the tenant entirely.","remediation":"Ask the operator to add the canonical repository URL (`https://github.com/<org>/<repo>`, lowercase) to `ramteid_agent_repos` for your tenant.","typical_param":"body.repo"},{"code":"ramteid_agent_run_not_found","type":"not_found_error","http_status":404,"title":"Run not found","description":"No run with this id exists for the calling tenant + user. Runs are strictly scoped: ids of other users or tenants answer 404 (no existence oracle).","remediation":"List your runs via `GET /a1/runs` and use one of the returned ids.","typical_param":"run_id"},{"code":"agent_busy","type":"conflict_error","http_status":409,"title":"A run is already active for this work item","description":"The external coding runtime allows one active run per work item. Starting another run while one is `queued` or `running` conflicts.","remediation":"Wait for the active run to finish (poll `GET /a1/runs/<id>`), or cancel it via `POST /a1/runs/<id>/cancel`, then retry.","typical_param":null},{"code":"tenant_config_key_readonly","type":"invalid_request_error","http_status":400,"title":"Tenant config key is operator-controlled","description":"`PUT /v1/tenant/config` accepts only keys classified as Group C (UX/Operational) per ADR 0013. The requested key is in Group B (security/billing/permissions) — `cost_markup_factor`, `models_allowlist`/`blacklist`, etc. Those settings are operator-curated and live in `tokens.yaml::tenants[].defaults`; runtime changes require a yaml-edit + rust-api restart so the change is reviewable in git.","remediation":"For Group B keys: open a PR against `data/auth/tokens.yaml`, get operator review, restart `dgx-rust-api`. For Group C keys: see the writable_keys list in `GET /v1/tenant/config`.","typical_param":"key"},{"code":"tenant_config_invalid_value","type":"invalid_request_error","http_status":400,"title":"Tenant config value failed validation","description":"The submitted value is out of range, of the wrong type, an unknown enum, or names a model that is not in this tenant's effective allowlist. Hard caps for L3 (DB) writes: image_gen_rate_per_hour ≤ 200, image_max_ttl_hours ≤ 720 (= 30d), compact_keep_last_n ≤ 200. Operator can lift caps higher via `tokens.yaml` (L2) when business justifies — see ADR 0013 § Klassifikation.","remediation":"Check the validation message in the error body for the exact rule. Use `GET /v1/tenant/config` to inspect current effective values + allowed ranges + writable keys.","typical_param":"key"},{"code":"image_not_found","type":"not_found_error","http_status":404,"title":"Image does not exist or has expired","description":"Generated images are stored per-tenant with a default TTL of 12 hours (caller can override up to 7 days via `ttl_hours` on the image_gen tool). Once the TTL elapses, a background-sweeper removes the blob from disk and the metadata-row from SQLite. Returns 404 either because the id is wrong, the image belongs to a different tenant, or the TTL has already lapsed.","remediation":"Use `GET /v1/images` to list this tenant's currently-stored images. To extend an image's lifetime, request a longer TTL on the next image_gen call.","typical_param":"id"},{"code":"memory_bucket_full","type":"conflict_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_error","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":"conflict_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_error","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 `spass-conversation-id` response header). Or list existing conversations via `GET /c1/conversations`.","typical_param":"conversation_id"},{"code":"route_not_found","type":"not_found_error","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":"cloud_consent_required","type":"conflict_error","http_status":409,"title":"Cloud processing requires explicit document consent","description":"GoCreate #4. One or more `kind:\"document\"` attachments would be sent to a cloud model (leaving local infrastructure) without the user having consented to uploading that specific document. The response body carries `dgx_code: \"cloud_consent_required\"` and `required_consents: [{content_hash, doc_name}]`. Only enforced for tenants with `c1_cloud_doc_consent: true` in tokens.yaml.","remediation":"Obtain the user's consent for each listed document, then re-send the request with header `SPASS-Cloud-Consent: <content_hash>[;persist][, ...]` (one entry per consented document; `;persist` stores a permanent grant). Alternatively send the documents to a local model.","typical_param":null},{"code":"content_blocked","type":"invalid_request_error","http_status":422,"title":"Input blocked by content-safety guard","description":"CR-0028. The user input was classified as unsafe by the tenant's content-safety guard and the tenant enforces `content_guard.mode: block`. The body's `param` carries the comma-separated safety categories. Only enforced for tenants with a `content_guard` block in tokens.yaml; audit-mode tenants log instead of blocking.","remediation":"Rephrase the request without the flagged content. If you believe this is a false positive, contact the operator — the tenant policy (`content_guard.policy`) can be tuned, or the tenant can be switched to audit mode.","typical_param":"categories"},{"code":"model_quota_exhausted","type":"rate_limit_error","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_error","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":"upstream_error","type":"api_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":"timeout_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":"overloaded_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":"api_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":"api_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}]}