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.
Files changed (171) hide show
  1. package/.claude/settings.local.json +32 -0
  2. package/STRATEGIC_PLAN_2025-12-01.md +934 -0
  3. package/costcanary/.claude/settings.local.json +9 -0
  4. package/costcanary/.env.production.template +38 -0
  5. package/costcanary/.eslintrc.json +22 -0
  6. package/costcanary/.nvmrc +1 -0
  7. package/costcanary/.prettierignore +11 -0
  8. package/costcanary/.prettierrc.json +12 -0
  9. package/costcanary/ADMIN_SETUP.md +68 -0
  10. package/costcanary/CLAUDE.md +228 -0
  11. package/costcanary/CLERK_SETUP.md +69 -0
  12. package/costcanary/DATABASE_SETUP.md +136 -0
  13. package/costcanary/DEMO_CHECKLIST.md +62 -0
  14. package/costcanary/DEPLOYMENT.md +31 -0
  15. package/costcanary/PRODUCTION_RECOVERY.md +109 -0
  16. package/costcanary/README.md +247 -0
  17. package/costcanary/STRIPE_SECURITY_AUDIT.md +123 -0
  18. package/costcanary/TESTING_ADMIN.md +92 -0
  19. package/costcanary/app/(auth)/sign-in/[[...sign-in]]/page.tsx +25 -0
  20. package/costcanary/app/(auth)/sign-up/[[...sign-up]]/page.tsx +25 -0
  21. package/costcanary/app/(dashboard)/dashboard/admin/page.tsx +260 -0
  22. package/costcanary/app/(dashboard)/dashboard/alerts/page.tsx +64 -0
  23. package/costcanary/app/(dashboard)/dashboard/api-keys/page.tsx +231 -0
  24. package/costcanary/app/(dashboard)/dashboard/billing/page.tsx +349 -0
  25. package/costcanary/app/(dashboard)/dashboard/layout.tsx +188 -0
  26. package/costcanary/app/(dashboard)/dashboard/page.tsx +13 -0
  27. package/costcanary/app/(dashboard)/dashboard/playground/page.tsx +605 -0
  28. package/costcanary/app/(dashboard)/dashboard/settings/page.tsx +86 -0
  29. package/costcanary/app/(dashboard)/dashboard/usage/page.tsx +354 -0
  30. package/costcanary/app/(dashboard)/dashboard/wrapped-keys/page.tsx +677 -0
  31. package/costcanary/app/(marketing)/page.tsx +90 -0
  32. package/costcanary/app/(marketing)/pricing/page.tsx +272 -0
  33. package/costcanary/app/admin/pricing-status/page.tsx +338 -0
  34. package/costcanary/app/api/admin/check-pricing/route.ts +127 -0
  35. package/costcanary/app/api/admin/debug/route.ts +44 -0
  36. package/costcanary/app/api/admin/fix-pricing/route.ts +216 -0
  37. package/costcanary/app/api/admin/pricing-jobs/[jobId]/route.ts +48 -0
  38. package/costcanary/app/api/admin/pricing-jobs/route.ts +45 -0
  39. package/costcanary/app/api/admin/trigger-pricing/route.ts +209 -0
  40. package/costcanary/app/api/admin/whoami/route.ts +44 -0
  41. package/costcanary/app/api/auth/clerk/[...nextjs]/route.ts +93 -0
  42. package/costcanary/app/api/debug/wrapped-key/route.ts +51 -0
  43. package/costcanary/app/api/debug-status/route.ts +9 -0
  44. package/costcanary/app/api/debug-version/route.ts +12 -0
  45. package/costcanary/app/api/health/route.ts +14 -0
  46. package/costcanary/app/api/health-simple/route.ts +18 -0
  47. package/costcanary/app/api/keys/route.ts +162 -0
  48. package/costcanary/app/api/keys/wrapped/[id]/revoke/route.ts +86 -0
  49. package/costcanary/app/api/keys/wrapped/[id]/rotate/route.ts +81 -0
  50. package/costcanary/app/api/keys/wrapped/route.ts +241 -0
  51. package/costcanary/app/api/optimizer/preview/route.ts +147 -0
  52. package/costcanary/app/api/optimizer/route.ts +118 -0
  53. package/costcanary/app/api/pricing/models/route.ts +102 -0
  54. package/costcanary/app/api/proxy/[...path]/route.ts +391 -0
  55. package/costcanary/app/api/proxy/anthropic/route.ts +539 -0
  56. package/costcanary/app/api/proxy/google/route.ts +395 -0
  57. package/costcanary/app/api/proxy/openai/route.ts +529 -0
  58. package/costcanary/app/api/simple-test/route.ts +7 -0
  59. package/costcanary/app/api/stripe/checkout/route.ts +201 -0
  60. package/costcanary/app/api/stripe/webhook/route.ts +392 -0
  61. package/costcanary/app/api/test-connection/route.ts +209 -0
  62. package/costcanary/app/api/test-proxy/route.ts +7 -0
  63. package/costcanary/app/api/test-simple/route.ts +20 -0
  64. package/costcanary/app/api/usage/current/route.ts +112 -0
  65. package/costcanary/app/api/usage/stats/route.ts +129 -0
  66. package/costcanary/app/api/usage/stream/route.ts +113 -0
  67. package/costcanary/app/api/usage/summary/route.ts +67 -0
  68. package/costcanary/app/api/usage/trend/route.ts +119 -0
  69. package/costcanary/app/api/ws/route.ts +23 -0
  70. package/costcanary/app/globals.css +280 -0
  71. package/costcanary/app/layout.tsx +87 -0
  72. package/costcanary/components/Header.tsx +85 -0
  73. package/costcanary/components/dashboard/AddApiKeyModal.tsx +264 -0
  74. package/costcanary/components/dashboard/dashboard-content.tsx +329 -0
  75. package/costcanary/components/landing/DashboardPreview.tsx +222 -0
  76. package/costcanary/components/landing/Features.tsx +238 -0
  77. package/costcanary/components/landing/Footer.tsx +83 -0
  78. package/costcanary/components/landing/Hero.tsx +193 -0
  79. package/costcanary/components/landing/Pricing.tsx +250 -0
  80. package/costcanary/components/landing/Testimonials.tsx +248 -0
  81. package/costcanary/components/theme-provider.tsx +8 -0
  82. package/costcanary/components/ui/alert.tsx +59 -0
  83. package/costcanary/components/ui/badge.tsx +36 -0
  84. package/costcanary/components/ui/button.tsx +56 -0
  85. package/costcanary/components/ui/card.tsx +79 -0
  86. package/costcanary/components/ui/dialog.tsx +122 -0
  87. package/costcanary/components/ui/input.tsx +22 -0
  88. package/costcanary/components/ui/label.tsx +26 -0
  89. package/costcanary/components/ui/progress.tsx +28 -0
  90. package/costcanary/components/ui/select.tsx +160 -0
  91. package/costcanary/components/ui/separator.tsx +31 -0
  92. package/costcanary/components/ui/switch.tsx +29 -0
  93. package/costcanary/components/ui/tabs.tsx +55 -0
  94. package/costcanary/components/ui/toast.tsx +127 -0
  95. package/costcanary/components/ui/toaster.tsx +35 -0
  96. package/costcanary/components/ui/use-toast.ts +189 -0
  97. package/costcanary/components.json +17 -0
  98. package/costcanary/debug-wrapped-keys.md +117 -0
  99. package/costcanary/fix-console.sh +30 -0
  100. package/costcanary/lib/admin-auth.ts +226 -0
  101. package/costcanary/lib/admin-security.ts +124 -0
  102. package/costcanary/lib/audit-events.ts +62 -0
  103. package/costcanary/lib/audit.ts +158 -0
  104. package/costcanary/lib/chart-colors.ts +152 -0
  105. package/costcanary/lib/cost-calculator.ts +212 -0
  106. package/costcanary/lib/db-utils.ts +325 -0
  107. package/costcanary/lib/db.ts +14 -0
  108. package/costcanary/lib/encryption.ts +120 -0
  109. package/costcanary/lib/kms.ts +358 -0
  110. package/costcanary/lib/model-alias.ts +180 -0
  111. package/costcanary/lib/pricing.ts +292 -0
  112. package/costcanary/lib/prisma.ts +52 -0
  113. package/costcanary/lib/railway-db.ts +157 -0
  114. package/costcanary/lib/sse-parser.ts +283 -0
  115. package/costcanary/lib/stripe-client.ts +81 -0
  116. package/costcanary/lib/stripe-server.ts +52 -0
  117. package/costcanary/lib/tokens.ts +396 -0
  118. package/costcanary/lib/usage-limits.ts +164 -0
  119. package/costcanary/lib/utils.ts +6 -0
  120. package/costcanary/lib/websocket.ts +153 -0
  121. package/costcanary/lib/wrapped-keys.ts +531 -0
  122. package/costcanary/market-research.md +443 -0
  123. package/costcanary/middleware.ts +48 -0
  124. package/costcanary/next.config.js +43 -0
  125. package/costcanary/nia-sources.md +151 -0
  126. package/costcanary/package-lock.json +12162 -0
  127. package/costcanary/package.json +92 -0
  128. package/costcanary/package.json.backup +89 -0
  129. package/costcanary/postcss.config.js +6 -0
  130. package/costcanary/pricing-worker/.env.example +8 -0
  131. package/costcanary/pricing-worker/README.md +81 -0
  132. package/costcanary/pricing-worker/package-lock.json +1109 -0
  133. package/costcanary/pricing-worker/package.json +26 -0
  134. package/costcanary/pricing-worker/railway.json +13 -0
  135. package/costcanary/pricing-worker/schema.prisma +326 -0
  136. package/costcanary/pricing-worker/src/index.ts +115 -0
  137. package/costcanary/pricing-worker/src/services/pricing-updater.ts +79 -0
  138. package/costcanary/pricing-worker/src/services/tavily-client.ts +474 -0
  139. package/costcanary/pricing-worker/test-tavily.ts +47 -0
  140. package/costcanary/pricing-worker/tsconfig.json +24 -0
  141. package/costcanary/prisma/migrations/001_add_stripe_fields.sql +26 -0
  142. package/costcanary/prisma/schema.prisma +326 -0
  143. package/costcanary/prisma/seed-pricing.ts +133 -0
  144. package/costcanary/public/costhawk-logo.png +0 -0
  145. package/costcanary/railway.json +30 -0
  146. package/costcanary/railway.toml +16 -0
  147. package/costcanary/research-nia.md +298 -0
  148. package/costcanary/research.md +411 -0
  149. package/costcanary/scripts/build-production.js +65 -0
  150. package/costcanary/scripts/check-current-pricing.ts +51 -0
  151. package/costcanary/scripts/check-pricing-data.ts +174 -0
  152. package/costcanary/scripts/create-stripe-prices.js +49 -0
  153. package/costcanary/scripts/fix-pricing-data.ts +135 -0
  154. package/costcanary/scripts/fix-pricing-db.ts +148 -0
  155. package/costcanary/scripts/postinstall.js +58 -0
  156. package/costcanary/scripts/railway-deploy.sh +52 -0
  157. package/costcanary/scripts/run-migration.js +61 -0
  158. package/costcanary/scripts/start-production.js +175 -0
  159. package/costcanary/scripts/test-wrapped-key.ts +85 -0
  160. package/costcanary/scripts/validate-deployment.js +176 -0
  161. package/costcanary/scripts/validate-production.js +119 -0
  162. package/costcanary/server.js.backup +38 -0
  163. package/costcanary/tailwind.config.ts +216 -0
  164. package/costcanary/test-pricing-status.sh +27 -0
  165. package/costcanary/tsconfig.json +42 -0
  166. package/docs/sessions/session-2025-12-01.md +570 -0
  167. package/executive-summary.md +302 -0
  168. package/index.js +1 -0
  169. package/nia-sources.md +163 -0
  170. package/package.json +16 -0
  171. 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
+ }