Provider catalogue¶
The catalogue describes known providers in YAML: where each lives, which behaviour plugin talks to it, which region it serves from, what it retains, and optional dated price hints. It has two layers:
- the built-in catalogue, shipped read-only in the image, and
- an operator directory, mounted read-only into the gateway, whose files override built-in entries field by field and may add providers of their own.
So you can fix one price, record a retention policy your contract gives you, or add a provider without waiting for a release, and upgrades still bring new built-in entries. The catalogue is read once, at startup: a change applies when the gateway restarts.
What it is, and what it is not¶
The catalogue holds no credential and creates no provider. A provider row (its encrypted API key, whether it is active, the models and grants behind it) is still made by an administrator in the console. The catalogue supplies what is true of the kind of provider.
The two are related by a link, providers.catalogue_id:
- Creating a provider in the console, you may pick a catalogue entry. The plugin, endpoint and model-name prefix are pre-filled from it (and you may change them); the entry id is stored on the row.
- Nothing else is copied into the database. Region and retention are read through the link each time they are needed, so correcting a file and restarting corrects every provider linked to it at once, with nothing stale left in a table.
- An existing provider can be linked or unlinked in its Edit dialog. Rows created before the catalogue existed are not linked automatically: guessing from the plugin name would attach region and retention claims nobody made.
- If a row names an entry the catalogue no longer has (you removed your own), the row keeps working, the Providers page flags it, and its region and retention count as unknown.
The Providers page lists every entry with its endpoint, region, retention and
price-hint count, and for each value says where it came from: built-in, or
override with the file that set it. GET /api/admin/provider-catalogue
returns the same, with the origin of every field.
The operator directory¶
| Setting | GATEWAY_PROVIDERS_DIR |
| Default (unset) | /etc/pystino/providers.d, used if it exists |
| If set | must exist, or the gateway refuses to start |
| Files read | *.yaml and *.yml, in name order; other files are ignored |
In deploy/compose.yaml the host directory ./providers.d (or
PYSTINO_PROVIDERS_DIR) is mounted read-only at /etc/pystino/providers.d, so
the default applies and nothing needs setting. An empty directory is fine.
After editing a file: docker compose restart gateway.
File format¶
Each file has one top-level key, providers, a list of entries:
providers:
- id: mistral # required: the key the layers merge on
name: Mistral AI # display name
description: Hosted in the EU. # optional
plugin: mistral # a provider plugin (see /api/admin/provider-plugins)
base_url: https://api.eu.mistral.ai/v1 # optional; the plugin's default if omitted
prefix: "" # optional model-name prefix
region: eu # lower-case tag: eu, us, eu-west, global, various, not-published
region_note: Hosted in the EU by default; a US endpoint is optional.
retention:
status: published # published | various (a router) | not_published
zero_data_retention: false # ZDR in force by default; leave out if unknown
zero_data_retention_available: true # ZDR can be had (request, contract, setting)
retention_days: 30
trains_on_customer_data: false
note: Per our DPA of 2026-03-01.
sources: # where the facts were read, and when
- url: https://example.org/dpa
retrieved: 2026-10-23
covers: retention
prices: # dated hints, per model id as the provider names it
mistral-large-latest:
- effective_from: 2026-01-01
input_per_mtok: "2.00" # per million tokens; quote decimals
output_per_mtok: "6.00"
currency: EUR
| Field | Notes |
|---|---|
id |
Required. Lower-case letters, digits, ., _, -. |
name |
Required once merged (built-in or override must set it). |
plugin |
Required once merged; must be an installed plugin. |
base_url |
http(s) with a host, no credentials in the URL. Omit to use the plugin's own default, where it has one. |
region |
A free-form lower-case tag. Routing rules compare tags, so be consistent. Three tags say how much is known rather than where: global (anywhere, no commitment), various (a router: the routed upstream decides) and not-published (the provider does not say). various and not-published reach routing as unknown, never as a place. |
region_note |
Plain words: is the region selectable, is there an EU option, what is not covered. |
retention.status |
published, various (a router) or not_published. Required of every built-in entry, so none ships blank. |
retention.zero_data_retention_available |
ZDR can be obtained. Unlike zero_data_retention, which is whether it is in force by default. |
sources |
A list of url (http or https), retrieved (the day it was read) and optional covers. An override replaces the whole list. The Providers page links them. |
retention.* |
Absent means unknown, which is not the same as "no". |
prices |
effective_from is a date. Give input_per_mtok and output_per_mtok, or per_image / per_page; cache_read_per_mtok and cache_write_per_mtok are optional. |
Price hints are used by model discovery and import when "fill missing
prices" is ticked: for a model the provider lists and publishes no price for, the
latest hint already in force is used before the community file, and the price
row is recorded with the source catalogue_hint. A price the provider publishes
is never replaced. A provider whose plugin has no other source of prices
(anthropic) has its hints applied without the tick, and the filled model also
takes its kind, modalities, features and limits from the listing.
Merging¶
Entries are merged by provider id, and a file later in the name order wins over an earlier one (built-in first, then the operator directory):
- every scalar is overridden individually: an override that names only
regionleaves the name, endpoint and the rest alone; retentionmerges per sub-field;pricesmerge per model andeffective_fromdate, and per rate within a dated hint, so fixing one rate leaves its sibling rates and the other dates;- an explicit
nullclears a value (region: null); leaving the key out keeps the lower layer's value; - a new id adds a provider; built-in entries you do not mention are kept.
An override for an existing id therefore only needs its id and what it changes:
# providers.d/local.yaml
providers:
# The contract with Mistral gives us zero retention.
- id: mistral
retention:
zero_data_retention: true
retention_days: 0
note: Zero-data-retention addendum signed 2026-05-12.
# A price hint: both rates are needed when no lower layer has this one yet.
# Once a hint exists (built-in, or an earlier file), a later file may name
# just the rate it fixes and keep the rest of that hint.
- id: nebius
prices:
meta-llama/Llama-3.3-70B-Instruct:
- effective_from: 2026-06-01
input_per_mtok: "0.13"
output_per_mtok: "0.40"
# A provider of our own.
- id: in-house
name: In-house vLLM
plugin: generic
base_url: http://vllm.internal:8000/v1
region: eu
retention:
zero_data_retention: true
Validation¶
The files are validated when the gateway starts. A bad file stops the gateway, and the message (also logged at CRITICAL) names the file and the field, for every problem at once:
override typo.yaml: providers[0].regoin: Extra inputs are not permitted (unknown key)
provider 'fresh' (from override new.yaml): plugin: required, and no layer sets it
- Unknown keys are refused, because a typo that is ignored is a residency rule that quietly does not apply.
- Types are strict:
zero_data_retention: "yes"is an error, not a guess. - No secrets. A field named like a secret (
api_key,secret,password,token,credential,authorization,bearer, ...), or a value that looks like a key (sk-...), or credentials inside a URL, is refused. Keys live encrypted in the database and are entered in the console. - After merging, each entry needs a
nameand an installedplugin.
Region and retention in routing¶
Routing selection rules (SelectionRule, see Model aliases)
see each candidate's catalogue facts as candidate.region,
candidate.zero_data_retention and candidate.retention_days (and the full
candidate.facts). None means unknown: a rule that excludes on a fact decides
for itself what an unknown provider means. The built-in rules do not use them;
residency and zero-data-retention rules are a later step.
The built-in entries¶
OpenAI, Anthropic, Mistral, Nebius, Melious, Tensorix, Cortecs and OpenRouter ship
built in. They state a region only where the provider commits to one (Mistral,
Melious and Cortecs: eu) and no retention claims at all: those are for you to record
from your own contract, in an override.
Built-in facts: what they claim¶
The built-in entries record only what each provider states on its own pages
(documentation, privacy policy, DPA, trust centre), with the URL and the day it
was read under sources. Where a provider does not say, the value is left
empty or marked not-published / not_published; nothing is inferred. Routers
(Cortecs, OpenRouter) say various for both location and retention, because
the answer is the routed upstream's, and keep the router's own policy in the
notes. Facts change: re-read the sources before relying on one for compliance,
and correct a value with an override file until a release carries the fix.
When a built-in endpoint changes¶
A provider row keeps the endpoint it was created with: the catalogue
pre-fills base_url at creation and nothing is copied afterwards, so an
upgrade that moves a built-in entry (Nebius to the Token Factory host, Mistral
to the EU endpoint) does not rewrite existing rows. The Providers page marks
a linked row whose endpoint differs from its entry's with "catalogue endpoint
differs" and a Use catalogue endpoint button; or edit the provider and set
the endpoint by hand. Region and retention, which are read through the link,
follow the new entry at once, so check that the row's endpoint matches what the
entry now claims.
Melious¶
Melious (https://api.melious.ai/v1) is a vendor entry on the OpenAI-compatible
protocol, like Nebius: its provider type is the protocol, and the melious
plugin carries what is particular to it.
- Catalogue. Discovery asks
GET /v1/models?include_meta=true, where each model carries its own type, modalities, context length, capabilities and EUR price (per million tokens, per image, per minute). Kinds, limits and prices therefore come from Melious itself and there is no price table to go stale. The reranker (type: "reranker", priced per million input tokens) is kind rerank; a type added later is left out until it has been measured. - Reranking is
POST /v1/rerankon both sides, and the gateway normalises the replies (Reranking): Melious reportsusage: nullwith the token count inmeta.billed_units, and returns the documents unless told not to. - Decision models (
clef,clef-flash) exist upstream on/v1/systemoneonly. The gateway serves both its routes:/v1/systemonepasses through, and/v1/decisionswith theinputform (predicate, choice and score questions) is translated to System One and the answer translated back, so a client written for the Cortecs shape (Cerea's reviewer) works unchanged. A predicate is anoulquestion;choicesandlevelsbecomecriteria. Onlymodel, the text and the questions are sent (preferencehas no Melious counterpart), and a question type outside predicate, choice and score is a 400 on/v1/decisionsnaming it. - Billing. The gateway bills from the configured prices (a reranker by the
tokens in
meta). Melious reports euro credits besideusageinbilling_cost; the gateway does not read it. - Region.
eu: Melious says every request is processed inside the EU, with no fallback outside it, and that it does not log prompt or response content or train on traffic.scripts/check_melious_live.pyre-checks each kind, and the translation, against a real key.