Documentation
¶
Overview ¶
GET /catalog/graph — the whole catalog as a minimal graph for the admin model picker.
Returns providers, hosts, and models (with their host bindings) in one call so the UI can render and cross-link them without three separate list fetches + client-side flattening. Only the fields the picker and the policy model view actually read are included — identity (id/name/ displayName), the enabled flag, host icon path, the featured flag (from the catalog's `labels.featured`), per-model deprecation, capabilities, context window, the pointer (the upstream wire name the bare model name resolves to at request time), and the (hostId, adapter, enabled) bindings. Heavy spec (full snapshot list, pricing refs, modalities, …) is deliberately omitted.
Reads the in-memory snapshot via the same index as /catalog/resolve, so it returns exactly what the data plane can route to right now: disabled providers/hosts/models are already absent, and disabled bindings are pruned here. The picker only offers routable targets; full-list / detail views use the CRUD APIs (PG), which show disabled rows.
GET /config.json — the public, unauthenticated runtime config the embedded admin UI fetches once at boot, same-origin (it is served from the control listener, which is also where the SPA is served from). Modelled on the OIDC `/.well-known/openid-configuration` discovery pattern.
Why runtime, not build-time: relay-ui is built once into a generic tarball and go:embed'd into the binary, so VITE_* build-time env can't encode a specific deployment's URLs/flags. The UI learns them here at boot instead.
WORLD-READABLE — public values only. Never add secrets (keys, DSNs to private services, internal hostnames). The body is rendered once at registration from static process env, so it is effectively a constant for the process lifetime.
Package control is the admin-plane HTTP API: /auth/*, CRUD across the eight catalog kinds, /version, /master-key/*, /reload.
Mount(r, deps) wires huma+chi onto an existing chi router and returns the huma.API. main.go constructs Deps and calls Mount.
GET /debug/snapshot — render the current data-plane snapshot for operator inspection. The snapshot is what the hot path actually sees; diffing it against PG (via the regular admin list endpoints) is the fastest way to spot what sanitize dropped.
detail=counts (default): per-kind row counts + per-policy reverse-join
sizes. Tiny payload, scans nothing.
detail=full: every entity in stable slug order, sanitized
spec included. Bigger; admin-only, no SLO.
list_filter.go wires the pure pkg/filter engine into the generic CRUD list handler. huma binds typed structs, but the filter allowlist is per-resource dynamic, so a small middleware stashes the raw url.Values on the request context and the list resolver reads them back to feed filterSchema.Parse. See registerKind in crud.go and the per-resource schemas in list_schemas.go.
list_schemas.go declares the per-resource filter allowlists consumed by the generic list handler (registerKind). Each schema is the single source of truth for that resource's filterable/sortable params: the accessors are typed closures, so renaming an underlying spec field is a compile error here. The query-param name, the allowlist entry, and the match logic all derive from one Field literal.
Covers all eight catalog kinds. Time filters (created/updated, release/ deprecation dates) read Metadata timestamps + spec date strings; ?label= k=v works on every kind via the Labels hook. The host-key circuit-breaker state filter (?health=) is intentionally absent here — it needs a snapshot+kv join the pure store-slice engine can't express; see docs/filtering.md F3.
Logs read-side endpoints: the full per-request lifecycle view that drives the frontend Logs page. A "log" is what happened on a request (routing, status, timing, tokens, errors, identity) — i.e. the full usage.Event — with the captured request/response bodies attached when payload logging was opted in for that request ("(not logged)" otherwise).
GET /logs full lifecycle records, newest first, filterable
GET /logs/{request_id} one record + its captured bodies (if any)
Distinction from /usage: /usage is the narrow metrics projection (billing); /logs is the full record + optional body. Both read the same log event stream via usagelog.Reader; the body joins by request_id via payload.Reader.
Policy model-grant normalization + enabled-resolution guard.
At policy create/update the catalog-ref strings in Spec.Models (and each RLBinding's Models) are:
- slugified to canonical form — operators may paste real-world names ("openai/GPT-4o", "anthropic/claude-3.5") and they're rewritten to the stored slug form ("openai/gpt-4o", "anthropic/claude-3-5"). This keeps PG, the data-plane snapshot, and the picker agreeing on one string.
- resolved against the catalog and required to match at least one *enabled* binding (model enabled, host enabled, binding enabled). Host-only "@host" refs only require the host to exist + be enabled — they grant every present and future binding on that host.
Refs that resolve to nothing enabled are rejected with 400. This is the cross-entity check the per-row policy.Validate() (grammar only, no catalog access) can't perform. Host-key / relay-key existence is deliberately NOT checked here — the inference path handles those at request time.
Attach / detach RelayKey ↔ Policy from the policy side. The authoritative field is RelayKey.Spec.PolicyID (1:N — one policy per key, many keys per policy); these endpoints mutate it so the policy form can manage its key roster without round-tripping through the relay-key form.
POST /policies/by-id/{id}/relay-keys/{relayKeyId} — attach
DELETE /policies/by-id/{id}/relay-keys/{relayKeyId} — detach
Attach overwrites any existing PolicyID on the relay key (no confirmation): moving a key from policy A to policy B is the common case. Detach succeeds when the key currently points at this policy; mismatched detach returns 409 to surface the drift instead of silently no-op'ing.
GET /{kind}/by-id/{id}/references — generic reverse-ref lookup.
Returns every PG row that references the target entity. Used by the admin UI for blast-radius confirmation dialogs ("deleting this rate limit will affect N policies") and for inline "in use by" lists.
Walks the relevant stores per kind; admin-only, no SLO. Indices come later if scan time matters. Reading PG (not the catalog snapshot) so disabled / soft-dropped refs are still visible — they exist in PG even when the data plane has filtered them out.
Custom POST /relay-keys: generates the bearer plaintext server-side via relaykey.Generate, persists only the hash + prefix, and returns the plaintext exactly once on the create response. The generic CRUD POST in registerKind is skipped for this kind (skipCreate=true) so callers can't sneak a precomputed keyHash through.
subresources.go binds the catalogview read projections to chi/huma as resource-navigation endpoints — "API UX":
GET /models/{ref}/hosts hosts serving this model (+ binding + pricing)
GET /models/{ref}/pricing pricing per host for this model
GET /models/{ref}/policies policies granting this model (+ per-model limits)
GET /hosts/{ref}/models models this host serves (+ binding + pricing)
GET /hosts/{ref}/keys upstream credentials this host owns (secret-free)
GET /hosts/{ref}/policies host-tier serving policies this host owns
GET /policies/{ref}/models models this policy grants (+ per-model limits)
GET /policies/{ref}/hosts hosts this policy reaches (+ host-keys)
GET /policies/{ref}/rate-limits rate-limit rule sets this policy references
All composition lives in app/catalogview (PG-backed, full state incl. disabled rows). These handlers are thin: build the Service from the stores, call the projection, return its rows. {ref} is a slug or UUID id.
Usage read-side endpoints: surface the JSONL stream the post-flight observer writes via filtered + aggregated queries. Backed by a usagelog.Reader, so the store can swap (file today, ClickHouse later) without touching this layer.
GET /usage/events raw events, newest first, filterable GET /usage/summary per-group aggregates over the filtered set
Index ¶
- func AdminTokenMiddleware(adminToken string) func(http.Handler) http.Handler
- func CORS(allowedOrigins ...string) func(http.Handler) http.Handler
- func Mount(r chi.Router, d Deps) huma.API
- func RequireActor(next http.Handler) http.Handler
- type Deps
- type HostHealthReader
- type RuntimeConfig
- type Telemetry
- type UsageFilterInput
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AdminTokenMiddleware ¶
AdminTokenMiddleware accepts the request iff it carries a valid bearer admin token in the Authorization header. On success it injects an Actor with AdminToken=true into context so downstream handlers + authz see a valid caller. On absent/mismatched token it does nothing — the next middleware in line (session) may still authenticate.
adminToken is the cleartext token from RELAY_ADMIN_TOKEN. Empty disables the bypass entirely.
func CORS ¶
CORS returns a middleware that handles preflight (OPTIONS) and adds the headers needed for a credentialed browser fetch from one of the allowed origins. Required because the control API is consumed by a frontend on a separate hostname (e.g. https://relay.wyolet.dev).
allowedOrigins is matched exactly against the Origin header — wildcard origins are not allowed when Access-Control-Allow-Credentials is true, per the CORS spec.
func Mount ¶
Mount installs the control-plane huma API on r and registers all operations. Returns the huma.API so the caller can attach test-only ops.
Middleware order on r (caller is responsible for wiring these onto the chi router before any operations are matched):
- session.Manager.Middleware — loads cookie session, sets Actor in ctx
- AdminTokenMiddleware — sets Actor in ctx when bearer matches
Per-operation Middlewares attached by Mount enforce RequireActor on every protected route. Public ops (/auth/login, /version) omit it.
Types ¶
type Deps ¶
type Deps struct {
// Identity is the YAML-backed user store used by /auth/login.
Identity *identity.Store
// Sessions is the cookie-backed session manager. Login/Logout write to
// it; the session middleware (installed by Mount) reads from it.
Sessions *session.Manager
// AdminToken is the cleartext break-glass bearer. Empty disables the
// bypass. Validated by AdminTokenMiddleware; not used directly by
// handlers.
AdminToken string
// Authz is the policy-decision interface. Handlers call
// d.Authz.Authorize before mutations; today's impl is permissive for
// any authenticated caller.
Authz authz.Authorizer
// Catalog is the in-memory snapshot used for slug→id resolution on
// reads. Writes go through Stores.
Catalog *appcatalog.Catalog
// Stores is the bundle of eight typed stores used by CRUD writes.
Stores *appcatalog.Stores
// CookieSecure controls the Secure attribute on the session cookie.
// Surfaced here so the OpenAPI doc can reflect deployment posture.
CookieSecure bool
// UsageReader serves /usage/* read-side endpoints. nil disables
// them — useful for deployments where usage events are consumed
// from a separate store.
UsageReader usagelog.Reader
// PayloadReader serves /payloads/* read-side endpoints (the Logs view).
// nil disables them — e.g. minimal builds or deployments where captured
// bodies are consumed from a separate store.
PayloadReader payloadlog.Reader
// Selector is the keypool circuit-breaker owner, shared with the data
// plane. The host-key health endpoint reads per-key breaker state through
// it. nil disables that endpoint.
Selector *keypool.Selector
// HostHealth reads per-host runtime reachability (observed state), shared
// with the data plane. Overlays host.Status on host reads. nil disables
// the overlay (Status stays absent → UI shows "unknown").
HostHealth HostHealthReader
// RuntimeConfig is the public config served at GET /config.json for the
// embedded admin UI. Zero value is fine (UI falls back to origin defaults).
RuntimeConfig RuntimeConfig
}
Deps is the typed dependency bundle for the admin plane.
type HostHealthReader ¶ added in v0.2.0
HostHealthReader reads observed host reachability. Implemented by app/hosthealth.Recorder.
type RuntimeConfig ¶ added in v0.2.0
type RuntimeConfig struct {
ControlAPIURL string `json:"controlApiUrl,omitempty"`
InferenceAPIURL string `json:"inferenceApiUrl,omitempty"`
Mode string `json:"mode,omitempty"` // "oss" | "cloud"
Version string `json:"version,omitempty"`
Features map[string]bool `json:"features,omitempty"`
Telemetry *Telemetry `json:"telemetry,omitempty"`
DocsURL string `json:"docsUrl,omitempty"`
SupportURL string `json:"supportUrl,omitempty"`
}
RuntimeConfig is the JSON shape the UI reads. Every field is optional and omitempty: an empty controlApiUrl tells the UI to use its own origin; an empty inferenceApiUrl has no safe origin default (UI/data-plane may differ), so the UI prompts rather than guessing. The object is additive — the UI reads keys it knows and ignores the rest.
type Telemetry ¶ added in v0.2.0
type Telemetry struct {
SentryDSN string `json:"sentryDsn,omitempty"`
Environment string `json:"environment,omitempty"`
}
Telemetry carries PUBLIC client-side telemetry config only.
type UsageFilterInput ¶
type UsageFilterInput struct {
Since string `query:"since" doc:"Relative window (e.g. \"1h\", \"24h\", \"7d\"). Default \"1h\". Ignored when from is set."`
From string `query:"from" doc:"Absolute lower bound (RFC3339). Overrides since."`
To string `query:"to" doc:"Absolute upper bound (RFC3339)."`
RequestID string `query:"request_id" doc:"Exact match on a single request id (deep-link one event)."`
RelayKeyHash []string `query:"relay_key_hash" doc:"Match any of the given sha256 hashes of the inbound bearer."`
PolicyID []string `query:"policy_id" doc:"Match any of the given Policy.metadata.id values."`
ModelID []string `query:"model_id" doc:"Match any of the given Model.metadata.id values."`
HostID []string `query:"host_id" doc:"Match any of the given Host.metadata.id values."`
Source []string `query:"source" doc:"Match any of \"pipeline\" | \"proxy\" | \"ws\" | \"batch\"."`
FinishReason []string `query:"finish_reason" doc:"Match any of \"stop\" | \"length\" | \"tool_calls\" | \"content_filter\" | \"refusal\"."`
ErrorKind []string `query:"error_kind" doc:"Match any of the given error_kind values."`
StatusMin int `query:"status_min" doc:"Minimum HTTP status to include."`
StatusMax int `query:"status_max" doc:"Maximum HTTP status to include."`
HostKeyID []string `query:"host_key_id" doc:"Match any of the given HostKey.metadata.id values."`
RequestedModel []string `query:"requested_model" doc:"Match any of the model strings as the caller sent them."`
Status []int `query:"status" doc:"Match any of these exact HTTP status codes."`
StatusClass string `query:"status_class" doc:"Convenience status band: \"2xx\" | \"4xx\" | \"5xx\". Sets status_min/max."`
Streamed string `query:"streamed" enum:"true,false" doc:"true = only streamed responses, false = only non-streamed."`
Error string `query:"error" enum:"true,false" doc:"true = only errors (status>=400 or error_kind set), false = only successes."`
AttemptsMin int `query:"attempts_min" doc:"Minimum upstream try count (failover) — finds retried requests."`
DurationMsMin int64 `query:"duration_ms_min" doc:"Minimum total duration in ms (slow-request filter)."`
DurationMsMax int64 `query:"duration_ms_max" doc:"Maximum total duration in ms."`
TTFTMsMin int64 `query:"ttft_ms_min" doc:"Minimum upstream time-to-first-byte (ms); excludes requests with no upstream timing."`
TTFTMsMax int64 `query:"ttft_ms_max" doc:"Maximum upstream time-to-first-byte (ms)."`
Q string `query:"q" doc:"Free-text substring across request_id, model_id, requested_model, source."`
}