Tether
DocsGitHub

Core concepts

The output contract

Tether does not return a paragraph for your frontend to guess at. It returns a validated array of typed blocks, and guarantees you always get something renderable.

Shape

{
  "content_type": "application/vnd.tether.blocks.v1+json",
  "version": "tether.blocks.v1",
  "title": "Open tickets",
  "blocks": [
    { "type": "kpi", "id": "headline",
      "items": [
        { "label": "Open", "value": 7 },
        { "label": "Oldest", "value": 41, "intent": "negative" }
      ] },
    { "type": "text", "id": "summary", "variant": "body",
      "markdown": "Seven tickets are open. **T-4417** has been open 41 minutes." },
    { "type": "quick_replies", "id": "followups",
      "replies": [
        { "id": "r1", "label": "Show the oldest" },
        { "id": "r2", "label": "Group by server" }
      ] }
  ],
  "meta": { "period_start": "…", "period_end": "…" },
  "session_id": "0f2c…",
  "pending_actions": []
}

Every block carries a type and a stable id. The full set of types and their fields is in the block type reference.

Why not prose

Three reasons, in order of how much they will cost you if ignored.

Rendering. A number the model wrote into a sentence cannot be right-aligned, formatted to your locale, coloured by sign, or made tappable. A kpi block can.

Testing. You can assert that a question returns a table with a total column. You cannot meaningfully assert on a paragraph.

Change. Prose output means every model upgrade is a silent regression risk across your whole UI. A schema means the contract either holds or fails loudly.

Degradation

Models produce malformed JSON. The contract survives it in three steps:

  1. Validate. The payload is checked against the schema.

  2. Repair. On failure, one repair pass runs — recoverable problems like a missing wrapper, a stray code fence, or a block missing its id are corrected.

  3. Fall back. If it still does not validate, the raw text is wrapped in a single text block and returned as a valid response.

Metadata

meta carries the period the answer covers and diagnostic fields. Two response fields are worth wiring up:

session_id — echo it back on the next request to continue the conversation.

pending_actions — staged writes from this turn. Empty on a read-only turn, which is most of them.

Versioning

content_type and version are constants on every response. A future breaking change to the block schema increments them; your client can branch on the value rather than sniffing shapes.

The response normalizer also accepts the legacy vellin. version prefix from before the service was renamed, so a model echoing an older constant is not treated as malformed.