From dev-frontend-ai
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'.
How this skill is triggered — by the user, by Claude, or both
Slash command
/dev-frontend-ai:ai-model-layerThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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...
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.
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.@ai-sdk/openai, @ai-sdk/anthropic, or @ai-sdk/google directly — all model access goes through @/lib/ai."gpt-4o", "claude-sonnet-4-6") live only in registry.ts; every other file uses the "provider:alias" form.fast, balanced, powerful — do not add, rename, or remove them without updating every provider and all downstream UI.getAvailableProviders() (filtered through getAvailableModels()) so that providers without a configured API key are never shown."<providerId>:<alias>" (e.g. "anthropic:balanced") — never as a raw model ID, a bare alias, or a display label.// src/lib/ai/registry.ts
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
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
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
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.",
);
}
}
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."gpt-4o" anywhere outside registry.ts means model upgrades silently break copy or comparisons.getAvailableProviders() / getAvailableModels(); displaying a provider with no key leads to opaque runtime errors for the user.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."balanced") instead of the full "provider:alias" ID — the registry requires the provider prefix; a bare alias will throw at resolution time.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.Install dependencies — add the AI SDK core and the provider packages you intend to support:
pnpm add ai @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google
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.
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.
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.
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.
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.
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:
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();
});
});
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.
Provides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
npx claudepluginhub libattistella/dev-frontend-ai --plugin dev-frontend-ai