Add subscribe admin

This commit is contained in:
songtianlun 2025-08-28 00:00:53 +08:00
parent ada862704b
commit 1d6b1ae0e1
9 changed files with 554 additions and 75 deletions

View File

@ -58,6 +58,7 @@ model SubscriptionPlan {
stripePriceId String? @unique // Stripe 价格 ID stripePriceId String? @unique // Stripe 价格 ID
isActive Boolean @default(true) // 是否激活 isActive Boolean @default(true) // 是否激活
sortOrder Int @default(0) // 排序顺序 sortOrder Int @default(0) // 排序顺序
costMultiplier Float @default(1.0) // 费用倍率默认1倍
// 权益配置 (JSON 格式存储) // 权益配置 (JSON 格式存储)
features Json // 功能特性配置 features Json // 功能特性配置
@ -213,11 +214,11 @@ model Subscription {
@@map("subscriptions") @@map("subscriptions")
} }
// AI 模型配置表 - 每个模型记录直接关联一个套餐 // AI 模型配置表 - 所有模型默认可用于所有套餐
model Model { model Model {
id String @id @default(cuid()) id String @id @default(cuid())
subscriptionPlanId String // 关联的套餐 ID subscriptionPlanId String @default("free") // 默认关联到 free 套餐
modelId String // OpenRouter 的模型 ID如 "openai/gpt-4" modelId String @unique // 全局唯一的模型 ID如 "openai/gpt-4"
name String // 显示名称,如 "GPT-4" name String // 显示名称,如 "GPT-4"
provider String // 提供商,如 "OpenAI" provider String // 提供商,如 "OpenAI"
serviceProvider String @default("openrouter") // 服务提供者,如 "openrouter", "replicate" serviceProvider String @default("openrouter") // 服务提供者,如 "openrouter", "replicate"
@ -235,12 +236,8 @@ model Model {
// 关联关系 // 关联关系
subscriptionPlan SubscriptionPlan @relation(fields: [subscriptionPlanId], references: [id], onDelete: Cascade) subscriptionPlan SubscriptionPlan @relation(fields: [subscriptionPlanId], references: [id], onDelete: Cascade)
simulatorRuns SimulatorRun[]
// 关联关系
simulatorRuns SimulatorRun[]
// 同一个套餐内不能有相同的模型ID
@@unique([subscriptionPlanId, modelId])
@@map("models") @@map("models")
} }

View File

@ -173,6 +173,25 @@ export default function AdminDashboard() {
</div> </div>
</Link> </Link>
<Link
href="/admin/plans"
className="group block p-3 lg:p-4 rounded-lg border border-border hover:border-primary/30 hover:bg-accent/50 transition-all duration-200"
>
<div className="flex items-center gap-3">
<div className="p-2 rounded-md bg-orange-50 dark:bg-orange-900/30 group-hover:bg-orange-100 dark:group-hover:bg-orange-900/50 transition-colors">
<Users className="h-4 w-4 text-orange-600" />
</div>
<div className="min-w-0 flex-1">
<div className="font-medium text-foreground group-hover:text-orange-600 transition-colors">
Subscription Plans
</div>
<div className="text-sm text-muted-foreground mt-0.5">
Manage subscription plans and cost multipliers
</div>
</div>
</div>
</Link>
<Link <Link
href="/debug/cache" href="/debug/cache"
className="group block p-3 lg:p-4 rounded-lg border border-border hover:border-primary/30 hover:bg-accent/50 transition-all duration-200" className="group block p-3 lg:p-4 rounded-lg border border-border hover:border-primary/30 hover:bg-accent/50 transition-all duration-200"

View File

@ -0,0 +1,461 @@
'use client'
import { useState, useEffect } from 'react'
import { useAuthUser } from '@/hooks/useAuthUser'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import { Trash2, Edit, Plus, Save, X } from 'lucide-react'
interface SubscriptionPlan {
id: string
name: string
displayName: string
description?: string
price: number
currency: string
interval: string
stripePriceId?: string
isActive: boolean
sortOrder: number
costMultiplier: number
features: Record<string, unknown>
limits: Record<string, unknown>
createdAt: string
updatedAt: string
_count?: {
users: number
subscriptions: number
models: number
}
}
export default function AdminPlansPage() {
const { userData, loading: authLoading } = useAuthUser()
const [plans, setPlans] = useState<SubscriptionPlan[]>([])
const [loading, setLoading] = useState(true)
const [editingPlan, setEditingPlan] = useState<SubscriptionPlan | null>(null)
const [isCreating, setIsCreating] = useState(false)
const [formData, setFormData] = useState({
id: '',
name: '',
displayName: '',
description: '',
price: 0,
currency: 'usd',
interval: 'month',
stripePriceId: '',
isActive: true,
sortOrder: 0,
costMultiplier: 1.0,
features: '{}',
limits: '{}'
})
useEffect(() => {
// AdminLayout已经处理了权限验证这里只需要在认证完成后获取数据
if (!authLoading && userData && userData.isAdmin) {
fetchPlans()
}
}, [userData, authLoading])
const fetchPlans = async () => {
try {
const response = await fetch('/api/admin/subscription-plans')
if (!response.ok) {
throw new Error('Failed to fetch plans')
}
const data = await response.json()
setPlans(data.plans || [])
} catch (error) {
alert('Failed to fetch subscription plans')
} finally {
setLoading(false)
}
}
const resetForm = () => {
setFormData({
id: '',
name: '',
displayName: '',
description: '',
price: 0,
currency: 'usd',
interval: 'month',
stripePriceId: '',
isActive: true,
sortOrder: 0,
costMultiplier: 1.0,
features: '{}',
limits: '{}'
})
}
const handleEdit = (plan: SubscriptionPlan) => {
setEditingPlan(plan)
setFormData({
id: plan.id,
name: plan.name,
displayName: plan.displayName,
description: plan.description || '',
price: plan.price,
currency: plan.currency,
interval: plan.interval,
stripePriceId: plan.stripePriceId || '',
isActive: plan.isActive,
sortOrder: plan.sortOrder,
costMultiplier: plan.costMultiplier,
features: JSON.stringify(plan.features, null, 2),
limits: JSON.stringify(plan.limits, null, 2)
})
setIsCreating(false)
}
const handleCreate = () => {
setIsCreating(true)
setEditingPlan(null)
resetForm()
}
const handleCancel = () => {
setEditingPlan(null)
setIsCreating(false)
resetForm()
}
const handleSave = async () => {
try {
// 验证 JSON 格式
let features, limits
try {
features = JSON.parse(formData.features)
limits = JSON.parse(formData.limits)
} catch {
alert('Invalid JSON format in features or limits')
return
}
const planData = {
...formData,
features,
limits
}
const url = isCreating
? '/api/admin/subscription-plans'
: `/api/admin/subscription-plans?id=${editingPlan?.id}`
const method = isCreating ? 'POST' : 'PUT'
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(planData)
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to save plan')
}
alert(isCreating ? 'Plan created successfully' : 'Plan updated successfully')
fetchPlans()
handleCancel()
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to save plan')
}
}
const handleDelete = async (planId: string) => {
if (!confirm('Are you sure you want to delete this plan?')) {
return
}
try {
const response = await fetch(`/api/admin/subscription-plans?id=${planId}`, {
method: 'DELETE'
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.error || 'Failed to delete plan')
}
alert('Plan deleted successfully')
fetchPlans()
} catch (error) {
alert(error instanceof Error ? error.message : 'Failed to delete plan')
}
}
// AdminLayout已经处理了认证和权限检查
if (authLoading || loading) {
return (
<div className="container mx-auto py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto"></div>
</div>
)
}
// 如果没有权限AdminLayout会重定向这里不需要额外处理
if (!userData || !userData.isAdmin) {
return null
}
return (
<div className="container mx-auto py-8 space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold tracking-tight">Subscription Plans</h1>
<p className="text-muted-foreground">Manage subscription plans and pricing</p>
</div>
<Button onClick={handleCreate} className="flex items-center gap-2">
<Plus className="h-4 w-4" />
Create Plan
</Button>
</div>
{/* Plan Editor Form */}
{(isCreating || editingPlan) && (
<Card>
<CardHeader>
<CardTitle>{isCreating ? 'Create New Plan' : 'Edit Plan'}</CardTitle>
<CardDescription>
{isCreating ? 'Create a new subscription plan' : 'Update subscription plan details'}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="id">Plan ID</Label>
<Input
id="id"
value={formData.id}
onChange={(e) => setFormData(prev => ({ ...prev, id: e.target.value }))}
placeholder="e.g., free, pro, premium"
disabled={!isCreating}
/>
</div>
<div className="space-y-2">
<Label htmlFor="name">Internal Name</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="Internal plan name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="displayName">Display Name</Label>
<Input
id="displayName"
value={formData.displayName}
onChange={(e) => setFormData(prev => ({ ...prev, displayName: e.target.value }))}
placeholder="User-facing plan name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="price">Price</Label>
<Input
id="price"
type="number"
step="0.01"
value={formData.price}
onChange={(e) => setFormData(prev => ({ ...prev, price: parseFloat(e.target.value) || 0 }))}
placeholder="0.00"
/>
</div>
<div className="space-y-2">
<Label htmlFor="currency">Currency</Label>
<Input
id="currency"
value={formData.currency}
onChange={(e) => setFormData(prev => ({ ...prev, currency: e.target.value }))}
placeholder="usd"
/>
</div>
<div className="space-y-2">
<Label htmlFor="interval">Billing Interval</Label>
<Input
id="interval"
value={formData.interval}
onChange={(e) => setFormData(prev => ({ ...prev, interval: e.target.value }))}
placeholder="month, year"
/>
</div>
<div className="space-y-2">
<Label htmlFor="costMultiplier">Cost Multiplier</Label>
<Input
id="costMultiplier"
type="number"
step="0.1"
value={formData.costMultiplier}
onChange={(e) => setFormData(prev => ({ ...prev, costMultiplier: parseFloat(e.target.value) || 1.0 }))}
placeholder="1.0"
/>
<p className="text-sm text-muted-foreground">AI usage cost multiplier (e.g., 10.0 for free, 3.0 for pro)</p>
</div>
<div className="space-y-2">
<Label htmlFor="sortOrder">Sort Order</Label>
<Input
id="sortOrder"
type="number"
value={formData.sortOrder}
onChange={(e) => setFormData(prev => ({ ...prev, sortOrder: parseInt(e.target.value) || 0 }))}
placeholder="0"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
placeholder="Plan description"
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="stripePriceId">Stripe Price ID</Label>
<Input
id="stripePriceId"
value={formData.stripePriceId}
onChange={(e) => setFormData(prev => ({ ...prev, stripePriceId: e.target.value }))}
placeholder="price_..."
/>
</div>
<div className="space-y-2">
<Label htmlFor="features">Features (JSON)</Label>
<Textarea
id="features"
value={formData.features}
onChange={(e) => setFormData(prev => ({ ...prev, features: e.target.value }))}
placeholder='{"promptLimit": 20, "versionLimit": 3}'
rows={4}
className="font-mono text-sm"
/>
</div>
<div className="space-y-2">
<Label htmlFor="limits">Limits (JSON)</Label>
<Textarea
id="limits"
value={formData.limits}
onChange={(e) => setFormData(prev => ({ ...prev, limits: e.target.value }))}
placeholder='{"maxPromptsPerDay": 100, "maxAPICallsPerMonth": 1000}'
rows={4}
className="font-mono text-sm"
/>
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="isActive"
checked={formData.isActive}
onChange={(e) => setFormData(prev => ({ ...prev, isActive: e.target.checked }))}
className="rounded border-gray-300"
/>
<Label htmlFor="isActive">Active</Label>
</div>
<div className="flex gap-2 pt-4">
<Button onClick={handleSave} className="flex items-center gap-2">
<Save className="h-4 w-4" />
{isCreating ? 'Create' : 'Update'}
</Button>
<Button onClick={handleCancel} variant="outline" className="flex items-center gap-2">
<X className="h-4 w-4" />
Cancel
</Button>
</div>
</CardContent>
</Card>
)}
{/* Plans List */}
<div className="grid gap-4">
{plans.map((plan) => (
<Card key={plan.id} className="relative">
<CardHeader>
<div className="flex justify-between items-start">
<div>
<CardTitle className="flex items-center gap-2">
{plan.displayName}
<Badge variant={plan.isActive ? 'default' : 'secondary'}>
{plan.isActive ? 'Active' : 'Inactive'}
</Badge>
</CardTitle>
<CardDescription>{plan.description}</CardDescription>
</div>
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={() => handleEdit(plan)}>
<Edit className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => handleDelete(plan.id)}
disabled={plan._count && (plan._count.users > 0 || plan._count.subscriptions > 0)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<p className="font-medium">ID</p>
<p className="text-muted-foreground">{plan.id}</p>
</div>
<div>
<p className="font-medium">Price</p>
<p className="text-muted-foreground">
{plan.price === 0 ? 'Free' : `$${plan.price}/${plan.interval}`}
</p>
</div>
<div>
<p className="font-medium">Cost Multiplier</p>
<p className="text-muted-foreground">{plan.costMultiplier}x</p>
</div>
<div>
<p className="font-medium">Sort Order</p>
<p className="text-muted-foreground">{plan.sortOrder}</p>
</div>
{plan._count && (
<>
<div>
<p className="font-medium">Users</p>
<p className="text-muted-foreground">{plan._count.users}</p>
</div>
<div>
<p className="font-medium">Subscriptions</p>
<p className="text-muted-foreground">{plan._count.subscriptions}</p>
</div>
<div>
<p className="font-medium">Models</p>
<p className="text-muted-foreground">{plan._count.models}</p>
</div>
</>
)}
</div>
</CardContent>
</Card>
))}
</div>
</div>
)
}

View File

@ -5,7 +5,7 @@ import { ReplicateService } from '@/lib/replicate'
import { UniAPIService } from '@/lib/uniapi' import { UniAPIService } from '@/lib/uniapi'
import { FalService } from '@/lib/fal' import { FalService } from '@/lib/fal'
// GET /api/admin/models - 获取所有模型(按套餐分组) // GET /api/admin/models - 获取所有模型
export async function GET() { export async function GET() {
try { try {
const models = await prisma.model.findMany({ const models = await prisma.model.findMany({
@ -19,7 +19,6 @@ export async function GET() {
} }
}, },
orderBy: [ orderBy: [
{ subscriptionPlan: { sortOrder: 'asc' } },
{ provider: 'asc' }, { provider: 'asc' },
{ name: 'asc' } { name: 'asc' }
] ]
@ -35,30 +34,12 @@ export async function GET() {
} }
} }
// POST /api/admin/models - 为指定套餐添加模型 // POST /api/admin/models - 同步和添加模型
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const { action, planId, selectedModels, serviceProvider } = await request.json() const { action, selectedModels, serviceProvider } = await request.json()
if (action === 'sync') { if (action === 'sync') {
if (!planId) {
return NextResponse.json(
{ error: 'Plan ID is required for sync action' },
{ status: 400 }
)
}
// 验证套餐是否存在
const plan = await prisma.subscriptionPlan.findUnique({
where: { id: planId }
})
if (!plan) {
return NextResponse.json(
{ error: 'Subscription plan not found' },
{ status: 404 }
)
}
const provider = serviceProvider || 'openrouter' const provider = serviceProvider || 'openrouter'
let availableModels: Array<Record<string, unknown>> = [] let availableModels: Array<Record<string, unknown>> = []
@ -116,74 +97,62 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ return NextResponse.json({
message: 'Models fetched successfully', message: 'Models fetched successfully',
availableModels, availableModels,
planId,
serviceProvider: provider serviceProvider: provider
}) })
} }
if (action === 'add') { if (action === 'add') {
if (!planId || !selectedModels || !Array.isArray(selectedModels)) { if (!selectedModels || !Array.isArray(selectedModels)) {
return NextResponse.json( return NextResponse.json(
{ error: 'Plan ID and selected models are required' }, { error: 'Selected models are required' },
{ status: 400 } { status: 400 }
) )
} }
// 检查模型 ID 的唯一性 // 检查模型 ID 的全局唯一性
const modelIds = selectedModels.map(model => model.modelId) const modelIds = selectedModels.map(model => model.modelId)
const existingModels = await prisma.model.findMany({ const existingModels = await prisma.model.findMany({
where: { where: {
modelId: { modelId: {
in: modelIds in: modelIds
},
subscriptionPlanId: {
not: planId // 排除当前套餐,允许同一套餐内更新
} }
}, },
include: { select: {
subscriptionPlan: { modelId: true,
select: { serviceProvider: true,
displayName: true name: true
}
}
} }
}) })
if (existingModels.length > 0) { if (existingModels.length > 0) {
const conflicts = existingModels.map(model => ({ const conflicts = existingModels.map(model => ({
modelId: model.modelId, modelId: model.modelId,
existingPlan: model.subscriptionPlan.displayName, existingServiceProvider: model.serviceProvider,
existingServiceProvider: model.serviceProvider modelName: model.name
})) }))
return NextResponse.json({ return NextResponse.json({
error: 'Model ID conflicts detected', error: 'Model ID conflicts detected',
conflicts: conflicts, conflicts: conflicts,
message: `The following model IDs are already used by other service providers: ${conflicts.map(c => `${c.modelId} (used by ${c.existingServiceProvider} in ${c.existingPlan})`).join(', ')}` message: `The following model IDs already exist: ${conflicts.map(c => `${c.modelId} (${c.modelName} - ${c.existingServiceProvider})`).join(', ')}`
}, { status: 409 }) }, { status: 409 })
} }
// 批量创建模型记录 // 批量创建模型记录 - 所有模型默认绑定到 free 套餐
const results = [] const results = []
for (const modelData of selectedModels) { for (const modelData of selectedModels) {
const result = await prisma.model.upsert({ const result = await prisma.model.create({
where: { data: {
subscriptionPlanId_modelId: {
subscriptionPlanId: planId,
modelId: modelData.modelId
}
},
update: {
...modelData, ...modelData,
subscriptionPlanId: planId, subscriptionPlanId: 'free' // 默认绑定到 free 套餐,所有套餐都可使用
updatedAt: new Date()
},
create: {
...modelData,
subscriptionPlanId: planId
}, },
include: { include: {
subscriptionPlan: true subscriptionPlan: {
select: {
id: true,
displayName: true
}
}
} }
}) })
@ -191,7 +160,7 @@ export async function POST(request: NextRequest) {
} }
return NextResponse.json({ return NextResponse.json({
message: `Successfully added ${results.length} models to ${planId} plan`, message: `Successfully added ${results.length} models`,
models: results models: results
}) })
} }

View File

@ -71,6 +71,7 @@ export async function POST(request: NextRequest) {
stripePriceId, stripePriceId,
isActive = true, isActive = true,
sortOrder = 0, sortOrder = 0,
costMultiplier = 1.0,
features, features,
limits limits
} = await request.json() } = await request.json()
@ -96,6 +97,7 @@ export async function POST(request: NextRequest) {
stripePriceId, stripePriceId,
isActive, isActive,
sortOrder, sortOrder,
costMultiplier,
features, features,
limits limits
} }
@ -130,6 +132,7 @@ export async function PUT(request: NextRequest) {
stripePriceId, stripePriceId,
isActive, isActive,
sortOrder, sortOrder,
costMultiplier,
features, features,
limits limits
} = await request.json() } = await request.json()
@ -154,6 +157,7 @@ export async function PUT(request: NextRequest) {
...(stripePriceId !== undefined && { stripePriceId }), ...(stripePriceId !== undefined && { stripePriceId }),
...(isActive !== undefined && { isActive }), ...(isActive !== undefined && { isActive }),
...(sortOrder !== undefined && { sortOrder }), ...(sortOrder !== undefined && { sortOrder }),
...(costMultiplier !== undefined && { costMultiplier }),
...(features && { features }), ...(features && { features }),
...(limits && { limits }) ...(limits && { limits })
} }

View File

@ -28,6 +28,16 @@ export async function POST(
prompt: true, prompt: true,
promptVersion: true, // 内部使用,用于获取内容 promptVersion: true, // 内部使用,用于获取内容
model: true, model: true,
user: {
include: {
subscriptionPlan: {
select: {
costMultiplier: true,
displayName: true
}
}
}
}
} }
}); });
@ -41,7 +51,8 @@ export async function POST(
// Check user's credit balance before execution // Check user's credit balance before execution
const userBalance = await getUserBalance(user.id); const userBalance = await getUserBalance(user.id);
const estimatedCost = calculateCost(0, 100, run.model); // Rough estimate const costMultiplier = run.user.subscriptionPlan.costMultiplier || 1.0;
const estimatedCost = calculateCost(0, 100, run.model, costMultiplier); // Rough estimate
if (userBalance < estimatedCost) { if (userBalance < estimatedCost) {
return NextResponse.json( return NextResponse.json(
@ -292,7 +303,7 @@ export async function POST(
if (data === '[DONE]') { if (data === '[DONE]') {
// 计算最终数据并更新数据库 // 计算最终数据并更新数据库
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
const actualCost = calculateCost(inputTokens, outputTokens, run.model); const actualCost = calculateCost(inputTokens, outputTokens, run.model, costMultiplier);
// Consume credits for this simulation // Consume credits for this simulation
let creditTransaction; let creditTransaction;

View File

@ -5,7 +5,7 @@ interface FalModel {
category?: string category?: string
type?: 'image' | 'video' | 'audio' | 'text' type?: 'image' | 'video' | 'audio' | 'text'
input_schema?: { input_schema?: {
properties?: Record<string, any> properties?: Record<string, unknown>
} }
pricing?: { pricing?: {
per_request?: number per_request?: number
@ -14,7 +14,7 @@ interface FalModel {
} }
max_resolution?: string max_resolution?: string
supported_formats?: string[] supported_formats?: string[]
[key: string]: any [key: string]: unknown
} }
interface FalModelsResponse { interface FalModelsResponse {

View File

@ -44,18 +44,36 @@ export function getOriginalPromptContent(run: Pick<SimulatorRun, 'prompt' | 'pro
} }
/** /**
* *
*/ */
export function calculateCost(inputTokens: number, outputTokens: number, model: { export function calculateCost(
inputCostPer1k?: number | null inputTokens: number,
outputCostPer1k?: number | null outputTokens: number,
}): number { model: {
inputCostPer1k?: number | null
outputCostPer1k?: number | null
},
costMultiplier: number = 1.0
): number {
const inputCostPer1k = model.inputCostPer1k || 0 const inputCostPer1k = model.inputCostPer1k || 0
const outputCostPer1k = model.outputCostPer1k || 0 const outputCostPer1k = model.outputCostPer1k || 0
// 计算费用token数除以1000再乘以每1k的费用 // 计算基础费用token数除以1000再乘以每1k的费用
const inputCost = (inputTokens / 1000) * inputCostPer1k const inputCost = (inputTokens / 1000) * inputCostPer1k
const outputCost = (outputTokens / 1000) * outputCostPer1k const outputCost = (outputTokens / 1000) * outputCostPer1k
const baseCost = inputCost + outputCost
return inputCost + outputCost // 应用套餐费用倍率
return baseCost * costMultiplier
}
/**
* 使
* @deprecated 使 costMultiplier
*/
export function calculateBaseCost(inputTokens: number, outputTokens: number, model: {
inputCostPer1k?: number | null
outputCostPer1k?: number | null
}): number {
return calculateCost(inputTokens, outputTokens, model, 1.0)
} }

View File

@ -38,7 +38,7 @@ export class UniAPIService {
}) })
if (!response.ok) { if (!response.ok) {
const errorText = await response.text() await response.text() // Read error text but don't use it
throw new Error(`UniAPI error: ${response.status} ${response.statusText}`) throw new Error(`UniAPI error: ${response.status} ${response.statusText}`)
} }