Model catalog
Catalog entries, pricing, and validation.
The model catalog lives as JSON per provider and is loaded into memory at startup:
src/adapters/openai/catalog.jsonsrc/adapters/google/catalog.jsonsrc/adapters/anthropic/catalog.jsonsrc/adapters/deepseek/catalog.jsonsrc/adapters/moonshot/catalog.jsonsrc/adapters/zai/catalog.jsonsrc/adapters/minimax/catalog.jsonsrc/adapters/azureopenai/catalog.jsonsrc/adapters/azurefoundry/catalog.json
Each file declares $schema: "../../../schemas/model-catalog.schema.json" for autocompletion and
validation in IDEs that support JSON Schema.
Custom models use the same contract in model_deployments.catalog_entry: the admin API receives
catalogEntry with the shape of a single models entry (for example { "operations": { ... } }).
If the model already exists in the adapter's catalog, catalogEntry is rejected to avoid two sources
of truth.
For snippets or custom payloads outside the full document, use
schemas/model-catalog-entry.schema.json; it references the same catalog entry and adds the required
rules the admin API enforces.
Base shape
{
"$schema": "../../../schemas/model-catalog.schema.json",
"schemaVersion": 1,
"provider": {
"id": "openai",
"adapterKey": "openai",
"name": "OpenAI",
"docs": ["https://developers.openai.com/api/docs/models"]
},
"models": {
"gpt-5.5": {
"operations": {
"text.generate": {
"capabilities": {
"tools": true,
"vision": true,
"reasoning": true,
"structuredOutputs": true
},
"maxInputTokens": 1050000,
"maxOutputTokens": 128000,
"reasoning": {
"kind": "openai_effort",
"levels": ["low", "medium", "high", "xhigh"],
"canDisable": true
}
}
},
"pricing": {
"inputCentsPerMTokens": 500,
"cacheReadCentsPerMTokens": 50,
"outputCentsPerMTokens": 3000
}
}
}
}Rules
modelsis an object keyed byupstreamModel; do not use arrays for the hot path.operationsis the declarative source: the presence of an operation implies support.- For custom models,
catalogEntry.operationsis required and the gateway validates per-operation minimums: text requirescapabilities.tools|vision|reasoning|structuredOutputs; image requiresoutputFormats,responseFormats, andsizesorarbitrarySize; audio transcribe requiresresponseFormats. - Embeddings are declared with
embedding.create. The profile may indicatedimensions,supportsDimensions,minDimensions,maxDimensions,encodingFormats, input limits, andsupportsTokenInput. The presence ofembedding.createenables/v1/embeddings. pricinguses the gateway's internal unit: USD cents per 1M tokens.reasoning.kindmust be compatible withadapter.reasoningKinds; it is validated at startup.openai_effortemitsreasoning_effort;openai_bodyemits a provider-specific top-level field such asthinking: {"type":"enabled"}and, where applicable, aneffortFieldlikereasoning_effort.- Optional metadata such as
name,family,lifecycle,modalities,sources, andlastVerifiedAtexists to enrich the catalog without affecting the runtime yet.
The loader is in src/catalog/jsonCatalog.ts; getCatalogEntry() indexes by provider/model and
resolves dated snapshots like gpt-5.5-2026-04-23 against their base model.
Adding catalog entries
A new model on an existing provider
The fastest contribution — no code, just JSON:
- Open the provider's catalog, e.g.
src/adapters/deepseek/catalog.json. - Add a key under
models, named by theupstreamModel(the exact id the provider expects). Fill itsoperations(see Rules),pricing, and any optional metadata. - Validate:
bun run --filter @boelabs/unified-gateway catalog:validate.
The model is then requestable by creating a deployment for it (see Creating deployments).
A new provider (with its own catalog)
Adding a provider touches four files. Missing the last one is the usual mistake — without it CI never validates your catalog.
1. Adapter — src/adapters/<provider>/index.ts. For an OpenAI-compatible API this is a few lines;
export both the adapter and a ProviderModule:
import type { ProviderModule } from "#adapters/types.ts";
import { makeOpenAIStyleAdapter } from "#adapters/openaiStyle.ts";
export const acmeAdapter = makeOpenAIStyleAdapter({
key: "acme", // must equal the catalog's provider.adapterKey
label: "Acme",
defaultBaseUrl: "https://api.acme.ai/v1",
defaultTransport: "chat_completions",
maxTokensField: "max_tokens",
});
export const acmeProvider: ProviderModule = { adapter: acmeAdapter };2. Catalog — src/adapters/<provider>/catalog.json. Start from the base shape; set
provider.adapterKey to the adapter key, and add your models.
3. Register the provider — src/adapters/index.ts: import acmeProvider and add it to
PROVIDER_REGISTRATIONS with its catalog URL:
{ provider: acmeProvider, catalogUrl: new URL("./acme/catalog.json", import.meta.url) },4. Register it for validation — scripts/validate-catalog.ts: add an entry to the catalogs
list, or catalog:validate (and CI) will skip it:
{ adapterKey: "acme", url: new URL("../src/adapters/acme/catalog.json", import.meta.url) },Then validate and run the suite:
bun run --filter @boelabs/unified-gateway catalog:validate
bun run --filter @boelabs/unified-gateway test
adapterKeymust be identical in all three places: the adapterkey,provider.adapterKeyin the catalog, and thevalidate-catalog.tsentry. The folder name usually matches too — the one exception today is Google (foldersrc/adapters/google, adapter keygoogleaistudio).
Per-model fields
Besides operations and pricing, each entry under models accepts:
| Field | Purpose |
|---|---|
name | Human-readable label. |
family | Groups variants/snapshots of one model. |
modalities | { input: [...], output: [...] } (text, image, audio). |
lifecycle / deprecated | Status (deprecated, deprecationDate); informational. |
contracts | Upstream wire(s) the model speaks, e.g. ["chat.completions"]. |
parameters | Per-parameter handling (supported / mapped / ignored) with notes. |
sources / lastVerifiedAt | Provenance for the pricing and capabilities. |
metadata | Free-form provider notes (base URLs, limits, features). |
Dated snapshots such as gpt-5.5-2026-04-23 resolve to their base model, so you normally declare only
the base id.
Embeddings
Minimal example of a custom OpenAI-compatible embeddings model:
{
"operations": {
"embedding.create": {
"dimensions": 3072,
"supportsDimensions": true,
"minDimensions": 128,
"maxDimensions": 3072,
"encodingFormats": ["float"],
"maxInputTokens": 8192,
"supportsTokenInput": false
}
},
"pricing": {
"inputCentsPerMTokens": 20
}
}Quick notes:
encodingFormatscontrolsencoding_format; if the model does not declarebase64, the gateway rejects it before reaching the upstream.- The request's
dimensionsis only accepted whensupportsDimensions: true. supportsTokenInput: falserejects pre-tokenized inputs (number[]/number[][]).- Google AI Studio declares
gemini-embedding-2andgemini-embedding-001in the catalog; the adapter translates the public contract to:embedContent/:batchEmbedContents.
To validate all catalogs:
bun run --filter @boelabs/unified-gateway catalog:validate