costhawk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +32 -0
- package/STRATEGIC_PLAN_2025-12-01.md +934 -0
- package/costcanary/.claude/settings.local.json +9 -0
- package/costcanary/.env.production.template +38 -0
- package/costcanary/.eslintrc.json +22 -0
- package/costcanary/.nvmrc +1 -0
- package/costcanary/.prettierignore +11 -0
- package/costcanary/.prettierrc.json +12 -0
- package/costcanary/ADMIN_SETUP.md +68 -0
- package/costcanary/CLAUDE.md +228 -0
- package/costcanary/CLERK_SETUP.md +69 -0
- package/costcanary/DATABASE_SETUP.md +136 -0
- package/costcanary/DEMO_CHECKLIST.md +62 -0
- package/costcanary/DEPLOYMENT.md +31 -0
- package/costcanary/PRODUCTION_RECOVERY.md +109 -0
- package/costcanary/README.md +247 -0
- package/costcanary/STRIPE_SECURITY_AUDIT.md +123 -0
- package/costcanary/TESTING_ADMIN.md +92 -0
- package/costcanary/app/(auth)/sign-in/[[...sign-in]]/page.tsx +25 -0
- package/costcanary/app/(auth)/sign-up/[[...sign-up]]/page.tsx +25 -0
- package/costcanary/app/(dashboard)/dashboard/admin/page.tsx +260 -0
- package/costcanary/app/(dashboard)/dashboard/alerts/page.tsx +64 -0
- package/costcanary/app/(dashboard)/dashboard/api-keys/page.tsx +231 -0
- package/costcanary/app/(dashboard)/dashboard/billing/page.tsx +349 -0
- package/costcanary/app/(dashboard)/dashboard/layout.tsx +188 -0
- package/costcanary/app/(dashboard)/dashboard/page.tsx +13 -0
- package/costcanary/app/(dashboard)/dashboard/playground/page.tsx +605 -0
- package/costcanary/app/(dashboard)/dashboard/settings/page.tsx +86 -0
- package/costcanary/app/(dashboard)/dashboard/usage/page.tsx +354 -0
- package/costcanary/app/(dashboard)/dashboard/wrapped-keys/page.tsx +677 -0
- package/costcanary/app/(marketing)/page.tsx +90 -0
- package/costcanary/app/(marketing)/pricing/page.tsx +272 -0
- package/costcanary/app/admin/pricing-status/page.tsx +338 -0
- package/costcanary/app/api/admin/check-pricing/route.ts +127 -0
- package/costcanary/app/api/admin/debug/route.ts +44 -0
- package/costcanary/app/api/admin/fix-pricing/route.ts +216 -0
- package/costcanary/app/api/admin/pricing-jobs/[jobId]/route.ts +48 -0
- package/costcanary/app/api/admin/pricing-jobs/route.ts +45 -0
- package/costcanary/app/api/admin/trigger-pricing/route.ts +209 -0
- package/costcanary/app/api/admin/whoami/route.ts +44 -0
- package/costcanary/app/api/auth/clerk/[...nextjs]/route.ts +93 -0
- package/costcanary/app/api/debug/wrapped-key/route.ts +51 -0
- package/costcanary/app/api/debug-status/route.ts +9 -0
- package/costcanary/app/api/debug-version/route.ts +12 -0
- package/costcanary/app/api/health/route.ts +14 -0
- package/costcanary/app/api/health-simple/route.ts +18 -0
- package/costcanary/app/api/keys/route.ts +162 -0
- package/costcanary/app/api/keys/wrapped/[id]/revoke/route.ts +86 -0
- package/costcanary/app/api/keys/wrapped/[id]/rotate/route.ts +81 -0
- package/costcanary/app/api/keys/wrapped/route.ts +241 -0
- package/costcanary/app/api/optimizer/preview/route.ts +147 -0
- package/costcanary/app/api/optimizer/route.ts +118 -0
- package/costcanary/app/api/pricing/models/route.ts +102 -0
- package/costcanary/app/api/proxy/[...path]/route.ts +391 -0
- package/costcanary/app/api/proxy/anthropic/route.ts +539 -0
- package/costcanary/app/api/proxy/google/route.ts +395 -0
- package/costcanary/app/api/proxy/openai/route.ts +529 -0
- package/costcanary/app/api/simple-test/route.ts +7 -0
- package/costcanary/app/api/stripe/checkout/route.ts +201 -0
- package/costcanary/app/api/stripe/webhook/route.ts +392 -0
- package/costcanary/app/api/test-connection/route.ts +209 -0
- package/costcanary/app/api/test-proxy/route.ts +7 -0
- package/costcanary/app/api/test-simple/route.ts +20 -0
- package/costcanary/app/api/usage/current/route.ts +112 -0
- package/costcanary/app/api/usage/stats/route.ts +129 -0
- package/costcanary/app/api/usage/stream/route.ts +113 -0
- package/costcanary/app/api/usage/summary/route.ts +67 -0
- package/costcanary/app/api/usage/trend/route.ts +119 -0
- package/costcanary/app/api/ws/route.ts +23 -0
- package/costcanary/app/globals.css +280 -0
- package/costcanary/app/layout.tsx +87 -0
- package/costcanary/components/Header.tsx +85 -0
- package/costcanary/components/dashboard/AddApiKeyModal.tsx +264 -0
- package/costcanary/components/dashboard/dashboard-content.tsx +329 -0
- package/costcanary/components/landing/DashboardPreview.tsx +222 -0
- package/costcanary/components/landing/Features.tsx +238 -0
- package/costcanary/components/landing/Footer.tsx +83 -0
- package/costcanary/components/landing/Hero.tsx +193 -0
- package/costcanary/components/landing/Pricing.tsx +250 -0
- package/costcanary/components/landing/Testimonials.tsx +248 -0
- package/costcanary/components/theme-provider.tsx +8 -0
- package/costcanary/components/ui/alert.tsx +59 -0
- package/costcanary/components/ui/badge.tsx +36 -0
- package/costcanary/components/ui/button.tsx +56 -0
- package/costcanary/components/ui/card.tsx +79 -0
- package/costcanary/components/ui/dialog.tsx +122 -0
- package/costcanary/components/ui/input.tsx +22 -0
- package/costcanary/components/ui/label.tsx +26 -0
- package/costcanary/components/ui/progress.tsx +28 -0
- package/costcanary/components/ui/select.tsx +160 -0
- package/costcanary/components/ui/separator.tsx +31 -0
- package/costcanary/components/ui/switch.tsx +29 -0
- package/costcanary/components/ui/tabs.tsx +55 -0
- package/costcanary/components/ui/toast.tsx +127 -0
- package/costcanary/components/ui/toaster.tsx +35 -0
- package/costcanary/components/ui/use-toast.ts +189 -0
- package/costcanary/components.json +17 -0
- package/costcanary/debug-wrapped-keys.md +117 -0
- package/costcanary/fix-console.sh +30 -0
- package/costcanary/lib/admin-auth.ts +226 -0
- package/costcanary/lib/admin-security.ts +124 -0
- package/costcanary/lib/audit-events.ts +62 -0
- package/costcanary/lib/audit.ts +158 -0
- package/costcanary/lib/chart-colors.ts +152 -0
- package/costcanary/lib/cost-calculator.ts +212 -0
- package/costcanary/lib/db-utils.ts +325 -0
- package/costcanary/lib/db.ts +14 -0
- package/costcanary/lib/encryption.ts +120 -0
- package/costcanary/lib/kms.ts +358 -0
- package/costcanary/lib/model-alias.ts +180 -0
- package/costcanary/lib/pricing.ts +292 -0
- package/costcanary/lib/prisma.ts +52 -0
- package/costcanary/lib/railway-db.ts +157 -0
- package/costcanary/lib/sse-parser.ts +283 -0
- package/costcanary/lib/stripe-client.ts +81 -0
- package/costcanary/lib/stripe-server.ts +52 -0
- package/costcanary/lib/tokens.ts +396 -0
- package/costcanary/lib/usage-limits.ts +164 -0
- package/costcanary/lib/utils.ts +6 -0
- package/costcanary/lib/websocket.ts +153 -0
- package/costcanary/lib/wrapped-keys.ts +531 -0
- package/costcanary/market-research.md +443 -0
- package/costcanary/middleware.ts +48 -0
- package/costcanary/next.config.js +43 -0
- package/costcanary/nia-sources.md +151 -0
- package/costcanary/package-lock.json +12162 -0
- package/costcanary/package.json +92 -0
- package/costcanary/package.json.backup +89 -0
- package/costcanary/postcss.config.js +6 -0
- package/costcanary/pricing-worker/.env.example +8 -0
- package/costcanary/pricing-worker/README.md +81 -0
- package/costcanary/pricing-worker/package-lock.json +1109 -0
- package/costcanary/pricing-worker/package.json +26 -0
- package/costcanary/pricing-worker/railway.json +13 -0
- package/costcanary/pricing-worker/schema.prisma +326 -0
- package/costcanary/pricing-worker/src/index.ts +115 -0
- package/costcanary/pricing-worker/src/services/pricing-updater.ts +79 -0
- package/costcanary/pricing-worker/src/services/tavily-client.ts +474 -0
- package/costcanary/pricing-worker/test-tavily.ts +47 -0
- package/costcanary/pricing-worker/tsconfig.json +24 -0
- package/costcanary/prisma/migrations/001_add_stripe_fields.sql +26 -0
- package/costcanary/prisma/schema.prisma +326 -0
- package/costcanary/prisma/seed-pricing.ts +133 -0
- package/costcanary/public/costhawk-logo.png +0 -0
- package/costcanary/railway.json +30 -0
- package/costcanary/railway.toml +16 -0
- package/costcanary/research-nia.md +298 -0
- package/costcanary/research.md +411 -0
- package/costcanary/scripts/build-production.js +65 -0
- package/costcanary/scripts/check-current-pricing.ts +51 -0
- package/costcanary/scripts/check-pricing-data.ts +174 -0
- package/costcanary/scripts/create-stripe-prices.js +49 -0
- package/costcanary/scripts/fix-pricing-data.ts +135 -0
- package/costcanary/scripts/fix-pricing-db.ts +148 -0
- package/costcanary/scripts/postinstall.js +58 -0
- package/costcanary/scripts/railway-deploy.sh +52 -0
- package/costcanary/scripts/run-migration.js +61 -0
- package/costcanary/scripts/start-production.js +175 -0
- package/costcanary/scripts/test-wrapped-key.ts +85 -0
- package/costcanary/scripts/validate-deployment.js +176 -0
- package/costcanary/scripts/validate-production.js +119 -0
- package/costcanary/server.js.backup +38 -0
- package/costcanary/tailwind.config.ts +216 -0
- package/costcanary/test-pricing-status.sh +27 -0
- package/costcanary/tsconfig.json +42 -0
- package/docs/sessions/session-2025-12-01.md +570 -0
- package/executive-summary.md +302 -0
- package/index.js +1 -0
- package/nia-sources.md +163 -0
- package/package.json +16 -0
- package/research.md +750 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { auth } from '@clerk/nextjs/server'
|
|
3
|
+
import { prisma } from '@/lib/prisma'
|
|
4
|
+
|
|
5
|
+
// Force dynamic rendering for this route
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* GET /api/optimizer/preview
|
|
10
|
+
*
|
|
11
|
+
* Returns projected monthly savings based on recent usage patterns
|
|
12
|
+
*/
|
|
13
|
+
export async function GET(_request: NextRequest) {
|
|
14
|
+
try {
|
|
15
|
+
const { userId, orgId } = await auth()
|
|
16
|
+
|
|
17
|
+
if (!userId) {
|
|
18
|
+
return NextResponse.json(
|
|
19
|
+
{ error: 'Unauthorized' },
|
|
20
|
+
{ status: 401 }
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const effectiveOrgId = orgId || userId
|
|
25
|
+
|
|
26
|
+
// Get last 30 days of usage
|
|
27
|
+
const thirtyDaysAgo = new Date()
|
|
28
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
|
|
29
|
+
|
|
30
|
+
// Get usage patterns
|
|
31
|
+
const usageLogs = await prisma.usageLog.findMany({
|
|
32
|
+
where: {
|
|
33
|
+
userId: { in: await getOrgUserIds(effectiveOrgId) },
|
|
34
|
+
timestamp: { gte: thirtyDaysAgo }
|
|
35
|
+
},
|
|
36
|
+
select: {
|
|
37
|
+
provider: true,
|
|
38
|
+
model: true,
|
|
39
|
+
inputTokens: true,
|
|
40
|
+
outputTokens: true,
|
|
41
|
+
cost: true
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
if (usageLogs.length === 0) {
|
|
46
|
+
return NextResponse.json({
|
|
47
|
+
estimatedMonthlySavings: 0,
|
|
48
|
+
techniques: [],
|
|
49
|
+
message: 'Not enough usage data to calculate savings'
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Calculate potential savings
|
|
54
|
+
let totalSavings = 0
|
|
55
|
+
const techniques = []
|
|
56
|
+
|
|
57
|
+
// 1. Model optimization (e.g., GPT-4 -> GPT-3.5 for simple tasks)
|
|
58
|
+
const gpt4Usage = usageLogs.filter((l: any) => l.model === 'gpt-4')
|
|
59
|
+
if (gpt4Usage.length > 0) {
|
|
60
|
+
const potentialSavings = gpt4Usage.reduce((sum: any, log: any) => {
|
|
61
|
+
// Estimate 30% could use GPT-3.5 instead
|
|
62
|
+
const gpt35Cost = (log.inputTokens * 0.0015 + log.outputTokens * 0.002) / 1000
|
|
63
|
+
const savings = Number(log.cost) - gpt35Cost
|
|
64
|
+
return sum + (savings * 0.3) // 30% of calls
|
|
65
|
+
}, 0)
|
|
66
|
+
|
|
67
|
+
if (potentialSavings > 0) {
|
|
68
|
+
totalSavings += potentialSavings
|
|
69
|
+
techniques.push({
|
|
70
|
+
name: 'Smart Model Selection',
|
|
71
|
+
description: 'Use GPT-3.5 for simpler queries',
|
|
72
|
+
estimatedSavings: potentialSavings,
|
|
73
|
+
percentage: 65 // 65% cost reduction for those calls
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 2. Caching for repeated queries
|
|
79
|
+
const totalCost = usageLogs.reduce((sum: any, log: any) => sum + Number(log.cost), 0)
|
|
80
|
+
const cachingSavings = totalCost * 0.15 // Estimate 15% cache hit rate
|
|
81
|
+
if (cachingSavings > 0) {
|
|
82
|
+
totalSavings += cachingSavings
|
|
83
|
+
techniques.push({
|
|
84
|
+
name: 'Response Caching',
|
|
85
|
+
description: 'Cache frequently repeated queries',
|
|
86
|
+
estimatedSavings: cachingSavings,
|
|
87
|
+
percentage: 15
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. Token optimization
|
|
92
|
+
const avgInputTokens = usageLogs.reduce((sum: any, log: any) => sum + log.inputTokens, 0) / usageLogs.length
|
|
93
|
+
if (avgInputTokens > 500) {
|
|
94
|
+
const tokenSavings = totalCost * 0.10 // 10% reduction through better prompts
|
|
95
|
+
totalSavings += tokenSavings
|
|
96
|
+
techniques.push({
|
|
97
|
+
name: 'Prompt Optimization',
|
|
98
|
+
description: 'Reduce prompt size without losing quality',
|
|
99
|
+
estimatedSavings: tokenSavings,
|
|
100
|
+
percentage: 10
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 4. Batching opportunities
|
|
105
|
+
const providers = new Set(usageLogs.map((l: any) => l.provider))
|
|
106
|
+
if (providers.has('OPENAI')) {
|
|
107
|
+
const batchingSavings = totalCost * 0.05 // 5% from batching
|
|
108
|
+
totalSavings += batchingSavings
|
|
109
|
+
techniques.push({
|
|
110
|
+
name: 'Request Batching',
|
|
111
|
+
description: 'Batch multiple requests when possible',
|
|
112
|
+
estimatedSavings: batchingSavings,
|
|
113
|
+
percentage: 5
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Project to monthly
|
|
118
|
+
const daysInPeriod = Math.min(30, Math.floor((Date.now() - thirtyDaysAgo.getTime()) / (1000 * 60 * 60 * 24)))
|
|
119
|
+
const monthlyMultiplier = 30 / daysInPeriod
|
|
120
|
+
const estimatedMonthlySavings = totalSavings * monthlyMultiplier
|
|
121
|
+
|
|
122
|
+
return NextResponse.json({
|
|
123
|
+
estimatedMonthlySavings,
|
|
124
|
+
currentMonthlyCost: totalCost * monthlyMultiplier,
|
|
125
|
+
techniques: techniques.sort((a: any, b: any) => b.estimatedSavings - a.estimatedSavings),
|
|
126
|
+
optimizationPotential: (totalSavings / totalCost) * 100,
|
|
127
|
+
dataPoints: usageLogs.length,
|
|
128
|
+
message: `Based on ${usageLogs.length} API calls over ${daysInPeriod} days`
|
|
129
|
+
})
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('Optimizer preview error:', error)
|
|
132
|
+
return NextResponse.json(
|
|
133
|
+
{ error: 'Failed to calculate optimization preview' },
|
|
134
|
+
{ status: 500 }
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function getOrgUserIds(orgId: string): Promise<string[]> {
|
|
140
|
+
const users = await prisma.wrappedKey.findMany({
|
|
141
|
+
where: { orgId },
|
|
142
|
+
select: { userId: true },
|
|
143
|
+
distinct: ['userId']
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
return users.map((u: any) => u.userId)
|
|
147
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
+
import { auth } from '@clerk/nextjs/server'
|
|
3
|
+
import { prisma } from '@/lib/prisma'
|
|
4
|
+
import { createAuditLog } from '@/lib/audit'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* POST /api/optimizer
|
|
8
|
+
*
|
|
9
|
+
* Enable or disable optimizer mode for the organization
|
|
10
|
+
*/
|
|
11
|
+
export async function POST(request: NextRequest) {
|
|
12
|
+
try {
|
|
13
|
+
const { userId, orgId } = await auth()
|
|
14
|
+
|
|
15
|
+
if (!userId) {
|
|
16
|
+
return NextResponse.json(
|
|
17
|
+
{ error: 'Unauthorized' },
|
|
18
|
+
{ status: 401 }
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { action } = await request.json()
|
|
23
|
+
|
|
24
|
+
if (!['enable', 'disable'].includes(action)) {
|
|
25
|
+
return NextResponse.json(
|
|
26
|
+
{ error: 'Invalid action. Use "enable" or "disable"' },
|
|
27
|
+
{ status: 400 }
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const effectiveOrgId = orgId || userId
|
|
32
|
+
const enabled = action === 'enable'
|
|
33
|
+
|
|
34
|
+
// Store optimizer setting (in production, this would be in a settings table)
|
|
35
|
+
// For now, we'll use user metadata
|
|
36
|
+
const user = await prisma.user.findUnique({
|
|
37
|
+
where: { id: userId }
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (!user) {
|
|
41
|
+
return NextResponse.json(
|
|
42
|
+
{ error: 'User not found' },
|
|
43
|
+
{ status: 404 }
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Update user settings (you might want to create a separate Settings model)
|
|
48
|
+
// For MVP, we'll use a simple flag
|
|
49
|
+
await prisma.user.update({
|
|
50
|
+
where: { id: userId },
|
|
51
|
+
data: {
|
|
52
|
+
// Store in a JSON field or create a Settings model
|
|
53
|
+
// For now, using the tier field as a placeholder
|
|
54
|
+
// In production, create a proper Settings model
|
|
55
|
+
updatedAt: new Date()
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Audit log
|
|
60
|
+
await createAuditLog({
|
|
61
|
+
userId,
|
|
62
|
+
action: enabled ? 'CREATE' : 'DELETE',
|
|
63
|
+
entityType: 'OptimizerMode',
|
|
64
|
+
entityId: effectiveOrgId,
|
|
65
|
+
newValue: { enabled, timestamp: new Date() }
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return NextResponse.json({
|
|
69
|
+
enabled,
|
|
70
|
+
message: enabled
|
|
71
|
+
? 'Optimizer mode enabled. AI will now optimize your API calls for cost savings.'
|
|
72
|
+
: 'Optimizer mode disabled. API calls will pass through without modifications.',
|
|
73
|
+
timestamp: new Date()
|
|
74
|
+
})
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Optimizer toggle error:', error)
|
|
77
|
+
return NextResponse.json(
|
|
78
|
+
{ error: 'Failed to toggle optimizer mode' },
|
|
79
|
+
{ status: 500 }
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* GET /api/optimizer
|
|
86
|
+
*
|
|
87
|
+
* Get current optimizer status
|
|
88
|
+
*/
|
|
89
|
+
export async function GET(_request: NextRequest) {
|
|
90
|
+
try {
|
|
91
|
+
const { userId } = await auth()
|
|
92
|
+
|
|
93
|
+
if (!userId) {
|
|
94
|
+
return NextResponse.json(
|
|
95
|
+
{ error: 'Unauthorized' },
|
|
96
|
+
{ status: 401 }
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// In production, fetch from settings table
|
|
101
|
+
// For MVP, we'll return a default
|
|
102
|
+
const enabled = false // Default to disabled
|
|
103
|
+
|
|
104
|
+
return NextResponse.json({
|
|
105
|
+
enabled,
|
|
106
|
+
mode: enabled ? 'optimize' : 'track',
|
|
107
|
+
description: enabled
|
|
108
|
+
? 'Optimizer is actively reducing costs'
|
|
109
|
+
: 'Tracking costs without modifications'
|
|
110
|
+
})
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('Get optimizer status error:', error)
|
|
113
|
+
return NextResponse.json(
|
|
114
|
+
{ error: 'Failed to get optimizer status' },
|
|
115
|
+
{ status: 500 }
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/prisma';
|
|
3
|
+
import { auth } from '@clerk/nextjs/server';
|
|
4
|
+
|
|
5
|
+
export async function GET() {
|
|
6
|
+
try {
|
|
7
|
+
// Check authentication
|
|
8
|
+
const { userId } = await auth();
|
|
9
|
+
if (!userId) {
|
|
10
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Fetch all pricing models directly from PricingModelPrice table
|
|
14
|
+
// Order by createdAt DESC so we get the most recent first for deduplication
|
|
15
|
+
const allModels = await prisma.pricingModelPrice.findMany({
|
|
16
|
+
orderBy: [
|
|
17
|
+
{ provider: 'asc' },
|
|
18
|
+
{ canonicalModel: 'asc' },
|
|
19
|
+
{ createdAt: 'desc' }
|
|
20
|
+
]
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Transform and DEDUPLICATE - keep only first occurrence (most recent) per provider+model
|
|
24
|
+
const providerModels: Record<string, Array<{
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
displayName: string;
|
|
28
|
+
inputPricePerK: number;
|
|
29
|
+
outputPricePerK: number;
|
|
30
|
+
}>> = {};
|
|
31
|
+
|
|
32
|
+
const seenModels = new Set<string>();
|
|
33
|
+
|
|
34
|
+
for (const model of allModels) {
|
|
35
|
+
const provider = model.provider.toLowerCase();
|
|
36
|
+
const uniqueKey = `${provider}:${model.canonicalModel}`;
|
|
37
|
+
|
|
38
|
+
// Skip duplicates
|
|
39
|
+
if (seenModels.has(uniqueKey)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
seenModels.add(uniqueKey);
|
|
43
|
+
|
|
44
|
+
if (!providerModels[provider]) {
|
|
45
|
+
providerModels[provider] = [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Create a display name that shows the price
|
|
49
|
+
const displayName = `${model.canonicalModel} ($${Number(model.unitInputPer1k).toFixed(4)}/$${Number(model.unitOutputPer1k).toFixed(4)} per 1K)`;
|
|
50
|
+
|
|
51
|
+
providerModels[provider].push({
|
|
52
|
+
id: model.canonicalModel,
|
|
53
|
+
name: model.canonicalModel,
|
|
54
|
+
displayName: displayName,
|
|
55
|
+
inputPricePerK: Number(model.unitInputPer1k),
|
|
56
|
+
outputPricePerK: Number(model.unitOutputPer1k)
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create the provider list with proper formatting
|
|
61
|
+
const providers = [
|
|
62
|
+
{
|
|
63
|
+
id: "openai",
|
|
64
|
+
name: "OpenAI",
|
|
65
|
+
icon: "🤖",
|
|
66
|
+
models: providerModels['openai'] || []
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "anthropic",
|
|
70
|
+
name: "Anthropic",
|
|
71
|
+
icon: "🧠",
|
|
72
|
+
models: providerModels['anthropic'] || []
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "google",
|
|
76
|
+
name: "Google",
|
|
77
|
+
icon: "🔷",
|
|
78
|
+
models: providerModels['google'] || []
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
return NextResponse.json({
|
|
83
|
+
success: true,
|
|
84
|
+
providers,
|
|
85
|
+
// Also return a flat model pricing map for easy lookup
|
|
86
|
+
modelPricing: Object.values(providerModels).flat().reduce((acc, model) => {
|
|
87
|
+
acc[model.id] = {
|
|
88
|
+
input: model.inputPricePerK,
|
|
89
|
+
output: model.outputPricePerK
|
|
90
|
+
};
|
|
91
|
+
return acc;
|
|
92
|
+
}, {} as Record<string, { input: number; output: number }>)
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('Failed to fetch pricing models:', error);
|
|
97
|
+
return NextResponse.json(
|
|
98
|
+
{ error: 'Failed to fetch pricing models' },
|
|
99
|
+
{ status: 500 }
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|