Per althing thread 01KRCNSF0V5NDCKB34H663MXHS — the catalog declared
14 services but 6 of them aren't running on irv-ml1 (chatterbox,
index-tts, qwen3-tts, cosyvoice, voxtral, kyutai-tts; missing from
docker ps entirely). Without action, the asset-engine UI would
declare them as available and consumers would hit unreachable
endpoints.
asset-engine consumer chose option (1) of three I sketched: extend
StatusT with `down` and treat it identically to `catalog-deferred`
in the picker (greyed, non-clickable). Lightweight, declarative, no
runtime health-check machinery, easy to revert when services
return.
Changes:
- StatusT enum (in asset_engine/catalog.py — committed there
separately) extended from
Literal["ready", "catalog-deferred", "experimental"]
to
Literal["ready", "catalog-deferred", "experimental", "down"]
- 6 services flipped to status: down.
- CATALOG-CONTRACT.md: replaced the bare-enum status row with a
four-row sub-table that names each value's meaning AND its picker
behavior. `down` and `catalog-deferred` get the same UI treatment
but the tooltip text differentiates ("Catalog-deferred" vs
"Service down — temporarily unreachable on irv-ml1") so the
semantic distinction (design state vs fleet-ops state) is
preserved.
- CATALOG-CONTRACT.md versioning policy table: new row codifying
"extending an existing enum (StatusT, FieldTypeT, ResponseTypeT,
CategoryT) with a non-conflicting value, with the consumer
updated in the same coordinated change" → no catalog_version
bump. Explicit rule for future enum extensions.
- JSON Schema regenerated.
catalog_version stays at 1.
Operational note (not catalog-side): the down services likely got
reaped 13+ days ago per the docker timestamps when other unrelated
work was done on irv-ml1. Bringing them back is a deploy task
outside this commit's scope. Flip status: down → ready in this file
once each one's confirmed running.
12 KiB
services.yaml — contract for downstream consumers
services.yaml in this directory is the canonical, versioned
description of the inference services this fleet exposes. It is a
first-class artifact: external tools (the asset-engine UI is the
first; CLIs, monitoring dashboards, and other internal services may
follow) consume it programmatically, and changes to its shape or
contents have downstream effect.
This document is the consumer contract.
Where the data lives
- Canonical source:
docs/asset-engine/services.yamlin theeshpfi-managementrepo (this file's directory). - Schema:
docs/asset-engine/services.schema.json(JSON Schema draft 2020-12). Generated from the Pydantic model in~/development/asset_engine/src/asset_engine/catalog.py— regenerate viacd ~/development/asset_engine && uv run scripts/dump_schema.pywhenever the model changes. - Change history: git log on this directory.
Data model — high level
catalog_version: 1 # bump on SCHEMA changes
services: [Service] # the entries; closed list
reproducibility_audit: [Audit] # one entry per service
Service entry
| field | type | required | meaning |
|---|---|---|---|
id |
string | yes | stable URL-safe identifier; never reused |
name |
string | yes | display name |
description |
string | no | one-paragraph summary |
category |
enum: tts, asr, sfx, music, image | yes | groups in pickers |
version |
int | yes | per-service schema version; bump on field changes |
status |
enum: ready, catalog-deferred, experimental, down | no | default: ready |
Status semantics:
| value | meaning | picker behavior |
|---|---|---|
ready |
Deployed, exercised, current default | clickable, primary surface |
experimental |
Deployed, may break under unusual params or while a custom renderer is being built (e.g. kokoro-captioned pre-renderer) | clickable, surfaced with a "experimental" tag |
catalog-deferred |
By-design out of scope this catalog version (e.g. ComfyUI requires per-asset workflow templates the catalog can't express today) | greyed, non-clickable, "deferred" tooltip |
down |
Temporarily unreachable on the host fleet — deployment gap, scheduled maintenance, container reaped, etc. Distinct from catalog-deferred because the intent is to flip back to ready once redeployed; the catalog row still describes a real intended service. |
greyed, non-clickable, "service down" tooltip |
host |
string (host alias) | yes |
endpoint |
URL | yes |
method |
string | yes |
content_type |
MIME type | yes |
model.id |
string | yes |
model.revision |
string | null | no |
model.image |
string | yes |
fields |
list[Field] | no |
section_groups |
list[SectionGroup] | no |
response.type |
enum: audio, image, video, text, json, file | yes |
response.mime |
string | no |
response.mime_from_field |
string | no |
response.output_field |
string | no |
response.audio_field |
string | no |
response.audio_format_field |
string | no |
response.timestamps_field |
string | no |
reproducibility.seedable |
bool | yes |
reproducibility.deterministic |
bool | yes |
reproducibility.notes |
string | no |
estimated_latency.cold_start_s |
int | float | no |
estimated_latency.warm_per_unit |
string | no |
license |
string | yes |
license_warning |
string | no |
notes |
string | no |
Field entry
The field-type vocabulary is closed. Adding a new type requires:
(a) updating the Pydantic model in asset_engine/catalog.py,
(b) bumping catalog_version, (c) coordinating with all consumers.
type |
meaning |
|---|---|
text |
single-line string |
textarea |
multi-line string |
number |
int or float, no bounded range |
slider |
bounded numeric — requires min, max, optional step |
select |
enum — options: list OR remote source_url: + source_jsonpath: |
bool |
true/false |
file |
file upload — requires accepted_types: list of MIME |
json |
freeform JSON value (arrays of floats, ref objects, etc) |
Optional/required: a field is required UNLESS it has either
required: false or optional: true. (Both ways are accepted; new
entries should prefer required: false.)
Response renderer vocabulary
Closed set: audio, image, video, text, json, file.
Same change-management as field types.
Versioning policy
| Change | Bump |
|---|---|
| Add a new service | nothing |
| Add a non-required field to an existing service | service version: |
Add an optional key to the response: schema (e.g. audio_field, timestamps_field) |
nothing — additive, backward-compatible |
| Add an optional key to a Field (e.g. section) or to Service (e.g. section_groups) | nothing — additive, backward-compatible |
| Extend an existing enum (StatusT, FieldTypeT, ResponseTypeT, CategoryT) with a non-conflicting new value, with the consumer updated in the same coordinated change | nothing — older consumers don't encounter the new value; updated consumers parse it correctly |
| Change a field's type, range, or default | service version: |
| Remove a service | service version: (sentinel: removed=true), then drop in next catalog_version bump |
| Add a new entry to the field-type vocabulary | catalog_version: |
| Add a new entry to the response-type vocabulary | catalog_version: |
| Drop a field-type or response-type | catalog_version: (BREAKING) |
| Pure prose edit (description, notes) | nothing |
Consumers should pin their tested catalog_version. When this repo
bumps it, downstreams should re-test before consuming the new shape.
Sync workflow for downstream consumers
The recommended pattern is vendor + drift-check, not fetch-at-runtime. Reasons: deployments stay reproducible; airgapped or network-flaky deploys still work; schema changes surface in PRs instead of breaking production silently.
A consumer should:
- Vendor a copy of
services.yamlin their repo (e.g. atdata/services.yaml). - Record the source SHA they synced from in a
services.yaml.lockalongside (path, source SHA, sync timestamp). - Provide a
sync_catalogscript that pulls the latest from the canonical path and updates both files. - Validate the vendored file against
services.schema.jsonon load — refuse to start if validation fails. - CI: re-validate on every commit.
The asset-engine repo at ~/development/asset_engine/ is the
reference implementation of this pattern; copy the script shape from
scripts/sync_catalog.py over there.
Known consumers
| Consumer | Vendored at | Pin policy |
|---|---|---|
asset_engine (FastAPI/HTMX UI; ~/development/asset_engine/) |
data/services.yaml + data/services.yaml.lock |
catalog_version=1 |
When you add a consumer, add a row here in the same PR that lands
the consumer. This list is what we audit when planning a
catalog_version bump.
Service authoring notes
When adding a service to the catalog:
- Source-of-truth precedence. OpenAPI is convenient but often lies
by omission (bare
stringtypes where the dispatch chain enforces a specific enum, missing required-only-in-the-handler fields, etc). The actual ground truth is, in order:- The Pydantic input model — for field shape, types, required-ness.
- The handler / pipeline code — for enum dispatches and runtime validation.
- The Gradio UI / client code (when present) — for blessed defaults and slider ranges, since the model authors picked these for human-facing UX.
- The README — least reliable; rots fastest. Every catalog change should cite which of these was read. "OpenAPI said X" is not enough on its own — past audits have caught three bugs in a single service (ace-step) that all looked correct in OpenAPI.
- The reproducibility audit at the bottom of
services.yamlMUST get a matching entry for any new service. - If a service can't produce reproducible output (no seed AND non-deterministic model), that's a bug in the service — fix it there before adding to the catalog. The asset-engine relies on reproducibility to power "regenerate" / "fork" affordances.
- If the service has a non-permissive license, populate
license_warning:with the user-facing language. The UI is required to surface it before a generation; missing it is a contract violation. - Image refs SHOULD be digest-pinned (
@sha256:...) for true reproducibility. Tag-pinned (:v1.2.3) is acceptable but vulnerable to upstream re-pushes.
When removing a service: see the versioning policy table. Removal is a two-step process; surprise removal breaks consumers' asset libraries (regenerate-from-history breaks).