Eunice REST API Reference
REST endpoints across every router, the streaming event format, and request and response schemas.
Developer reference for every HTTP endpoint mounted by the FastAPI process. All routers are wired in app.py:497-515. Every endpoint that talks to the database or the agent runs through one authentication middleware and one tenant resolver.
| Concept | Value |
|---|---|
| Base URL (dev) | http://localhost:8001/api/v1 |
| Auth | JWT bearer (Authorization: Bearer <token>); see Auth middleware |
| Tenant scope | X-Tenant-ID request header; falls back to settings.default_tenant_id |
| Content type | application/json unless explicitly documented as multipart, SSE, or binary |
| Time format | ISO 8601 UTC strings |
| Cost fields | cost_usd, total_cost_usd, cost, floating-point USD; 0.0 means no LLM call |
This page covers all routers. For chat-loop internals see Engine; for the data registry Fabric; for capability mapping Auth & RBAC.
Auth middleware
app.py:320-476 runs in front of every router. Behavior:
- Three path sets bypass auth entirely:
/api/v1/auth/{register,login,refresh}(public auth),/api/v1/{health,ready,version}(operational probes), and the OpenAPI docs (/docs,/openapi.json,/redoc). - Otherwise the middleware decodes the JWT, validates
type == "access", and resolves the active tenant in this precedence:X-Tenant-IDheader → JWTtenant_idclaim → first membership →settings.default_tenant_id. A header pointing at a tenant the user lacks membership in returns 403 (system_adminis exempt). - Capabilities are resolved via
resolve_capabilities(tenant_role, user.system_role, db)and stashed on the per-requestRequestContext(see Glossary). CATALYST_AUTH_DEV_BYPASS=true(default in dev) lets unauthenticated requests through as a syntheticdev@localhostuser withSYSTEM_ADMIN_CAPABILITIES. Production safety check atapp.py:101-109refuses to boot if dev bypass is on with a non-loopback bind.- A catch-all handler (
_handle_db_error) returns503with"Database was re-initialized. Please retry your request."after re-initialising the database. It matches both backends’ missing-table phrasings and catchesProgrammingErroras well asOperationalError, which is what PostgreSQL raises. Until 2026-09 it matched only the fallback’s wording and so never fired on PostgreSQL.
Endpoints requiring fine-grained capabilities use the @requires("capability.name") decorator; the middleware sets the context that decorator reads.
Operational endpoints
Public; no auth, no tenant scoping.
| Method | Path | Source | Description |
|---|---|---|---|
| GET | /api/v1/health | app.py:518 | Liveness, process is up. Returns release, sha (12-char prefix), platform. |
| GET | /api/v1/ready | app.py:530 | Readiness, runs SELECT 1. Returns {status, database}. |
| GET | /api/v1/version | app.py:542 | Build identity, release, sha, branch, built_at, optional rolled_back_from. Resolution order: CATALYST_RELEASE/CATALYST_BUILD_SHA/etc env vars → build_info.json next to app.py → live git rev-parse. |
Auth
shared/auth_router.py, prefix /api/v1/auth.
| Method | Path | Description |
|---|---|---|
| POST | /register | Create a user. Requires settings.allow_open_registration. Auto-assigns member role in default_tenant_id. |
| POST | /login | Email + password → token pair. Pushes a low-priority operational alert on first login of the day or new IP (_notify_login). |
| POST | /refresh | Exchange a type=refresh token for a new pair. |
| GET | /me | Current user + sorted capabilities. |
| POST | /switch-tenant | Reissue tokens scoped to a different tenant. system_admin may switch to any tenant; everyone else must hold membership. |
All four token-emitting endpoints return the same TokenResponse:
{
"access_token": "...",
"refresh_token": "...",
"token_type": "bearer",
"expires_in": 3600,
"user": {
"id": "uuid",
"email": "...",
"display_name": "...",
"system_role": null,
"is_active": true,
"tenants": [{ "tenant_id": "epa-default", "role": "member" }],
"active_tenant_id": "epa-default"
}
}
Token TTLs come from settings.auth_access_token_minutes and settings.auth_refresh_token_days. Email addresses are HMAC-hashed before they hit log lines (_hash_email_for_log).
Assistant, chat + tools
Three routers share the /api/v1/assistant prefix.
Chat loop (engine/orchestrator.py)
| Method | Path | Description |
|---|---|---|
| POST | /chat | Agentic chat. Streams Server-Sent Events by default. |
ChatRequest body:
| Field | Type | Description |
|---|---|---|
message | string | User message text. |
conversation_id | string|null | Existing conversation UUID; a new one is minted if omitted. |
dataset_id | string|null | Scope this turn to a specific upload. |
complexity | simple|moderate|complex | Optional frontend hint. |
model_override | string|null | Per-request override of the configured chat model. |
file_context | string[] | Raw text snippets attached to the message (e.g. file previews). |
data_refs | string[] | Catalog entry IDs picked from the file picker / Fabric. Resolved server-side via catalog.get_entry_by_id(). |
inference_params | dict | Override temperature, top_p, etc. |
focus_context | string|null | Extra context injected from focus mode. |
agentic_mode | bool | Opt into the agentic prompt + plan tools + session state. |
stream | bool | Default true. Set false for a single JSON response. |
SSE event types, every event is a JSON object on a data: line. Event ordering is monotonic by sequence.
| Event | Payload | Notes |
|---|---|---|
status | {message} | Short progress label for the UI. |
thinking | {text} | Extended thinking content (pacing is RAF-batched on the frontend; see Streaming). |
text_delta | {text} | Incremental assistant text. Coalesced by useDisplayEvents.ts. |
tool_call | {tool_name, tool_input} | Full untruncated input. |
tool_result | {tool_name, result, duration_ms} | Full untruncated output. |
progress | {message, pct?} | Long-running tool progress. |
card | {type, data} | Rich artifact (chart, map, file_download, etc.). |
done | {cost, tokens_in, tokens_out, model} | Terminal event on success. |
error | {message} | Terminal event on failure. |
When stream: false the response collapses to {message, cards, metadata} with metadata.cost / tokens_in / tokens_out.
Convened conversations (new, 2026-09)
| Method | Path | Description |
|---|---|---|
| GET | /convened-workers | Durable state for a convened agent. Query by worker_id (single) or conversation_id (list, most-recent-first, limit 50). Tenant-scoped; a worker is reachable only through the ACL of the conversation that convened it, no second auth path. Returns 404 (not 403) on an inaccessible worker_id so existence can’t be probed. |
Backs tools/convene.py / engine/turn_driver.py, one conversation convening real conversations as workers and exchanging messages with them over N rounds. The card that renders this attaches to the live SSE stream first and falls back to this endpoint once the stream’s replay buffer has been cleared (i.e. the worker has already finished).
Admin/utility routes (engine/admin_routes.py)
Same prefix; carved out of the orchestrator so the chat module stays under its LOC budget.
| Method | Path | Description |
|---|---|---|
| POST | /chat/build-tool | Submit a ChatRequest-shaped body to the ToolJobQueue. Returns {message, cards: [tool_job_status], metadata}. |
| GET | /tools/jobs/{job_id} | Poll a build job. Returns the job status dict; 404 if unknown. |
| GET | /tools | List all tools registered for the current tenant. |
| GET | /chat/models | Models flagged chat_selectable=True in model_config, deduplicated by model_id, with provider, pricing, tier, and an available flag derived from API-key presence. |
| POST | /upload-image | Multipart upload (file). Extensions limited to .png/.jpg/.jpeg/.webp/.gif/.bmp/.tiff/.tif. Returns {file_path, filename, size_bytes, mime_type}. Size capped by settings.max_upload_bytes. |
Peer review
Defined in engine/orchestrator.py. Same prefix.
| Method | Path | Description |
|---|---|---|
| POST | /peer-review | SSE stream from a second model reviewing a conversation. Body: {conversation_id, reviewer_provider, reviewer_model}. |
| GET | /peer-review/models | Models eligible to act as reviewer. |
Downloads (api/downloads.py)
| Method | Path | Description |
|---|---|---|
| GET | /downloads/{file_id} | Serve a generated file from the generated_files table. Tenant-scoped; cross-user requires conversation.read.all. Falls back to a legacy glob lookup for pre-table files. |
| GET | /downloads/{file_id}/markdown | Convert a .docx to markdown for inline viewing. @requires("analyze"). |
file_id must match ^[a-f0-9\-]+$; anything else returns 400.
Chart pipeline
Four routers share the /api/v1/assistant prefix and back the v2-insight chart card lifecycle.
api/chart_snapshots.py, auto-capture canvas PNGs from rendered chart cards.
| Method | Path | Description |
|---|---|---|
| POST | /chart-snapshot | Persist a chart PNG keyed by (conversation_id, insight_hash). Deduped via deterministic filename, second POST returns {file_id, reused: true}. Caps body at 25 MB, validates PNG magic bytes, and patches ChatMessage.structured_content so Word/PDF exports embed the snapshot. |
api/map_snapshots.py, composited live-map PNGs (basemap + deck.gl layers + legend).
| Method | Path | Description |
|---|---|---|
| POST | /map-snapshot | Persist a map composite keyed by (conversation_id, display_id). Overwrite-latest: the row is reused and bytes are refreshed each turn as layers accrue. |
api/chart_bookmarks.py, user-pinned analytical views (named snapshots) and per-user UI prefs.
| Method | Path | Description |
|---|---|---|
| POST | /chart-snapshots | Save a named bookmark (title, description, insight, setup, thumbnail_file_id). Owner is the calling user. |
| GET | /chart-snapshots | List the caller’s bookmarks. conversation_read.all widens to tenant. Filters: conversation_id, dataset_id. |
| DELETE | /chart-snapshots/{snapshot_id} | Soft-delete (sets deleted_at). Owner-only unless conversation.read.all. |
| GET | /chart-prefs | Return the caller’s chart UI prefs dict. |
| PUT | /chart-prefs | Merge prefs into the stored row. Keys with null values are deleted. |
api/chart_url_states.py, server-side overflow for shareable chart URLs.
| Method | Path | Description |
|---|---|---|
| POST | /chart-url-state | Store an oversized state payload, return {id}. Validates state.shape == "v2-insight". |
| GET | /chart-url-state/{state_id} | Resolve a stored state and refresh last_accessed_at (rolling 365-day TTL; GC runs from scripts/background_jobs.py). |
Conversations
conversations/router.py, prefix /api/v1/conversations. Conversations are private-by-default; access is gated by check_conversation_access().
| Method | Path | Description |
|---|---|---|
| GET | “ | List recent conversations (most-recent first, limit 50). |
| POST | “ | Create a new conversation. |
| GET | /{conversation_id}/messages | Full message history including structured_content (canonical LLM messages). |
| POST | /messages | Persist a message. Backend owns assistant-message persistence; this endpoint is primarily used for user-side saves. |
| PATCH | /{conversation_id} | Update title. |
| DELETE | /{conversation_id} | Delete a conversation and its messages. |
| GET | /{conversation_id}/replay | Full immutable event log with optional filters: message_id, event_types (comma-separated), from_seq, limit (default 10000). Returns {events, total_events, total_cost_usd, tool_calls, errors}. |
| GET | /{conversation_id}/messages/{message_id}/events | Thinking/tool/progress/error events for a single message, used when reloading a conversation. |
| GET | /telemetry/summary | Conversation list with telemetry event counts for the Telemetry UI. |
Telemetry sorts by (timestamp, sequence), see shared/telemetry.py.
Notifications
shared/notification_router.py, prefix /api/v1/notifications. All endpoints require the notification.read capability except /broadcast which requires notification.broadcast.
| Method | Path | Description |
|---|---|---|
| GET | “ | List notifications for the caller. Query: unread_only, channel_type, reason, limit (default 50), offset. |
| GET | /unread-count | {count} for the badge. |
| GET | /stream | SSE stream. On connect, backfills up to 50 unread items; thereafter pushes new notifications. Sends : keepalive every 5 s. Honors the Last-Event-ID reconnect header. Filters server-side by recipient_scope (user/tenant/system). |
| POST | /{id}/read | Mark read. |
| POST | /{id}/dismiss | Mark dismissed. |
| POST | /{id}/action | Mark actioned. |
| POST | /read-all | Mark every notification for the caller read. Returns {status, count}. |
| POST | /broadcast | Tenant-wide broadcast. Body is NotificationCreate. |
SSE frame format:
id: <notification_id>
event: notification
data: { ... notification JSON ... }
Knowledge
knowledge/router.py, prefix /api/v1/knowledge.
| Method | Path | Description |
|---|---|---|
| GET | /corpora | List corpora with document_count and chunk_count. |
| POST | /corpora | Create a corpus. Returns 409 on duplicate name. |
| POST | /upload | Multipart upload (file, corpus_id). Extracts via unstructured for DOCX/PDF; openpyxl/pyxlsb for Excel. Returns {document_id, filename, chunk_count, char_count, duplicate}. |
| POST | /query | Direct RAG with synthesis. Body: {query, corpus_id?, top_k?}. Returns {message, cards: [cited_answer], metadata}. Useful for non-chat integrations. |
Predict
predict/router.py, prefix /api/v1/predict.
| Method | Path | Description |
|---|---|---|
| POST | /upload | Multipart upload. Accepts CSV/TSV/XLSX/XLS/XLSB; XLSB is converted via LibreOffice. String columns are auto-stripped on load. Returns dataset id, profile, preview, dtypes, optional sheets/active_sheet. |
| GET | /datasets | List uploaded datasets. |
| POST | /recommend | LLM picks algorithm + target. Body: {dataset_id, user_goal?}. |
| POST | /train | Train. Body: {dataset_id, target_column, feature_columns?, algorithm?, hyperparams?}. Returns metrics, shap_summary, scores, training time, row counts. |
| GET | /models | List trained models (in-memory + disk-restored on startup). |
| POST | /score | Score with a trained model. Body: {model_id, dataset_id?}. |
| POST | /explain | SHAP explanation for a single row. Body: {model_id, entity_index}. |
Insight
insight/router.py, prefix /api/v1/insight.
| Method | Path | Description |
|---|---|---|
| GET | /taxonomies | List classification taxonomies. |
| POST | /taxonomies/{taxonomy_id}/categories | Add a category. Body: {label, description, examples?}. |
| DELETE | /taxonomies/{taxonomy_id}/categories/{label} | Remove a category. |
| GET | /schemas | List extraction schemas. |
| POST | /schemas | Create a schema. Body: {name, description?, fields?, applies_to?}. Name must be unique. |
| PUT | /schemas/{schema_id} | Patch a schema. Body: {add_fields?, remove_fields?, update_applies_to?}. |
| DELETE | /schemas/{schema_id} | Delete a schema. |
| GET | /history | Recent processing results. Query: limit (default 50). |
| GET | /stats | Aggregate counts and total_cost_usd. |
Apps
apps/router.py, prefix /api/v1/apps. This is the App Platform surface, it replaced the old Workflow feature’s /api/v1/workflow/* surface, which is gone: workflow/router.py, workflow/engine.py, workflow/primitives.py, and workflow/assembly.py no longer exist (retired 2026-07; see the Skills & Primitives note in the project CLAUDE.md). What survives of the old workflow layer is workflow/factory.py (PrimitiveFactory) and workflow/seed_primitives.py, both now built on skills/primitive_runtime.py, see Building primitives.
An “app” here is a versioned, frozen manifest (tools + bindings) that runs under its own principal rather than the acting user’s, not the retired Workflow graph concept.
| Method | Path | Description |
|---|---|---|
| POST | /trigger | Fire a trigger. Body carries either an inline manifest (draft/preview, runs as the acting user) or app_id (+ optional version) naming a published app (runs under that app’s frozen principal). A suspended app returns a labeled refusal envelope, not an HTTP error. |
| POST | /validate | Validate a manifest, returning the invalidation map + derived triggers. Used by the app shell before rendering a draft and by the app factory’s dry-run loop. |
| GET | “ | List published apps for the tenant. |
| GET | /{app_id} | Fetch one app’s current published manifest. |
| POST | /{app_id}/publish | Publish a manifest as a new app version. |
| POST | /{app_id}/suspend | Suspend an app (subsequent triggers get a refusal envelope). |
| POST | /{app_id}/reactivate | Reactivate a suspended app. |
| POST | /{app_id}/transfer | Transfer app ownership. |
Auth: a normal authenticated user with tool.execute, the app principal for a published app is resolved server-side from app_id, never supplied by the caller. The chat-facing entry point (build_app tool, tools/factory_tools.py) drafts a manifest via apps/factory.py:build_app_manifest before any of these endpoints are hit.
Fabric
fabric/router.py, prefix /api/v1/fabric. The unified data catalog: tenant uploads (deduped by content_hash), conversation-scoped derived datasets, exports, Knowledge documents, and Kits (semantic dataset groupings). See Fabric module.
| Method | Path | Description |
|---|---|---|
| GET | /datasets | Tenant uploads grouped by (name, content_hash). |
| GET | /datasets/{dataset_id} | Full metadata. |
| GET | /datasets/{dataset_id}/preview | Preview first n_rows (default 10). |
| DELETE | /datasets/{dataset_id} | Tombstone, reports how many conversations referenced the same hash. |
| GET | /catalogs | All conversation catalogs with their datasets. |
| GET | /catalogs/{conversation_id}/datasets/{name}/dependents | Dependent datasets, for confirmation dialogs. |
| DELETE | /catalogs/{conversation_id}/datasets/{name} | Tombstone a conversation dataset. Body {cascade} opt-in tombstones derived datasets. |
| GET | /documents | Catalog documents. |
| DELETE | /documents/{document_id} | Hard-delete document + chunks; tombstones catalog entries. |
| GET | /exports | Generated exports for the tenant. Members see only their own; admins see all. |
| GET | /entries/{entry_id} | Resolve a catalog UUID, used by attachment badges. |
| GET | /kits | List Kits. |
| POST | /kits | Create. Body: {name, description?, dataset_ids?}. |
| GET | /kits/{kit_id} | Kit with members (dataset id, name, reason, added_by). |
| POST | /kits/{kit_id}/members | Add members. Body: {dataset_ids}. |
| DELETE | /kits/{kit_id}/members/{dataset_id} | Remove a member. |
| DELETE | /kits/{kit_id} | Delete a Kit. |
| POST | /kits/search | Body: {query}. Vector-search backed. |
| GET | /overview | Temperature-sorted view for the Fabric page. Datasets deduped by hash and filtered to durable entry types. Temperature labels: hot (≥70), warm (≥30), cold (≥5), frozen (<5). |
| POST | /dream-cycle | Run the dream cycle on demand. Requires tenant.manage. The dream cycle normally runs out of process via scripts/background_jobs.py. |
Document deletion is asymmetric: content (rows + chunks) is hard-deleted for storage hygiene, but catalog entries are tombstoned to preserve lineage. The “Tombstone, Never Delete” principle applies to the catalog surface only.
Admin
admin/router.py, prefix /api/v1/admin. Tenant-scoped; sensitive operations gated by capability.
| Method | Path | Description |
|---|---|---|
| GET | /models | All model_config rows ordered by role then provider. Includes pricing columns, context_window, max_output_tokens, capability flags. |
| GET | /models/available | Distinct models (deduped by model_id). |
| GET | /models/roles | Each role with its current active model. |
| PUT | /models/role/{role} | Assign a model to a role. Body: {model_id, max_output_tokens?}. Deactivates the previous winner. Valid roles: chat, fast, strong, compaction, tool_build_simple, tool_build_complex, tool_audit, peer_review, kit_curator, embedding. |
| GET | /models/role/{role}/chain | The models serving a role in priority order (redundancy chain, get_models_for_role). |
| PUT | /models/role/{role}/chain | Set the ordered list of models serving a role. Body: {model_ids}. Models not listed are deactivated for the role rather than deleted. Requires system.models.manage (platform-wide, unlike the single-model assign endpoint). |
| DELETE | /models/role/{role}/override | Clear this tenant’s tenant_model_overrides row for a role, falling back to the system default. Requires tenant.models.manage. |
| GET | /settings | Every administrator-visible runtime setting, grouped, with current + default value and a withheld_secret_count (secrets are never listed or writable, see shared/settings_surface.py). Requires system.settings.manage. |
| PUT | /settings/{key} | Set one override. Body: {value}. Applied immediately for settings in LIVE_SETTINGS, otherwise on next restart. |
| DELETE | /settings/{key} | Drop an override, reverting to the environment/default value. |
| GET | /memory/census | Per-store memory residency snapshot (namespaces, catalogs, SQL engine, session streams, async jobs), each section reports independently so one store’s failure doesn’t 500 the whole census. |
| GET | /mcp/connections | List MCP connections with live status. |
| POST | /mcp/connections | Add a connection. Body: MCPConnectionCreate (name, transport, plus stdio or HTTP fields). |
| PUT | /mcp/connections/{name} | Update a connection. |
| DELETE | /mcp/connections/{name} | Soft-delete and disconnect. |
| POST | /mcp/connections/{name}/connect | (Re)connect. |
| POST | /mcp/connections/{name}/disconnect | Disconnect. |
| GET | /mcp/marketplace | Smithery.ai search proxy (avoids browser CORS). Query: q, pageSize, page, verified, remote, isDeployed. |
| GET | /mcp/marketplace/{qualified_name} | Server detail proxy. |
| GET | /users | Users in the current tenant. Requires tenant.users.manage. |
| POST | /users | Add an existing user to the tenant. Body: {email, role?}. |
| PUT | /users/{user_id}/role | Change role. Body: {role}. |
| DELETE | /users/{user_id} | Remove membership (does not delete the user account). |
| GET | /async/status | Pool state for the AsyncExecutionService, worker counts, circuit breaker, dead letter queue. |
FinOps
shared/finops_router.py, prefix /api/v1/finops. Every endpoint requires finops.read; tenant-wide endpoints additionally require finops.read.all. Cross-tenant access requires finops.read.cross_tenant. Members see only their own rows (plus NULL-user rows for background work they triggered) unless they hold finops.read.all.
| Method | Path | Description |
|---|---|---|
| GET | /spend/{tenant_id} | Spend summary. Query: period=today|week|month|all. |
| GET | /budget/{tenant_id} | Budget utilization for the tenant. |
| GET | /summary/{tenant_id} | Higher-level summary. Default period=30d. |
| GET | /timeseries/{tenant_id} | Spend over time. Query: period, granularity=day|..., group_by=null|module|attribution|user. group_by=user requires admin. |
| GET | /by_model/{tenant_id} | Spend grouped by model. |
| GET | /by_module/{tenant_id} | Spend grouped by module. |
| GET | /overhead/{tenant_id} | Background/automation overhead. Admin-only. |
| GET | /by_user/{tenant_id} | User leaderboard. Admin-only. |
| GET | /top_conversations/{tenant_id} | Most-expensive conversations. limit clamped to 1-200. |
| GET | /anomalies/{tenant_id} | Heuristic anomalies. Admin-only. Default period=7d. |
A missing DB table on the legacy /spend / /budget paths returns a degraded but valid zero-filled response instead of 500.
Spatial
tools/spatial_router.py, prefix /api/v1. Serves content-addressed blobs produced by mapping tools so the browser fetches map data over normal HTTP instead of through the SSE chat stream. Hashes must match ^[a-f0-9]{64}$. Responses are immutable (Cache-Control: public, max-age=31536000, immutable, ETag: "<sha>", X-Content-Type-Options: nosniff).
| Method | Path | Capability | Description |
|---|---|---|---|
| GET | /geojson/{sha256_hex} | geojson.read | Gzipped GeoJSON. Content-Encoding: gzip, Content-Type: application/geo+json. |
| GET | /geoparquet/{sha256_hex} | geoparquet.read | Raw Parquet bytes (zstd internally). Content-Type: application/vnd.apache.parquet. |
| GET | /blob/{sha256_hex} | geoparquet.read | Type-agnostic gzipped binary used by the volumetric pipeline. Sniffs decompressed magic bytes (PNG, JPEG, GIF, WebP) to set Content-Type; falls back to application/octet-stream. |
| POST | /volume/run_dispersion | geoparquet.read | Drives simulate_dispersion through the tool registry and returns {manifest_url, summary, save_as}. Used by the volume lab. |
Status codes for blob fetches:
| Status | Meaning |
|---|---|
| 200 | Hit |
| 400 | Malformed hash |
| 404 | Blob not found, wrong type, or wrong tenant |
| 410 | The catalog entry that originally bound the blob is tombstoned AND no other live catalog entry references the same hash |
The 410 path matters for back-history: a blob whose original catalog entry was retired can still be served if another live entry points at the same content hash, so a previously-rendered card doesn’t break when the user re-saves a dataset under the same name.
Environmental layers
tools/env_layer_router.py, prefix /api/v1. Both endpoints require the search capability.
| Method | Path | Description |
|---|---|---|
| GET | /environmental-layers | Grouped layer catalog (groups[].layers[] with id, label, levels, forecast_hours, element_type, units). |
| POST | /environmental-layers/fetch | Fetch a layer and return a map_display card. Body: {element, bbox[4], config?, forecast_hour?, display_id?}. |
Fetch behavior:
element_type == "tile_raster"short-circuits to aTileLayerspec (Iowa Mesonet NEXRAD, etc.), no fetch, no blob.- Otherwise auto-scales grid resolution by bbox span:
>20°→ 0.1° (~11 km),>5°→ 0.05° (~5.5 km), else 0.02° (~2.2 km). - Field data is serialized to GeoParquet via
write_vector_field, content-addressed into the blob store, and surfaced as avector_fieldsource pointing at/api/v1/geoparquet/<sha>. - Errors map to
422(validation / empty coverage),404(unknown layer),502(upstream failure),500(serialization or blob write). display_idprovided ⇒update_mode: "merge"so the card layers onto the existing map. Otherwise"full".
SSE conventions
Three endpoints stream Server-Sent Events: /assistant/chat, /assistant/peer-review, and /notifications/stream. Common shape:
- Headers:
Content-Type: text/event-stream,Cache-Control: no-cache,X-Accel-Buffering: no. - Frames are
data: <json>\n\n. The notifications stream additionally setsid:(forLast-Event-IDreconnect) andevent: notification. - The notification stream issues
: keepalive\n\nevery 5 s while idle. The chat stream relies on continuous event flow; if the clientis_disconnected()mid-turn, the orchestrator’s cancellation check unwinds the loop. - Chat events carry a monotonic
sequencefield. Telemetry replay sorts by(timestamp, sequence).
The frontend SSE consumer (frontend/src/hooks/useDisplayEvents.ts) coalesces consecutive reasoning events and applies RAF-batched updates; see Streaming.
Error envelope
Every error response is {"detail": <string | array>}. FastAPI’s default 422 validator emits an array of field-level error objects; everything else is a string.
| Status | Category | Common triggers |
|---|---|---|
| 400 | Bad request | Invalid input that bypassed schema validation (malformed hash, wrong MIME, oversized payload via 413). |
| 401 | Unauthenticated | Missing/invalid/expired JWT (when dev bypass is off). |
| 403 | Forbidden | Capability missing, tenant mismatch, cross-user resource. |
| 404 | Not found | Resource missing or hidden by tenant scoping (cross-tenant reads are indistinguishable from absence). |
| 409 | Conflict | Duplicate corpus name, etc. |
| 410 | Gone | Spatial blob whose binding is tombstoned and unreachable. |
| 413 | Payload too large | Snapshot uploads (>25 MB), settings.max_upload_bytes overruns. |
| 422 | Validation | Request body fails Pydantic schema; layer fetch has no coverage; bbox malformed. |
| 500 | Server error | Unhandled exception. Goes through _unhandled_exception_handler which dedup-pushes a high-priority alert keyed on (path, exception class, args[:80]). |
| 503 | Service unavailable | DB re-initialized under a stale connection, or DI container not yet wired. |
Tenant header
X-Tenant-ID: my-tenant
Every resource is strictly scoped by tenant. Omitting the header uses settings.default_tenant_id. Cross-tenant reads are indistinguishable from 404 by design, listing endpoints filter at the database level, and read endpoints 404 rather than 403 to avoid leaking the existence of out-of-scope resources where it matters (downloads, blobs).
Uniform Tool Contract, model-facing surface
Every tool handler’s return value is one of three named result variants, Ok | Hint | Err (engine/contract.py), applied at the dispatch boundary in engine/tool_registry.py:_apply_contract. The migration ratchet (tests/contract_deferred_baseline.json) reached zero deferred tools in June 2026; every tool registered in engine/tool_definitions.py:SYSTEM_TOOLS now goes through the contract. The contract governs the tool boundary the agent loop sees, not REST. REST endpoints are out of scope; calling /api/v1/predict/train directly still returns the legacy JSON envelope, that adapter (envelope_to_tool_result) stays in place for the orchestrator path even though the deferred set is empty.
What this means for REST callers in practice:
tool_resultSSE events from/assistant/chatcarry the rendered envelope text. Hint events look like recoverable corrections (“did you mean…”, “add filter:…”) rather than errors. Theerrorevent is reserved for actual platform failures.- The
doneevent’scostcontinues to come from theusage_logtable.ToolSpec.is_cost_rollup=Trueondelegate/delegate_batchkeeps leaf costs from being double-counted in the rollup. - Telemetry events carry a
contract_envelopetype (envelope-kind histogram per tool). It surfaces through/conversations/{id}/replaylike any other event.
See Engine and Tool System for the boundary spec.
Related developer docs
- Engine, orchestrator, dispatcher, tool registry
- Conversations, access control, telemetry replay
- Fabric module, catalog, Kits, dream cycle
- Admin module, model config, MCP, users
- Knowledge module, corpora, ingest, RAG
- Predict, training, scoring, SHAP
- Insight, classification, extraction
- Skills, SKILL.md authoring, discovery, and primitive execution
- Streaming, SSE consumption on the client
- Glossary, capabilities, roles, request context