Skip to content

Repository files navigation

model-catalog

A tiny JS/TS library and CLI for building AI model catalogs and listing/filtering models.

Model Catalog solves your data source problem for your js/ts AI apps. Fetch models.dev once. Add custom generators. Store the snapshot anywhere. Render the model list your app wants.

Demo

Note: the UI is not part of the lib. But you can reference it on examples/* if you want to build it for your own app.

demo.mp4

Why

  • Powered by models.dev.
  • Supports custom providers/generators for models that models.dev does not cover.
  • Lists and filters models from catalog data with one core API: catalog.listModels().
  • Storage-agnostic: database, file, localStorage, API route, static JSON, generated TypeScript.
  • Frameworkless core with copyable examples instead of framework adapters.

Install

npm install model-catalog

Quick start

import { createCatalog, refreshSnapshot } from "model-catalog";

const snapshot = await refreshSnapshot();

const catalog = createCatalog(snapshot);
const models = catalog.listModels({
  includeProviders: ["anthropic"],
  query: "claude tools",
  groupBy: "providerId",
});

console.log(models.groups[0]?.models[0]?.providerLogoUrl);

refreshSnapshot is never a fast operation so make sure to persist snapshot somewhere (i.e. database, redis, a json file on a non-ephemeral deployment, indexedDB, localStorage) before using it with createCatalog.

App-owned provider filtering

The package does not model provider credentials or provider configuration. If your app only wants to show providers that the current user configured, derive those provider IDs in your app and pass them to includeProviders.

const enabledProviders = providerConfigs
  .filter((provider) => provider.isEnabled && provider.isConfigured)
  .map((provider) => provider.providerId);

const catalog = createCatalog(snapshot);
const models = catalog.listModels({
  includeProviders: enabledProviders,
  require: ["tool_call"],
  minContext: 128_000,
  excludeDeprecated: true,
});

providerConfigs can be local UI state, but in production it is often a backend query. Store encrypted provider credentials server-side and return safe metadata to the client:

const providerConfigs = await api.providerConfigs.list();
// [{ providerId: "openai", isConfigured: true, isEnabled: true }, ...]

const enabledProviders = providerConfigs
  .filter((provider) => provider.isEnabled && provider.isConfigured)
  .map((provider) => provider.providerId);

For example, the React example context accepts backend-owned configs directly:

function App() {
  const { data: providerConfigs } = useQuery(api.providerConfigs.list);

  return (
    <CatalogProvider providerConfigs={providerConfigs ?? []}>
      <ModelPicker />
    </CatalogProvider>
  );
}

Search

The built-in query option is intentionally a small substring filter, not a full-text search engine. The query is split on whitespace, lowercased, and every token must appear in the item search text.

catalog.listProviders({ query: "open" });
catalog.listModels({ query: "claude tools" });

Provider queries search provider id and name. Model queries search model/provider identity fields plus common model metadata such as name, description, family, capabilities, and modalities.

For exact identity lookup, prefer the explicit APIs:

catalog.getProvider("openai");
catalog.getModel("openai", "gpt-4o");
catalog.listProviders({ includeProviders: ["openai"] });
catalog.listModels({ includeModels: ["openai/gpt-4o"] });

For frontend-ranked, fuzzy, or indexed search, keep model-catalog as the data source and compose it with your app's search library. For instance, use something like examples/solid/src/lib/use-flex-search.ts to accomplish richer search.

Define the search index in an app-wide context so the index is created once and reused by your UI:

const [providerSearch, setProviderSearch] = createSignal("");
const providers = createMemo(() => catalog().listProviders());

const results = createFlexSearch(providers, providerSearch, {
  indexerFn: (provider) => [provider.id, provider.name].join(" "),
});

Custom generators

Generators run during refreshSnapshot() and receive a tiny context with ctx.addProvider(), ctx.addModel(), ctx.updateProvider(), and ctx.updateModel().

There are three useful patterns:

  1. Static JSON / static objects for small catalog extensions.
  2. API-backed generators that fetch from a remote endpoint and add plain provider/model records.
  3. Runtime generators that inspect the current machine, such as a local CLI. Runtime generators are Node/machine-only and should not be used in browser bundles.

Add models to an existing provider

If models.dev already has the provider and you only want to add or patch models, call ctx.addProvider() with the same provider id and only the models you care about. Existing provider data is preserved; model ids you define are added, and matching model ids are overwritten by your generator.

This is the same shape as the built-in catalog_extensions.json entry for xAI Composer 2.5.

import { defineGenerator, refreshSnapshot } from "model-catalog";

const xaiComposer = defineGenerator({
  id: "xai-composer-extension",
  kind: "static",
  generate(ctx) {
    ctx.addProvider({
      id: "xai",
      name: "xAI",
      models: [
        {
          id: "grok-composer-2.5-fast",
          name: "Composer 2.5",
          family: "grok-build",
          attachment: false,
          reasoning: false,
          reasoning_options: [],
          tool_call: true,
          structured_output: true,
          temperature: true,
          open_weights: false,
          modalities: { input: ["text", "pdf"], output: ["text"] },
          limit: { context: 256_000, output: 256_000 },
          cost: { input: 0.5, output: 2.5, cache_read: 0.2 },
        },
      ],
    });
  },
});

const snapshot = await refreshSnapshot({ generators: [xaiComposer] });

Add a completely new provider

For a provider that is not in models.dev, add the provider record and its models directly.

import { defineGenerator, refreshSnapshot } from "model-catalog";

const companyGateway = defineGenerator({
  id: "company-gateway",
  kind: "static",
  generate(ctx) {
    ctx.addProvider({
      id: "company",
      name: "Company Gateway",
      logoUrl: "https://example.com/company.svg",
      models: [
        {
          id: "fast",
          name: "Fast",
          attachment: false,
          reasoning: true,
          reasoning_options: [
            { type: "effort", values: ["low", "medium", "high"] },
          ],
          tool_call: true,
          structured_output: true,
          temperature: true,
          open_weights: false,
          modalities: { input: ["text"], output: ["text"] },
          limit: { context: 128_000 },
        },
      ],
    });
  },
});

const snapshot = await refreshSnapshot({ generators: [companyGateway] });

API-backed generator

For providers with a remote model endpoint, put the fetch/parsing logic in your generator. model-catalog does not need to know the provider's API URL shape; your generator just converts the response into provider/model records.

import { defineGenerator } from "model-catalog";

const remoteModels = defineGenerator({
  id: "remote-company-models",
  kind: "api",
  async generate(ctx) {
    const response = await ctx.fetch("https://api.company.test/v1/models");
    const payload = await response.json();

    ctx.addProvider({
      id: "company",
      name: "Company",
      api: "https://api.company.test/v1",
      npm: "@ai-sdk/openai-compatible",
      models: payload.data.map((model: { id: string; name?: string }) => ({
        id: model.id,
        name: model.name ?? model.id,
        tool_call: true,
        temperature: true,
        modalities: { input: ["text"], output: ["text"] },
      })),
    });
  },
});

Built-in generators

refreshSnapshot() already has the baseline catalog by default:

  • models.dev
  • catalog_extensions.json, package-maintained patches for things models.dev does not cover yet

There are also a few opt-in built-in generators:

import {
  commandCodeGenerator,
  ollamaCliGenerator,
  refreshSnapshot,
} from "model-catalog";

const snapshot = await refreshSnapshot({
  generators: [
    commandCodeGenerator(), // remote API-backed generator; opt-in latency
    ollamaCliGenerator(), // local CLI generator; opt-in machine state
  ],
});
  • commandCodeGenerator - for commandcode.ai since they're pretty much ignoring this models.dev request and opencode request.
  • ollamaCliGenerator() - runs ollama ls to get a personalized model list based on the user's config. It only works in Node.js on machines where the Ollama CLI is installed. Never run it in the browser; browser code cannot execute local CLIs.

CLI

model-catalog refresh --out ./model-catalog.json
model-catalog generate --out ./src/model-catalog.generated.ts
model-catalog inspect ./model-catalog.json
model-catalog list ./model-catalog.json --provider anthropic --query claude --tools

Examples

The core package does not export UI components. A self-contained, compile-ready Solid example lives at:

examples/solid/chat-model-selector.tsx

It shows how to achieve a great model-selector UX by consuming catalog.listModels()

What this library is not

  • A chat/completions SDK.
  • A replacement for Vercel AI SDK or provider SDKs.
  • Provider credential flows or secret storage.
  • A database/storage adapter layer.
  • A required model-selector UI abstraction.
  • A replacement for models.dev. It is powered by it.

License

MIT

About

🤖 A tiny JS/TS library and CLI for building AI model catalogs and listing

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages