asset_engine consumer needed to render kokoro-captioned, whose wire
shape is a JSON envelope carrying base64-encoded audio plus a
structured timestamps array. Modeling it as response.type=json
would force either a per-service-id renderer (forbidden by
brief §1.7) or extending the closed response-type vocabulary
(forbidden by brief §2.2 without a coordinated bump).
Resolution (per althing thread 01KRCF4W66X3): keep response.type
closed at the existing six values and decompose at the response
*field* level instead — the same flexibility seam already used by
mime / mime_from_field / output_field. Adds three optional keys:
- audio_field: JSON key holding base64-encoded audio bytes
- audio_format_field: JSON key holding the decoded audio MIME
- timestamps_field: JSON key holding a structured timestamps array
(independent of type, declared by any service emitting time-
aligned markers)
Validators in CatalogResponse enforce sane combinations:
- audio_field requires response.type=audio
- audio_field forbids mime_from_field
- audio_format_field requires audio_field
This is additive and backward-compatible — no catalog_version bump,
existing services parse unchanged. CATALOG-CONTRACT.md updated with
the new rows in the response-field table and a versioning-policy
row codifying that adding optional keys to response: doesn't bump.
kokoro-captioned re-shaped to use the new schema:
response:
type: audio
audio_field: audio
audio_format_field: audio_format
timestamps_field: timestamps
And marked status: experimental until the asset_engine consumer's
audio-with-timestamps renderer ships.
JSON Schema regenerated to reflect the new Pydantic shape.
Pydantic-model side of this change lives in the asset_engine repo
at src/asset_engine/catalog.py — committed there separately.
9.4 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 | no | default: ready |
host |
string (host alias) | yes | which fleet host serves this |
endpoint |
URL | yes | absolute URL of the inference endpoint |
method |
string | yes | HTTP method (POST common) |
content_type |
MIME type | yes | request body type |
model.id |
string | yes | upstream model id (HF or other) |
model.revision |
string | null | no | SHA when known |
model.image |
string | yes | container image ref this is hosted from |
fields |
list[Field] | no | request parameters; empty for catalog-deferred |
response.type |
enum: audio, image, video, text, json, file | yes | renderer dispatch (closed vocabulary) |
response.mime |
string | no | static response MIME (raw-bytes wire) |
response.mime_from_field |
string | no | name of a field whose value determines the MIME (raw-bytes wire) |
response.output_field |
string | no | for text/json responses, JSON key holding the asset |
response.audio_field |
string | no | for type: audio with JSON-envelope wire, JSON key holding base64-encoded audio bytes |
response.audio_format_field |
string | no | for type: audio with JSON-envelope wire, JSON key holding the decoded audio MIME (e.g. "audio/wav") |
response.timestamps_field |
string | no | JSON key holding a structured timestamps array — independent of type, declared by any service that emits time-aligned markers alongside its primary output |
reproducibility.seedable |
bool | yes | does the endpoint accept a seed? |
reproducibility.deterministic |
bool | yes | same params → same bytes? |
reproducibility.notes |
string | no | gotchas |
estimated_latency.cold_start_s |
int | float | no | informational only |
estimated_latency.warm_per_unit |
string | no | informational only |
license |
string | yes | upstream license |
license_warning |
string | no | non-empty → consumer MUST surface a warning before use |
notes |
string | no | freeform |
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 |
| 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:
- The Pydantic model in code is the ground truth for
fields:. Read the actual server code, not the README; READMEs rot. The reproducibility audit at the bottom ofservices.yamlMUST get a matching entry. - 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).