Skip to main content
vibestrap ships six AI providers behind a single facade. The mock provider is always on. Each real provider only registers when its API key is set, and AI_PROVIDER picks the active one. That way you can develop offline, swap providers per environment, and never ship a half-configured stack.

Prerequisites

  • A working install (pnpm dev boots).
  • One real API key — OpenRouter is the easiest if you only want to pick one (openrouter.ai).
  • Read src/ai/index.ts — it’s 80 lines and shows the whole story.

The six providers

NameSource fileOperationsNotes
mockproviders/mock.tschat, chatStream, imageDefault. Returns canned text + picsum URLs.
openrouterproviders/openai-compat.tschat, chatStreamOpenAI-shape API; routes 100+ models.
openaiproviders/openai-compat.tschat, chatStreamDirect OpenAI endpoint.
anthropicproviders/anthropic.tschat, chatStreamDistinct Messages API + SSE event types.
replicateproviders/replicate.tsimagePoll-based prediction API.
falproviders/fal.tsimageSync queue, fast for FLUX schnell.
Calling an unsupported method (e.g. image() on anthropic) throws AIUnsupportedError. The manager catches it and tries opts.fallback if you pass one.

Step-by-step: switch to a real provider

  1. Pick a provider and add its key to .env.local:
    AI_PROVIDER=openrouter
    OPENROUTER_API_KEY=sk-or-v1-...
    # OPENROUTER_BASE_URL defaults to https://openrouter.ai/api/v1
    
  2. Restart the dev server. Conditional registration runs at import time:
    if (env.OPENROUTER_API_KEY) {
      PROVIDERS.openrouter = createOpenAICompatProvider({ ... });
    }
    
  3. Make a call from a server action or route handler:
    import { chat } from '@/ai/manager';
    
    const result = await chat(
      { model: 'openai/gpt-4o-mini', messages: [{ role: 'user', content: 'hi' }] },
      { userId: ctx.user.id }
    );
    
    result is ChatResult | InsufficientCredits. Always narrow with isInsufficientCredits(result) before reading .text.

Adding a model to the price book

src/ai/pricing.ts is the single source of truth for cost. Keys are provider:model. Numbers are per-1k tokens in micro-cents (1/100 of a cent — $0.0001 = 100 micro-cents). Edit, save, done:
const TOKEN_PRICES: Record<string, ModelPrice> = {
  'openai:gpt-4o-mini': { inputPer1kMicroCents: 1500, outputPer1kMicroCents: 6000 },
  // add a new line ↓
  'openai:gpt-4o-2026-q1': { inputPer1kMicroCents: 1200, outputPer1kMicroCents: 4800 },
};
If you forget to add a row, getTokenPrice falls back to mock:any (cheap), so the call still goes through but cost reports will under-count. Search for “forgetting price book” in Observability for the fix.

Errors you can catch

import { AIProviderError, AIUnsupportedError } from '@/ai/types';
import { isInsufficientCredits } from '@/ai/manager';

try {
  const r = await chat(req, { userId, fallback: 'mock' });
  if (isInsufficientCredits(r)) return { error: 'topup', needed: r.needed };
  return { text: r.text };
} catch (err) {
  if (err instanceof AIProviderError) console.error(err.provider, err.status);
  if (err instanceof AIUnsupportedError) console.error('try a different provider');
  throw err;
}
opts.fallback is your retry knob. The manager invokes it once on AIProviderError or AIUnsupportedError — not on InsufficientCredits (refunds already handled). Use mock as a fallback in dev so demos never break.

Verify it works

  1. Set AI_PROVIDER=mock (default). Hit the chat demo at /playground/chat — you should see the “(mock provider …)” canned response.
  2. Set a real key and AI_PROVIDER=<name>. Restart. Same demo should now stream a real reply.
  3. Tail the dev server. The ai_call insert log line includes provider=<name> — confirms the active selection.
  4. psql into your dev DB and run SELECT provider, model, status FROM ai_call ORDER BY created_at DESC LIMIT 5;

Common pitfalls

  • AI_PROVIDER set but no key. Manager silently falls back to mock. Look at Object.keys(PROVIDERS) in getProvider — your provider is missing.
  • OpenRouter model id format. It’s vendor/model (e.g. openai/gpt-4o-mini), not just gpt-4o-mini. Different from the direct OpenAI provider.
  • Anthropic system messages. They live at the top level, not in messages. The provider auto-splits, but if you build a request manually, system goes separate.
  • Replicate version pinning. model is a version hash, not a friendly name. Use black-forest-labs/flux-schnell only after looking up its current version on Replicate.
  • AIUnsupportedError on image with Anthropic. Anthropic doesn’t do images. Set fallback: 'replicate' or 'fal', or branch on operation upstream.

Official docs