feat(catalogue): a curated file of providers and models, refreshed daily

The five connections in the database were never configured by anyone — a
migration minted one per model from the adapter's own list, so the table held
five decisions nobody made. What that list actually is, is a CATALOGUE: what
you can choose from, which is not the same kind of thing as what you chose.

So it is a file. `sidecar/catalogue/shipped.ts` is the curated baseline, in
git, reviewed; `$OFFICER_ROOT/data/atelier/catalogue.json` is what a refresh
writes, and it wins where it has something to say. Data dir, not the plugin
dir — a plugin directory is a clone an update replaces, which is the rule
env.ts already states for images.

Refresh runs in the sidecar, daily, and only on boot when the file is actually
stale — this process has restarted twenty times in an afternoon and should not
hit OpenAI twenty times for it.

Two sources, unequal, and the code says so. `GET /v1/models` is authoritative
and thin: ids and nothing else, so a model matching /^(gpt-image|dall-e)/
appears the day it ships without a release. Prices come off an HTML page
because there is no pricing endpoint for any plan, and that parser is written
to give up rather than guess — a guess gets written to disk and then believed.
When it gives up it says so in the log; the durable replacement is handing the
page to a model for structured output, which Officer can already do locally.

Nothing a refresh cannot establish overwrites a curated value: fields merge
one by one, not objects wholesale, so an id-only listing cannot wipe the sizes
and prices that were written by hand. Retiring is decided by the merge, which
can tell a complete listing from a request that failed halfway — and it marks
`deprecated` rather than deleting, because the gallery labels 88 historical
rows from this list.

Per-model sizes, qualities and transparency now travel with each entry, which
is what will let the form stop offering dall-e-2 a transparent background it
cannot do. The catalogue is advisory throughout: validate.ts still refuses to
enforce per-model rules, exactly as before.
This commit is contained in:
2026-08-20 22:15:35 +01:00
parent 82ca696ccf
commit 0de38f0b13
11 changed files with 641 additions and 12 deletions
+87
View File
@@ -0,0 +1,87 @@
import { CATALOGUE_REVISION, type Catalogue } from './types';
/**
* The catalogue as it ships with the plugin — the floor a refresh builds on, and the whole of it when
* no refresh has ever run or every refresh has failed.
*
* Written by hand and reviewed in git, because none of it is fetchable: `GET /v1/models` returns ids,
* an owner and a timestamp, and says nothing about price, sizes, transparency, or whether a model is
* one you would still choose. That is the entire reason this file exists rather than a nightly fetch.
*
* The prices are USD per million tokens, from OpenAI's published pricing, and three of the five models
* have none — deliberately. `PRICING` in providers/openai.ts had the same gap and the same reason: a
* guessed price produces a number that looks authoritative and sums into a wrong total.
*/
export const SHIPPED_CATALOGUE: Catalogue = {
revision: CATALOGUE_REVISION,
providers: [
{
name: 'openai',
label: 'OpenAI',
envVar: 'OPENAI_API_KEY',
defaultModel: 'gpt-image-1',
models: [
{
id: 'gpt-image-1.5',
label: 'GPT Image 1.5',
kind: 'image',
sizes: ['auto', '1024x1024', '1536x1024', '1024x1536'],
qualities: ['auto', 'low', 'medium', 'high'],
transparency: true,
edit: true,
pricing: { unit: 'token', textInput: 5, imageInput: 8, imageOutput: 32 },
status: 'current',
source: 'shipped',
},
{
id: 'gpt-image-1',
label: 'GPT Image 1',
kind: 'image',
sizes: ['auto', '1024x1024', '1536x1024', '1024x1536'],
qualities: ['auto', 'low', 'medium', 'high'],
transparency: true,
edit: true,
pricing: { unit: 'token', textInput: 5, imageInput: 10, imageOutput: 40 },
status: 'current',
source: 'shipped',
},
{
id: 'gpt-image-1-mini',
label: 'GPT Image 1 mini',
kind: 'image',
sizes: ['auto', '1024x1024', '1536x1024', '1024x1536'],
qualities: ['auto', 'low', 'medium', 'high'],
transparency: true,
edit: true,
pricing: { unit: 'token', textInput: 2, imageInput: 2.5, imageOutput: 8 },
status: 'current',
source: 'shipped',
},
{
// No transparency and a different size set — which is exactly what the form was getting
// wrong by offering one hard-coded list of sizes and qualities for every model.
id: 'dall-e-3',
label: 'DALL·E 3',
kind: 'image',
sizes: ['1024x1024', '1792x1024', '1024x1792'],
qualities: ['standard', 'hd'],
transparency: false,
edit: false,
status: 'current',
source: 'shipped',
},
{
id: 'dall-e-2',
label: 'DALL·E 2',
kind: 'image',
sizes: ['256x256', '512x512', '1024x1024'],
qualities: ['standard'],
transparency: false,
edit: true,
status: 'current',
source: 'shipped',
},
],
},
],
};