AI 网关
NextShip 内置了一个 AI API 网关,可以代理请求到 AI 服务商(OpenAI、Anthropic 等),并支持基于积分的计费和用量追踪。
功能特性
- 代理请求到多个 AI 服务商
- 按请求的积分计费
- 用量日志和分析
- 用户 API 密钥管理
- 速率限制
- 请求/响应日志记录
架构
客户端请求
↓
API 密钥验证
↓
积分余额检查
↓
AI 服务商 (OpenAI/Anthropic)
↓
扣除积分
↓
记录用量
↓
返回响应给客户端
配置
环境变量
# AI 服务商
OPENAI_API_KEY=sk-xxx
ANTHROPIC_API_KEY=sk-ant-xxx
# 可选:自定义基础 URL
OPENAI_BASE_URL=https://api.openai.com/v1
ANTHROPIC_BASE_URL=https://api.anthropic.com定价配置
API 定价在管理后台 (/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"), // 每 1K token 的积分数
outputPricePerToken: integer("output_price"),
isActive: boolean("is_active").default(true),
});API 路由
聊天补全
// POST /api/v1/chat/completions
// 兼容 OpenAI API 格式
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!" }
],
}),
});路由处理器
// src/app/api/v1/chat/completions/route.ts
export async function POST(req: Request) {
// 1. 验证 API 密钥
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. 检查积分余额
const balance = await getUserCredits(keyData.userId);
if (balance <= 0) {
return Response.json({ error: "Insufficient credits" }, { status: 402 });
}
// 3. 转发到 AI 服务商
const body = await req.json();
const response = await forwardToProvider(body);
// 4. 计算并扣除积分
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. 记录用量
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 密钥管理
用户可以在 /api-keys 页面创建和管理 API 密钥:
// src/server/actions/api-keys.ts
// 创建新的 API 密钥
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), // 存储哈希值
lastUsed: null,
});
return { key }; // 明文密钥只返回一次
}
// 列出用户的 API 密钥
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(安全考虑)
},
});
}
// 删除 API 密钥
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)
)
);
}用量追踪
用量日志 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(),
});查看用量
用户可以在 /api-usage 页面查看 API 用量:
// 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),
// 日期过滤...
),
orderBy: desc(apiUsageLogs.createdAt),
limit: params.limit,
offset: (params.page - 1) * params.limit,
});
return { items: logs, total };
}管理后台
API 定价页面 (/api-pricing) 允许管理员:
- 配置每个模型的定价
- 查看所有用户的 API 用量汇总
- 启用/禁用特定模型
- 设置速率限制
积分计算
// 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;
}最佳实践
- 对 API 密钥进行哈希 - 永远不要存储明文 API 密钥
- 速率限制 - 实现每用户的速率限制
- 监控用量 - 为异常活动设置告警
- 优雅降级 - 处理服务商故障
- 审计日志 - 记录所有 API 请求以便调试