Frontend types#

The backend’s fastrag.serve Pydantic models and the chatbot/local-chatbot widget in ui/registry/ need to agree about the /v1 wire format. Generating widget types from the backend’s OpenAPI document prevents hand-maintained shapes from drifting; CI fails when fresh generation changes either artifact.

The dashboard reads Postgres directly and derives its database types from a separate Drizzle-introspected schema. It does not import the widget’s types.

The one-way flow#

fastrag.serve's Pydantic models  (source of truth)
        │  task openapi
        ▼
sdk/openapi.json                 (checked in, generated)
        │  task generate:api-types   (from ui/registry/)
        ▼
ui/registry/.../lib/chatbot/api.d.ts     (checked in, generated)
        │  imported locally
        └──────────────► widget chatbot/local-chatbot types

Never the reverse, and never a third hand-maintained schema file both sides are supposed to follow. To change the contract:

  1. Edit the Pydantic models in fastrag.serve (_schemas.py for request/response bodies, _generation.py/_schemas.py for the SSE event payloads — see Serving internals and Streaming).

  2. task openapi — rebuilds an app via create_app (no server, no real QueryEngine/ChatStore needed — see scripts/generate_openapi.py) and overwrites sdk/openapi.json.

  3. cd ui/registry && task generate:api-types — runs openapi-typescript against ../../sdk/openapi.json and overwrites src/registry/new-york/chatbot/lib/chatbot/api.d.ts.

  4. Commit openapi.json and api.d.ts together with the model change. The “Frontend types” GitHub Actions workflow repeats steps 2–3 on every pull request touching sdk/ or ui/registry/ and fails if either file comes out different from what is checked in.

Why the generated file lives inside the widget#

api.d.ts lives inside the chatbot registry item’s own directory, not in a shared package, because ui/registry/ is a shadcn-style registry: a consumer runs npx shadcn add .../chatbot.json and gets a copy of every file the item lists, api.d.ts included — the widget has to be self-contained, so its own generated types travel with it. types.ts (the widget’s hand-maintained, friendly names — Role, Source, Message, …) is a thin layer of type aliases onto api.d.ts’s components["schemas"], not a second hand-typed copy of the server’s shapes.

The dashboard uses task db:pull (drizzle-kit pull) to introspect the SDK-owned Postgres schema into dashboard/lib/chats/generated/schema.ts. dashboard/lib/chats/postgres.ts derives MessageRole, FinishReason, and ErrorCode from Drizzle’s inferred message row type. JSONB source URLs are validated at the read boundary because database introspection does not describe nested JSON shapes. This database schema is separate from the widget’s HTTP contract and is not regenerated by the Frontend types workflow.

Two error.code vocabularies, two schemas#

The OpenAPI document publishes two different enums for what looks like the same field:

  • ErrorCode — the nine values a generation that started and then failed can record (fastrag.query.chat.base.ERROR_CODES), carried in the SSE message_end event’s error.code and in a stored message’s MessageResponse.error.code.

  • ApiErrorCode — the values a request-level failure carries instead (a 401/404/409/413/415/422/429/500 raised before any stream opens), derived from _errors.py’s HTTP_STATUS_ERROR_CODES plus its two default codes.

See Streaming for why a request that never reaches generation can never produce an ErrorCode. Both are real Python enum.Enum classes (fastrag.serve._schemas.ErrorCode / ApiErrorCode, and the matching MessageRole / FinishReason), built from the same tuples fastrag.query.chat.base already exposes — not a second hand-typed list — so they can never enumerate a value the store itself does not know about.

SSE event schemas#

FastAPI only publishes a Pydantic model into the OpenAPI document’s components/schemas when some route’s request or response references it. The three streaming routes’ actual response is a bare StreamingResponse with no declared body, so the SSE event payloads (MessageStartEvent, SourcesEvent, DeltaEvent, TextDelta, MessageEndEvent) would never appear on their own. _openapi.py’s openapi_schema() force-publishes them after the fact — scripts/generate_openapi.py calls it instead of app.openapi() directly — purely so api.d.ts still gets a named type for every event Streaming documents.

See also#

HTTP API and Streaming for the contract itself; Serving internals for fastrag.serve’s module layout; ui/registry/src/registry/new-york/chatbot/lib/chatbot/types.ts for the widget’s friendly aliases.