Files
esh-pfi-infrastructure/docs/asset-engine/CATALOG-CONTRACT.md
T
vh d3faeb0314 catalog-contract: add response-decomposition fields (audio_field, timestamps_field, audio_format_field)
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.
2026-05-11 14:55:15 -07:00

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.yaml in the eshpfi-management repo (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 via cd ~/development/asset_engine && uv run scripts/dump_schema.py whenever 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:

  1. Vendor a copy of services.yaml in their repo (e.g. at data/services.yaml).
  2. Record the source SHA they synced from in a services.yaml.lock alongside (path, source SHA, sync timestamp).
  3. Provide a sync_catalog script that pulls the latest from the canonical path and updates both files.
  4. Validate the vendored file against services.schema.json on load — refuse to start if validation fails.
  5. 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 of services.yaml MUST 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).