Unified Gateway

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.json
  • src/adapters/google/catalog.json
  • src/adapters/anthropic/catalog.json
  • src/adapters/deepseek/catalog.json
  • src/adapters/moonshot/catalog.json
  • src/adapters/zai/catalog.json
  • src/adapters/minimax/catalog.json
  • src/adapters/azureopenai/catalog.json
  • src/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

  • models is an object keyed by upstreamModel; do not use arrays for the hot path.
  • operations is the declarative source: the presence of an operation implies support.
  • For custom models, catalogEntry.operations is required and the gateway validates per-operation minimums: text requires capabilities.tools|vision|reasoning|structuredOutputs; image requires outputFormats, responseFormats, and sizes or arbitrarySize; audio transcribe requires responseFormats.
  • Embeddings are declared with embedding.create. The profile may indicate dimensions, supportsDimensions, minDimensions, maxDimensions, encodingFormats, input limits, and supportsTokenInput. The presence of embedding.create enables /v1/embeddings.
  • pricing uses the gateway's internal unit: USD cents per 1M tokens.
  • reasoning.kind must be compatible with adapter.reasoningKinds; it is validated at startup.
  • openai_effort emits reasoning_effort; openai_body emits a provider-specific top-level field such as thinking: {"type":"enabled"} and, where applicable, an effortField like reasoning_effort.
  • Optional metadata such as name, family, lifecycle, modalities, sources, and lastVerifiedAt exists 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:

  1. Open the provider's catalog, e.g. src/adapters/deepseek/catalog.json.
  2. Add a key under models, named by the upstreamModel (the exact id the provider expects). Fill its operations (see Rules), pricing, and any optional metadata.
  3. 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. Adaptersrc/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. Catalogsrc/adapters/<provider>/catalog.json. Start from the base shape; set provider.adapterKey to the adapter key, and add your models.

3. Register the providersrc/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 validationscripts/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

adapterKey must be identical in all three places: the adapter key, provider.adapterKey in the catalog, and the validate-catalog.ts entry. The folder name usually matches too — the one exception today is Google (folder src/adapters/google, adapter key googleaistudio).

Per-model fields

Besides operations and pricing, each entry under models accepts:

FieldPurpose
nameHuman-readable label.
familyGroups variants/snapshots of one model.
modalities{ input: [...], output: [...] } (text, image, audio).
lifecycle / deprecatedStatus (deprecated, deprecationDate); informational.
contractsUpstream wire(s) the model speaks, e.g. ["chat.completions"].
parametersPer-parameter handling (supported / mapped / ignored) with notes.
sources / lastVerifiedAtProvenance for the pricing and capabilities.
metadataFree-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:

  • encodingFormats controls encoding_format; if the model does not declare base64, the gateway rejects it before reaching the upstream.
  • The request's dimensions is only accepted when supportsDimensions: true.
  • supportsTokenInput: false rejects pre-tokenized inputs (number[]/number[][]).
  • Google AI Studio declares gemini-embedding-2 and gemini-embedding-001 in the catalog; the adapter translates the public contract to :embedContent/:batchEmbedContents.

To validate all catalogs:

bun run --filter @boelabs/unified-gateway catalog:validate

On this page