Docs/modules/AI Gateway

AI Gateway

NextShip includes an AI API gateway that proxies requests to AI providers (OpenAI, Anthropic, etc.) with built-in credit-based billing and usage tracking.

Features

  • Proxy requests to multiple AI providers
  • Credit-based billing per request
  • Usage logging and analytics
  • API key management for users
  • Rate limiting
  • Request/response logging

Architecture

Client Request
     ↓
API Key Validation
     ↓
Credit Balance Check
     ↓
AI Provider (OpenAI/Anthropic)
     ↓
Deduct Credits
     ↓
Log Usage
     ↓
Response to Client

Configuration

Environment Variables

# AI Providers
OPENAI_API_KEY=sk-xxx
ANTHROPIC_API_KEY=sk-ant-xxx
 
# Optional: Custom base URLs
OPENAI_BASE_URL=https://api.openai.com/v1
ANTHROPIC_BASE_URL=https://api.anthropic.com

Pricing Configuration

API pricing is managed in the admin dashboard (/api-pricing):

// src/lib/db/schema.ts
export const apiPricing = pgTable("api_pricing", {
  id: text("id").primaryKey(),
  provider: text("provider").notNull(),      // "openai", "anthropic"
  model: text("model").notNull(),            // "gpt-4", "claude-3"
  inputPricePerToken: integer("input_price"), // credits per 1K tokens
  outputPricePerToken: integer("output_price"),
  isActive: boolean("is_active").default(true),
});

API Routes

Chat Completions

// POST /api/v1/chat/completions
// Compatible with OpenAI API format
 
const response = await fetch("/api/v1/chat/completions", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer YOUR_API_KEY",
  },
  body: JSON.stringify({
    model: "gpt-4",
    messages: [
      { role: "user", content: "Hello!" }
    ],
  }),
});

Route Handler

// src/app/api/v1/chat/completions/route.ts
export async function POST(req: Request) {
  // 1. Validate API key
  const apiKey = req.headers.get("authorization")?.replace("Bearer ", "");
  const keyData = await validateApiKey(apiKey);
  if (!keyData) {
    return Response.json({ error: "Invalid API key" }, { status: 401 });
  }
 
  // 2. Check credit balance
  const balance = await getUserCredits(keyData.userId);
  if (balance <= 0) {
    return Response.json({ error: "Insufficient credits" }, { status: 402 });
  }
 
  // 3. Forward to AI provider
  const body = await req.json();
  const response = await forwardToProvider(body);
 
  // 4. Calculate and deduct credits
  const usage = response.usage;
  const cost = calculateCost(body.model, usage);
  await deductCredits(keyData.userId, cost, {
    model: body.model,
    inputTokens: usage.prompt_tokens,
    outputTokens: usage.completion_tokens,
  });
 
  // 5. Log usage
  await logApiUsage({
    userId: keyData.userId,
    apiKeyId: keyData.id,
    model: body.model,
    inputTokens: usage.prompt_tokens,
    outputTokens: usage.completion_tokens,
    creditsUsed: cost,
  });
 
  return Response.json(response);
}

API Key Management

Users can create and manage API keys in /api-keys:

// src/server/actions/api-keys.ts
 
// Create new API key
export async function createApiKey(name: string) {
  const session = await requireAuth();
  const key = `sk-${generateRandomString(48)}`;
 
  await db.insert(apiKeys).values({
    id: crypto.randomUUID(),
    userId: session.user.id,
    name,
    key: hashApiKey(key), // Store hashed
    lastUsed: null,
  });
 
  return { key }; // Return plain key only once
}
 
// List user's API keys
export async function getMyApiKeys() {
  const session = await requireAuth();
  return db.query.apiKeys.findMany({
    where: eq(apiKeys.userId, session.user.id),
    columns: {
      id: true,
      name: true,
      lastUsed: true,
      createdAt: true,
      // key is not returned (security)
    },
  });
}
 
// Delete API key
export async function deleteApiKey(keyId: string) {
  const session = await requireAuth();
  await db.delete(apiKeys).where(
    and(
      eq(apiKeys.id, keyId),
      eq(apiKeys.userId, session.user.id)
    )
  );
}

Usage Tracking

Usage Logs Schema

export const apiUsageLogs = pgTable("api_usage_logs", {
  id: text("id").primaryKey(),
  userId: text("user_id").references(() => users.id),
  apiKeyId: text("api_key_id").references(() => apiKeys.id),
  model: text("model").notNull(),
  inputTokens: integer("input_tokens"),
  outputTokens: integer("output_tokens"),
  creditsUsed: integer("credits_used"),
  createdAt: timestamp("created_at").defaultNow(),
});

Viewing Usage

Users can view their API usage in /api-usage:

// src/server/actions/api-keys.ts
export async function getMyApiUsageLogs(params: {
  page: number;
  limit: number;
  startDate?: Date;
  endDate?: Date;
}) {
  const session = await requireAuth();
 
  const logs = await db.query.apiUsageLogs.findMany({
    where: and(
      eq(apiUsageLogs.userId, session.user.id),
      // date filters...
    ),
    orderBy: desc(apiUsageLogs.createdAt),
    limit: params.limit,
    offset: (params.page - 1) * params.limit,
  });
 
  return { items: logs, total };
}

Admin Dashboard

The API pricing page (/api-pricing) allows admins to:

  • Configure pricing per model
  • View total API usage across users
  • Enable/disable specific models
  • Set rate limits

Credit Calculation

// src/lib/ai-gateway/pricing.ts
export async function calculateCost(
  model: string,
  usage: { prompt_tokens: number; completion_tokens: number }
) {
  const pricing = await getModelPricing(model);
 
  const inputCost = Math.ceil(
    (usage.prompt_tokens / 1000) * pricing.inputPricePerToken
  );
  const outputCost = Math.ceil(
    (usage.completion_tokens / 1000) * pricing.outputPricePerToken
  );
 
  return inputCost + outputCost;
}

Best Practices

  1. Hash API keys - Never store plain text API keys
  2. Rate limiting - Implement per-user rate limits
  3. Monitor usage - Set up alerts for unusual activity
  4. Graceful degradation - Handle provider outages
  5. Audit logging - Log all API requests for debugging