---
name: ai-model-layer
description: "Conventions for the multi-provider model layer in Next.js + AI SDK apps — how to structure the registry, customProvider aliases, available-providers detection, and model resolution so the rest of the app never touches raw model strings or provider packages. Use whenever creating, updating, or porting the src/lib/ai/ module — registry, customProvider aliases, available-providers detection, model resolution. Triggers include: 'set up models', 'add a provider', 'multi-provider', 'model registry', 'model picker', 'switch model'."
---

# The multi-provider model layer

## Goal

A single file (`registry.ts`) owns every volatile model ID string; the rest of the codebase references stable aliases (`fast`, `balanced`, `powerful`) prefixed by provider, e.g. `"openai:balanced"`. Providers are opt-in at runtime: `getAvailableProviders()` inspects environment variables and returns only the providers whose API key is present, so the app degrades gracefully when only one key is configured. Upgrading a model version (e.g. `gpt-4o` → `gpt-4.1`) is a one-line change in `registry.ts` — no other file needs to change.

## Files

- **`src/lib/ai/registry.ts`** — Creates one `customProvider` per provider with `fast`/`balanced`/`powerful` aliases mapped to concrete model IDs, then combines them into a single `createProviderRegistry` export that resolves IDs of the form `"provider:alias"`.
- **`src/lib/ai/available-providers.ts`** — Reads `process.env` at call time and returns only the `ProviderMeta` objects whose API key is present; this is the single authoritative source for which providers the current deployment supports.
- **`src/lib/ai/models.ts`** — Exports the `MODEL_CATALOG` (human-readable label + description per `provider:alias` entry) and a `getAvailableModels()` helper that filters the catalog to available providers; UI components import from here, never from `registry.ts`.
- **`src/lib/ai/get-model.ts`** — Exports `getModel(modelId?)`, the server-side resolver that calls `registry.languageModel()` and falls back to the first available provider's `balanced` alias if the requested ID fails.

## Rules

1. App code (route handlers, server actions, components) must never import `@ai-sdk/openai`, `@ai-sdk/anthropic`, or `@ai-sdk/google` directly — all model access goes through `@/lib/ai`.
2. Volatile model ID strings (e.g. `"gpt-4o"`, `"claude-sonnet-4-6"`) live **only** in `registry.ts`; every other file uses the `"provider:alias"` form.
3. The three alias names are stable and fixed: `fast`, `balanced`, `powerful` — do not add, rename, or remove them without updating every provider and all downstream UI.
4. The model picker must read its list from `getAvailableProviders()` (filtered through `getAvailableModels()`) so that providers without a configured API key are never shown.
5. The selected model is always stored and transmitted as `"<providerId>:<alias>"` (e.g. `"anthropic:balanced"`) — never as a raw model ID, a bare alias, or a display label.

## Canonical example

// src/lib/ai/registry.ts
```typescript
import { createProviderRegistry, customProvider } from "ai";
import { openai } from "@ai-sdk/openai";
import { anthropic } from "@ai-sdk/anthropic";
import { google } from "@ai-sdk/google";

// customProvider maps stable aliases to concrete model IDs.
// App code references "openai:balanced" — never "gpt-4o" directly.
// Upgrading a model version is a one-line change here, nothing else moves.

const openaiWithAliases = customProvider({
  languageModels: {
    fast: openai("gpt-4o-mini"),
    balanced: openai("gpt-4o"),
    powerful: openai("gpt-4.1"),
  },
  fallbackProvider: openai,
});

const anthropicWithAliases = customProvider({
  languageModels: {
    fast: anthropic("claude-haiku-4-5"),
    balanced: anthropic("claude-sonnet-4-6"),
    powerful: anthropic("claude-opus-4-7"),
  },
  fallbackProvider: anthropic,
});

const googleWithAliases = customProvider({
  languageModels: {
    fast: google("gemini-2.0-flash"),
    balanced: google("gemini-2.5-pro"),
  },
  fallbackProvider: google,
});

// Registry resolves IDs of the form "providerId:modelId".
// e.g. registry.languageModel("openai:balanced") → gpt-4o instance
export const registry = createProviderRegistry({
  openai: openaiWithAliases,
  anthropic: anthropicWithAliases,
  google: googleWithAliases,
});
```

// src/lib/ai/available-providers.ts
```typescript
export interface ProviderMeta {
  id: string;
  label: string;
}

// Only surfaces providers whose API key is present in the environment.
// With just OPENAI_API_KEY set the model picker shows OpenAI only — the app
// still fully works. Add more keys to unlock more providers at runtime.
export function getAvailableProviders(): ProviderMeta[] {
  const providers: ProviderMeta[] = [];
  if (process.env.OPENAI_API_KEY)
    providers.push({ id: "openai", label: "OpenAI" });
  if (process.env.ANTHROPIC_API_KEY)
    providers.push({ id: "anthropic", label: "Anthropic" });
  if (process.env.GOOGLE_GENERATIVE_AI_API_KEY)
    providers.push({ id: "google", label: "Google" });
  return providers;
}
```

// src/lib/ai/models.ts
```typescript
export type ModelAlias = "fast" | "balanced" | "powerful";

export interface ModelEntry {
  /** Full registry ID passed to getModel(), e.g. "openai:balanced" */
  id: string;
  providerId: string;
  alias: ModelAlias;
  label: string;
  description: string;
}

// Volatile model name strings are isolated to registry.ts.
// This catalog holds the human-readable metadata the UI needs.
export const MODEL_CATALOG: ModelEntry[] = [
  // OpenAI
  {
    id: "openai:fast",
    providerId: "openai",
    alias: "fast",
    label: "GPT-4o mini",
    description: "Fast · cost-efficient",
  },
  {
    id: "openai:balanced",
    providerId: "openai",
    alias: "balanced",
    label: "GPT-4o",
    description: "Smart · versatile",
  },
  {
    id: "openai:powerful",
    providerId: "openai",
    alias: "powerful",
    label: "GPT-4.1",
    description: "Most capable",
  },

  // Anthropic
  {
    id: "anthropic:fast",
    providerId: "anthropic",
    alias: "fast",
    label: "Claude Haiku 4.5",
    description: "Fast · cost-efficient",
  },
  {
    id: "anthropic:balanced",
    providerId: "anthropic",
    alias: "balanced",
    label: "Claude Sonnet 4.6",
    description: "Smart · versatile",
  },
  {
    id: "anthropic:powerful",
    providerId: "anthropic",
    alias: "powerful",
    label: "Claude Opus 4.7",
    description: "Most capable",
  },

  // Google
  {
    id: "google:fast",
    providerId: "google",
    alias: "fast",
    label: "Gemini 2.0 Flash",
    description: "Fast · cost-efficient",
  },
  {
    id: "google:balanced",
    providerId: "google",
    alias: "balanced",
    label: "Gemini 2.5 Pro",
    description: "Smart · versatile",
  },
];

/** Returns only the entries whose provider has an API key configured. */
export function getAvailableModels(
  availableProviderIds: string[],
): ModelEntry[] {
  return MODEL_CATALOG.filter((m) =>
    availableProviderIds.includes(m.providerId),
  );
}
```

// src/lib/ai/get-model.ts
```typescript
import { registry } from "./registry";
import { getAvailableProviders } from "./available-providers";

export const DEFAULT_MODEL_ID = "openai:balanced";

// The registry's languageModel() only accepts provider-prefixed template
// literals. We accept a plain string at the boundary (from client JSON) and
// cast it — runtime errors are caught and fall back to a safe default.
type RegistryModelId =
  | `openai:${string}`
  | `anthropic:${string}`
  | `google:${string}`;

/**
 * Resolves a registry model ID (e.g. "openai:balanced") to a LanguageModel.
 * Falls back to the first available provider's "balanced" alias if the
 * requested ID fails (e.g. provider key not configured).
 */
export function getModel(modelId?: string) {
  const id = (modelId ?? DEFAULT_MODEL_ID) as RegistryModelId;

  try {
    return registry.languageModel(id);
  } catch {
    const [first] = getAvailableProviders();
    if (first) {
      return registry.languageModel(`${first.id}:balanced` as RegistryModelId);
    }
    throw new Error(
      "No AI providers available. Set at least one API key in .env.local.",
    );
  }
}
```

## Common mistakes

- **Importing a provider package outside `registry.ts`** — e.g. `import { openai } from "@ai-sdk/openai"` in a route handler couples that file to a specific provider and bypasses the alias system.
- **Hardcoding a raw model ID in UI text or component props** — writing `"gpt-4o"` anywhere outside `registry.ts` means model upgrades silently break copy or comparisons.
- **Showing all providers in the model picker regardless of key presence** — always filter through `getAvailableProviders()` / `getAvailableModels()`; displaying a provider with no key leads to opaque runtime errors for the user.
- **Bumping a model ID in `registry.ts` without verifying all three aliases** — if a provider doesn't offer a `powerful`-tier model (e.g. Google currently has no `powerful` alias), either map it to the next best or omit the entry and document the gap.
- **Storing or transmitting a bare alias** (e.g. `"balanced"`) instead of the full `"provider:alias"` ID — the registry requires the provider prefix; a bare alias will throw at resolution time.
- **Calling `getAvailableProviders()` at module load time** — it reads `process.env`, which may not be populated yet during edge-runtime initialization; always call it inside a request handler or server action.

## How to apply this skill in a new project

1. **Install dependencies** — add the AI SDK core and the provider packages you intend to support:
   ```bash
   pnpm add ai @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google
   ```

2. **Create `src/lib/ai/registry.ts`** — import each provider SDK, wrap each one in `customProvider` mapping `fast`/`balanced`/`powerful` aliases to current model IDs, then export a single `registry` from `createProviderRegistry`. Only add a provider entry here if you are ready to supply its API key.

3. **Create `src/lib/ai/available-providers.ts`** — export `getAvailableProviders()` that checks each expected `process.env.*_API_KEY` variable and pushes a `{ id, label }` object only when the key is truthy.

4. **Create `src/lib/ai/models.ts`** — define `ModelAlias`, `ModelEntry`, and `MODEL_CATALOG` with one entry per `provider:alias` combination. Export `getAvailableModels(availableProviderIds)` that filters the catalog. This file imports nothing from `registry.ts`.

5. **Create `src/lib/ai/get-model.ts`** — export `getModel(modelId?: string)` that casts the string to the `RegistryModelId` union, calls `registry.languageModel(id)`, and catches errors by falling back to `getAvailableProviders()[0].id + ":balanced"`. Export a `DEFAULT_MODEL_ID` constant.

6. **Wire environment variables** — add the following to `.env.local` (and document them in `.env.example`):
   ```
   OPENAI_API_KEY=
   ANTHROPIC_API_KEY=
   GOOGLE_GENERATIVE_AI_API_KEY=
   ```
   Only the keys you populate will activate the corresponding provider.

7. **Write a unit test for `getAvailableProviders`** — stub `process.env` with `vi.stubEnv`, assert that setting only `OPENAI_API_KEY` returns a single-element array with `id: "openai"`, and that setting all three keys returns all three providers in order:
   ```typescript
   import { describe, it, expect, vi } from "vitest";
   import { getAvailableProviders } from "@/lib/ai/available-providers";

   describe("getAvailableProviders", () => {
     it("returns only providers whose key is set", () => {
       vi.stubEnv("OPENAI_API_KEY", "sk-test");
       vi.stubEnv("ANTHROPIC_API_KEY", "");
       vi.stubEnv("GOOGLE_GENERATIVE_AI_API_KEY", "");
       expect(getAvailableProviders()).toEqual([{ id: "openai", label: "OpenAI" }]);
       vi.unstubAllEnvs();
     });
   });
   ```

8. **Enforce the boundary** — add a lint rule or note in `CLAUDE.md` that `@ai-sdk/*` imports are forbidden outside `src/lib/ai/registry.ts`; this is the single enforcement point that keeps the rest of the pattern coherent.
