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:
@@ -3,6 +3,7 @@ import { getProvider, getProviderByName, listProviders, resolveProviderForModel
|
||||
import type { Provider } from '../db';
|
||||
import { decrypt } from '../crypto';
|
||||
import { ADAPTERS, adapterByName } from './index';
|
||||
import { getCatalogue } from '../catalogue/store';
|
||||
import type { ImageAdapter } from './types';
|
||||
|
||||
/**
|
||||
@@ -115,13 +116,38 @@ export async function providerStatuses() {
|
||||
);
|
||||
}
|
||||
|
||||
/** The adapters a provider can be built on, for the "add provider" form. */
|
||||
export const adapterCatalogue = () =>
|
||||
ADAPTERS.map((adapter) => ({
|
||||
name: adapter.name,
|
||||
label: adapter.label,
|
||||
models: adapter.models,
|
||||
default_model: adapter.defaultModel,
|
||||
env_var: envVarFor(adapter.name),
|
||||
supports_edit: typeof adapter.edit === 'function',
|
||||
}));
|
||||
/**
|
||||
* What a new connection can be built on: the CATALOGUE, filtered to adapters that exist as code.
|
||||
*
|
||||
* The model list used to come off the adapter itself — a hard-coded array beside the HTTP client —
|
||||
* which meant adding a model was a code change and a release. It comes from the curated catalogue
|
||||
* now, refreshed daily, and the adapter keeps the one thing only it knows: whether it can edit.
|
||||
*
|
||||
* A catalogue provider with no adapter is skipped rather than offered. Listing a vendor we cannot
|
||||
* actually call is an offer that fails at the moment of use, which is worse than not being offered.
|
||||
*/
|
||||
export const adapterCatalogue = async () => {
|
||||
const catalogue = await getCatalogue();
|
||||
|
||||
return catalogue.providers.flatMap((provider) => {
|
||||
const adapter = adapterByName(provider.name);
|
||||
if (!adapter) return [];
|
||||
|
||||
// Deprecated models stay in the catalogue so old rows keep their labels; they are not offered.
|
||||
const models = provider.models.filter((model) => model.status !== 'deprecated');
|
||||
|
||||
return [
|
||||
{
|
||||
name: provider.name,
|
||||
label: provider.label,
|
||||
models: models.map((model) => model.id),
|
||||
/** The full records, for a UI that offers per-model sizes and qualities rather than one list. */
|
||||
model_details: models,
|
||||
default_model: provider.defaultModel ?? adapter.defaultModel,
|
||||
env_var: provider.envVar || envVarFor(provider.name),
|
||||
supports_edit: typeof adapter.edit === 'function',
|
||||
updated_at: catalogue.updatedAt ?? null,
|
||||
},
|
||||
];
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user