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:
Edit the Pydantic models in
fastrag.serve(_schemas.pyfor request/response bodies,_generation.py/_schemas.pyfor the SSE event payloads — see Serving internals and Streaming).task openapi— rebuilds an app viacreate_app(no server, no realQueryEngine/ChatStoreneeded — seescripts/generate_openapi.py) and overwritessdk/openapi.json.cd ui/registry && task generate:api-types— runsopenapi-typescriptagainst../../sdk/openapi.jsonand overwritessrc/registry/new-york/chatbot/lib/chatbot/api.d.ts.Commit
openapi.jsonandapi.d.tstogether with the model change. The “Frontend types” GitHub Actions workflow repeats steps 2–3 on every pull request touchingsdk/orui/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 SSEmessage_endevent’serror.codeand in a stored message’sMessageResponse.error.code.ApiErrorCode— the values a request-level failure carries instead (a401/404/409/413/415/422/429/500raised before any stream opens), derived from_errors.py’sHTTP_STATUS_ERROR_CODESplus 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.