Vibestacks LogoVibestacks
Integrations

AI SDK & Gateway

Use Vercel AI SDK with AI Gateway for multi-provider AI features.

Use Vercel AI SDK with AI Gateway for multi-provider AI features.

Architecture Overview

lib/
├── ai.ts           # Gateway setup, getModel(), re-exports from 'ai'
├── ai-credits.ts   # Credit tracking and consumption
└── actions/ai.ts   # Server actions for non-streaming operations

hooks/
└── use-ai.ts       # React hooks for client components

app/api/
├── chat/route.ts   # Streaming chat endpoint (default for useChat)
└── ai/models/      # Available models endpoint

Configuration

All AI settings in config/app.ts:

config/app.ts
aiConfig = {
  enabled: true,
  defaultModel: "openai/gpt-4o-mini",
  models: [...],  // Allowed models
  creditCosts: { chat: 1, completion: 1, generation: 2 },
  defaults: { maxTokens: 1024, temperature: 0.7 },
}

Environment variables in .env:

.env
AI_GATEWAY_BASE_URL=https://gateway.ai.cloudflare.com/v1/...
AI_GATEWAY_API_KEY=your-api-key

Using AI in Server Code

Basic Text Generation

import { getModel, generateText } from "@/lib/ai";

const model = getModel("openai/gpt-4o");  // or use default
const result = await generateText({
  model,
  prompt: "Write a tagline for a SaaS product",
});

console.log(result.text);
console.log(result.usage);  // { inputTokens, outputTokens }

Streaming Response

import { gateway, streamText, convertToModelMessages, type UIMessage } from "@/lib/ai";

// Convert UI messages to model format
const messages: UIMessage[] = [...];
const modelMessages = await convertToModelMessages(messages);

const result = streamText({
  model: gateway("openai/gpt-4o"),
  messages: modelMessages,
  maxOutputTokens: 1024,
  temperature: 0.7,
});

// For API routes returning to useChat
return result.toUIMessageStreamResponse();

With Credit Checking

import { getModel, generateText } from "@/lib/ai";
import { canUseCredits, consumeCredits } from "@/lib/ai-credits";
import { aiConfig } from "@/config/app";

// Check credits first
const cost = aiConfig.creditCosts.completion;
if (!(await canUseCredits(userId, planName, cost))) {
  throw new Error("Insufficient AI credits");
}

const model = getModel();
const result = await generateText({ model, prompt });

// Deduct credits after success
await consumeCredits(userId, cost, "completion", {
  model: aiConfig.defaultModel,
  provider: "openai",
  promptTokens: result.usage.inputTokens,
  completionTokens: result.usage.outputTokens,
});

Using AI in Client Components

Streaming Chat (Primary Method)

Use useChat from @ai-sdk/react for streaming conversations:

components/chat.tsx
"use client";
import { useChat } from "@ai-sdk/react";

export function ChatComponent() {
  const { messages, sendMessage, status, stop, setMessages } = useChat();

  const handleSubmit = (text: string, modelId: string) => {
    sendMessage(
      { text },
      { body: { modelId } }  // Pass model selection to API
    );
  };

  return (
    <div>
      {messages.map((m) => (
        <div key={m.id}>
          <strong>{m.role}:</strong>
          {/* Messages use parts array */}
          {m.parts.map((part, i) => (
            part.type === "text" ? <span key={i}>{part.text}</span> : null
          ))}
        </div>
      ))}

      {status === "streaming" && <span>AI is typing...</span>}

      <form onSubmit={(e) => {
        e.preventDefault();
        handleSubmit(inputValue, "openai/gpt-4o");
      }}>
        {/* input handling */}
      </form>
    </div>
  );
}

Non-Streaming Generation

Use server actions for one-off generation:

components/generator.tsx
"use client";
import { useGenerateText } from "@/hooks/use-ai";

export function GeneratorComponent() {
  const { execute, status, result } = useGenerateText();

  return (
    <div>
      <button
        onClick={() => execute({ prompt: "Write a haiku about coding" })}
        disabled={status === "executing"}
      >
        {status === "executing" ? "Generating..." : "Generate"}
      </button>
      {result?.data?.text && <p>{result.data.text}</p>}
    </div>
  );
}

Checking Credits

components/credit-display.tsx
"use client";
import { useAICredits } from "@/hooks/use-ai";
import { useEffect } from "react";

export function CreditDisplay() {
  const { execute, result } = useAICredits();

  useEffect(() => {
    execute();
  }, [execute]);

  if (!result?.data) return null;

  const { used, limit, remaining, isUnlimited } = result.data;

  return (
    <div>
      {isUnlimited ? (
        <span>Unlimited credits</span>
      ) : (
        <span>{remaining} / {limit} credits remaining</span>
      )}
    </div>
  );
}

Model Selection

components/model-selector.tsx
"use client";
import { useAvailableModels } from "@/hooks/use-ai";

export function ModelSelector({ value, onChange }) {
  const { models, isLoading, error } = useAvailableModels();

  if (isLoading) return <span>Loading models...</span>;
  if (error) return <span>Failed to load models</span>;

  return (
    <select value={value} onChange={(e) => onChange(e.target.value)}>
      {models.map((m) => (
        <option key={m.id} value={m.id}>{m.name}</option>
      ))}
    </select>
  );
}

Server Actions

Server actions in lib/actions/ai.ts handle auth and credits automatically:

lib/actions/ai.ts
import { generateTextAction, chatAction, getCreditsAction } from "@/lib/actions/ai";

// Non-streaming text generation
const result = await generateTextAction({
  prompt: "Write a summary",
  systemPrompt: "You are a helpful assistant",
  model: "openai/gpt-4o",  // optional
});

// Non-streaming chat
const response = await chatAction({
  messages: [{ role: "user", content: "Hello" }],
  model: "openai/gpt-4o",
});

// Get credit status
const credits = await getCreditsAction();

API Routes

Streaming Chat Endpoint

POST /api/chat - Default endpoint for useChat hook

// Request body
{
  messages: UIMessage[],
  modelId?: string  // defaults to aiConfig.defaultModel
}

// Response: UI message stream (SSE)

Models Endpoint

GET /api/ai/models - Returns available models

// Response
{
  models: [
    { id: "openai/gpt-4o", name: "GPT-4o" },
    { id: "anthropic/claude-sonnet-4-20250514", name: "Claude Sonnet 4" },
    ...
  ],
  defaultModel: "openai/gpt-4o-mini"
}

Credit System

Credits reset monthly per user. Tracked in ai_usage table.

OperationCost
Chat message1 credit
Text completion1 credit
Structured generation2 credits

Plan limits set in stripeConfig.plans[].limits.aiCredits:

  • -1 = unlimited
  • 0 = no AI access
  • 100 = 100 credits/month

Adding New Models

  1. Add to aiConfig.models in config/app.ts:
config/app.ts
models: [
  "openai/gpt-4o",
  "anthropic/claude-sonnet-4-20250514",
  "google/gemini-1.5-pro",
  // Add new model here
  "openai/gpt-4-turbo",
]
  1. Model is immediately available via gateway

Model Format

Models use provider/model format for AI Gateway:

  • OpenAI: openai/gpt-4o, openai/gpt-4o-mini
  • Anthropic: anthropic/claude-sonnet-4-20250514, anthropic/claude-3-5-haiku-20241022
  • Google: google/gemini-1.5-pro, google/gemini-1.5-flash

Error Handling

AI errors are automatically captured to Sentry. User-facing errors:

  • 402: Insufficient credits - prompt upgrade
  • 400: Invalid model - check aiConfig.models
  • 401: Unauthorized - user not logged in
  • 500: AI provider error - retry or fallback
// Hooks handle errors automatically with toast notifications
const { execute } = useGenerateText();

// useChat also handles errors
const { error } = useChat();
if (error) {
  // Error displayed, also available for custom handling
}

Testing

Visit /ai-test for a demo chat interface that shows:

  • Streaming messages
  • Model selection
  • Error handling
  • Status indicators

For more details, see the official AI SDK documentation.