@littlebearapps/platform-admin-sdk 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 (94) hide show
  1. package/README.md +112 -0
  2. package/dist/index.d.ts +16 -0
  3. package/dist/index.js +89 -0
  4. package/dist/prompts.d.ts +27 -0
  5. package/dist/prompts.js +80 -0
  6. package/dist/scaffold.d.ts +5 -0
  7. package/dist/scaffold.js +65 -0
  8. package/dist/templates.d.ts +16 -0
  9. package/dist/templates.js +131 -0
  10. package/package.json +46 -0
  11. package/templates/full/migrations/006_pattern_discovery.sql +199 -0
  12. package/templates/full/migrations/007_notifications_search.sql +127 -0
  13. package/templates/full/workers/lib/pattern-discovery/ai-prompt.ts +644 -0
  14. package/templates/full/workers/lib/pattern-discovery/clustering.ts +278 -0
  15. package/templates/full/workers/lib/pattern-discovery/shadow-evaluation.ts +603 -0
  16. package/templates/full/workers/lib/pattern-discovery/storage.ts +806 -0
  17. package/templates/full/workers/lib/pattern-discovery/types.ts +159 -0
  18. package/templates/full/workers/lib/pattern-discovery/validation.ts +278 -0
  19. package/templates/full/workers/pattern-discovery.ts +661 -0
  20. package/templates/full/workers/platform-alert-router.ts +1809 -0
  21. package/templates/full/workers/platform-notifications.ts +424 -0
  22. package/templates/full/workers/platform-search.ts +480 -0
  23. package/templates/full/workers/platform-settings.ts +436 -0
  24. package/templates/full/wrangler.alert-router.jsonc.hbs +34 -0
  25. package/templates/full/wrangler.notifications.jsonc.hbs +23 -0
  26. package/templates/full/wrangler.pattern-discovery.jsonc.hbs +33 -0
  27. package/templates/full/wrangler.search.jsonc.hbs +16 -0
  28. package/templates/full/wrangler.settings.jsonc.hbs +23 -0
  29. package/templates/shared/README.md.hbs +69 -0
  30. package/templates/shared/config/budgets.yaml.hbs +72 -0
  31. package/templates/shared/config/services.yaml.hbs +45 -0
  32. package/templates/shared/migrations/001_core_tables.sql +117 -0
  33. package/templates/shared/migrations/002_usage_warehouse.sql +830 -0
  34. package/templates/shared/migrations/003_feature_tracking.sql +250 -0
  35. package/templates/shared/migrations/004_settings_alerts.sql +452 -0
  36. package/templates/shared/migrations/seed.sql.hbs +4 -0
  37. package/templates/shared/package.json.hbs +21 -0
  38. package/templates/shared/scripts/sync-config.ts +242 -0
  39. package/templates/shared/tsconfig.json +12 -0
  40. package/templates/shared/workers/lib/analytics-engine.ts +357 -0
  41. package/templates/shared/workers/lib/billing.ts +293 -0
  42. package/templates/shared/workers/lib/circuit-breaker-middleware.ts +25 -0
  43. package/templates/shared/workers/lib/control.ts +292 -0
  44. package/templates/shared/workers/lib/economics.ts +368 -0
  45. package/templates/shared/workers/lib/metrics.ts +103 -0
  46. package/templates/shared/workers/lib/platform-settings.ts +407 -0
  47. package/templates/shared/workers/lib/shared/allowances.ts +333 -0
  48. package/templates/shared/workers/lib/shared/cloudflare.ts +1362 -0
  49. package/templates/shared/workers/lib/shared/types.ts +58 -0
  50. package/templates/shared/workers/lib/telemetry-sampling.ts +360 -0
  51. package/templates/shared/workers/lib/usage/collectors/example.ts +96 -0
  52. package/templates/shared/workers/lib/usage/collectors/index.ts +128 -0
  53. package/templates/shared/workers/lib/usage/handlers/audit.ts +306 -0
  54. package/templates/shared/workers/lib/usage/handlers/backfill.ts +845 -0
  55. package/templates/shared/workers/lib/usage/handlers/behavioral.ts +429 -0
  56. package/templates/shared/workers/lib/usage/handlers/data-queries.ts +507 -0
  57. package/templates/shared/workers/lib/usage/handlers/dlq-admin.ts +364 -0
  58. package/templates/shared/workers/lib/usage/handlers/health-trends.ts +222 -0
  59. package/templates/shared/workers/lib/usage/handlers/index.ts +35 -0
  60. package/templates/shared/workers/lib/usage/handlers/usage-admin.ts +421 -0
  61. package/templates/shared/workers/lib/usage/handlers/usage-features.ts +1262 -0
  62. package/templates/shared/workers/lib/usage/handlers/usage-metrics.ts +2420 -0
  63. package/templates/shared/workers/lib/usage/handlers/usage-settings.ts +610 -0
  64. package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +1032 -0
  65. package/templates/shared/workers/lib/usage/queue/cost-budget-enforcement.ts +128 -0
  66. package/templates/shared/workers/lib/usage/queue/cost-calculator.ts +77 -0
  67. package/templates/shared/workers/lib/usage/queue/dlq-handler.ts +161 -0
  68. package/templates/shared/workers/lib/usage/queue/index.ts +19 -0
  69. package/templates/shared/workers/lib/usage/queue/telemetry-processor.ts +790 -0
  70. package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +732 -0
  71. package/templates/shared/workers/lib/usage/scheduled/data-collection.ts +956 -0
  72. package/templates/shared/workers/lib/usage/scheduled/error-digest.ts +343 -0
  73. package/templates/shared/workers/lib/usage/scheduled/index.ts +18 -0
  74. package/templates/shared/workers/lib/usage/scheduled/rollups.ts +1561 -0
  75. package/templates/shared/workers/lib/usage/shared/constants.ts +362 -0
  76. package/templates/shared/workers/lib/usage/shared/index.ts +14 -0
  77. package/templates/shared/workers/lib/usage/shared/types.ts +1066 -0
  78. package/templates/shared/workers/lib/usage/shared/utils.ts +795 -0
  79. package/templates/shared/workers/platform-usage.ts +1915 -0
  80. package/templates/shared/wrangler.usage.jsonc.hbs +58 -0
  81. package/templates/standard/migrations/005_error_collection.sql +162 -0
  82. package/templates/standard/workers/error-collector.ts +2670 -0
  83. package/templates/standard/workers/lib/error-collector/capture.ts +213 -0
  84. package/templates/standard/workers/lib/error-collector/digest.ts +448 -0
  85. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +262 -0
  86. package/templates/standard/workers/lib/error-collector/fingerprint.ts +258 -0
  87. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +293 -0
  88. package/templates/standard/workers/lib/error-collector/github.ts +329 -0
  89. package/templates/standard/workers/lib/error-collector/types.ts +262 -0
  90. package/templates/standard/workers/lib/sentinel/gap-detection.ts +734 -0
  91. package/templates/standard/workers/lib/shared/slack-alerts.ts +585 -0
  92. package/templates/standard/workers/platform-sentinel.ts +1744 -0
  93. package/templates/standard/wrangler.error-collector.jsonc.hbs +44 -0
  94. package/templates/standard/wrangler.sentinel.jsonc.hbs +45 -0
@@ -0,0 +1,507 @@
1
+ /**
2
+ * Data Query Functions for Usage Handlers
3
+ *
4
+ * D1 and KV access functions used by handler modules.
5
+ * Extracted from platform-usage.ts as part of Phase B migration.
6
+ */
7
+
8
+ import type { Env, TimePeriod, ProjectedBurn, DailyCostData, CostBreakdown } from '../shared';
9
+ import type { AIGatewaySummary } from '../../shared/cloudflare';
10
+
11
+ // =============================================================================
12
+ // PRICING VERSION CACHE
13
+ // =============================================================================
14
+
15
+ // In-memory cache for pricing version ID (per-request lifetime)
16
+ let cachedPricingVersionId: number | null = null;
17
+
18
+ /**
19
+ * Get current pricing version ID from D1.
20
+ * Caches result for the lifetime of the request.
21
+ */
22
+ export async function getCurrentPricingVersionId(env: Env): Promise<number | null> {
23
+ if (cachedPricingVersionId !== null) {
24
+ return cachedPricingVersionId;
25
+ }
26
+
27
+ try {
28
+ const result = await env.PLATFORM_DB.prepare(
29
+ `SELECT id FROM pricing_versions WHERE effective_to IS NULL ORDER BY effective_from DESC LIMIT 1`
30
+ ).first<{ id: number }>();
31
+
32
+ cachedPricingVersionId = result?.id ?? null;
33
+ return cachedPricingVersionId;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Reset cached pricing version ID.
41
+ */
42
+ export function resetPricingVersionCache(): void {
43
+ cachedPricingVersionId = null;
44
+ }
45
+
46
+ // =============================================================================
47
+ // D1 USAGE DATA QUERIES
48
+ // =============================================================================
49
+
50
+ /**
51
+ * Query D1 for aggregated usage data over a time period.
52
+ * Uses hourly snapshots for 24h period, daily rollups for 7d/30d.
53
+ */
54
+ export async function queryD1UsageData(
55
+ env: Env,
56
+ period: TimePeriod,
57
+ project: string
58
+ ): Promise<{ costs: CostBreakdown; rowCount: number } | null> {
59
+ try {
60
+ const isHourly = period === '24h';
61
+ const table = isHourly ? 'hourly_usage_snapshots' : 'daily_usage_rollups';
62
+ const dateCol = isHourly ? 'snapshot_hour' : 'snapshot_date';
63
+
64
+ const now = new Date();
65
+ let startFilter: string;
66
+
67
+ if (period === '24h') {
68
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
69
+ startFilter = yesterday.toISOString().slice(0, 13) + ':00:00Z';
70
+ } else if (period === '7d') {
71
+ const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
72
+ startFilter = weekAgo.toISOString().slice(0, 10);
73
+ } else {
74
+ const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
75
+ startFilter = monthAgo.toISOString().slice(0, 10);
76
+ }
77
+
78
+ const result = await env.PLATFORM_DB.prepare(
79
+ `
80
+ SELECT
81
+ SUM(workers_cost_usd) as workers_cost,
82
+ SUM(d1_cost_usd) as d1_cost,
83
+ SUM(kv_cost_usd) as kv_cost,
84
+ SUM(r2_cost_usd) as r2_cost,
85
+ SUM(do_cost_usd) as do_cost,
86
+ SUM(vectorize_cost_usd) as vectorize_cost,
87
+ SUM(aigateway_cost_usd) as aigateway_cost,
88
+ SUM(pages_cost_usd) as pages_cost,
89
+ SUM(queues_cost_usd) as queues_cost,
90
+ SUM(workersai_cost_usd) as workersai_cost,
91
+ SUM(total_cost_usd) as total_cost,
92
+ COUNT(*) as row_count
93
+ FROM ${table}
94
+ WHERE ${dateCol} >= ?
95
+ AND project = ?
96
+ `
97
+ )
98
+ .bind(startFilter, project)
99
+ .first<{
100
+ workers_cost: number | null;
101
+ d1_cost: number | null;
102
+ kv_cost: number | null;
103
+ r2_cost: number | null;
104
+ do_cost: number | null;
105
+ vectorize_cost: number | null;
106
+ aigateway_cost: number | null;
107
+ pages_cost: number | null;
108
+ queues_cost: number | null;
109
+ workersai_cost: number | null;
110
+ total_cost: number | null;
111
+ row_count: number;
112
+ }>();
113
+
114
+ if (!result || result.row_count === 0) {
115
+ return null;
116
+ }
117
+
118
+ return {
119
+ costs: {
120
+ workers: result.workers_cost ?? 0,
121
+ d1: result.d1_cost ?? 0,
122
+ kv: result.kv_cost ?? 0,
123
+ r2: result.r2_cost ?? 0,
124
+ durableObjects: result.do_cost ?? 0,
125
+ vectorize: result.vectorize_cost ?? 0,
126
+ aiGateway: result.aigateway_cost ?? 0,
127
+ pages: result.pages_cost ?? 0,
128
+ queues: result.queues_cost ?? 0,
129
+ workersAI: result.workersai_cost ?? 0,
130
+ workflows: 0,
131
+ total: result.total_cost ?? 0,
132
+ },
133
+ rowCount: result.row_count,
134
+ };
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Query D1 for daily cost breakdown (for charts).
142
+ */
143
+ export async function queryD1DailyCosts(
144
+ env: Env,
145
+ period: TimePeriod | { start: string; end: string },
146
+ project: string = 'all'
147
+ ): Promise<DailyCostData | null> {
148
+ try {
149
+ let startDate: string;
150
+ let endDate: string;
151
+
152
+ if (typeof period === 'object') {
153
+ startDate = period.start;
154
+ endDate = period.end;
155
+ } else {
156
+ const now = new Date();
157
+ endDate = now.toISOString().slice(0, 10);
158
+
159
+ if (period === '24h') {
160
+ startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
161
+ } else if (period === '7d') {
162
+ startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
163
+ } else {
164
+ startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
165
+ }
166
+ }
167
+
168
+ // When project is 'all', sum across individual projects instead of using the 'all' row
169
+ // This ensures we get complete data even if the 'all' rollup row is missing for some dates
170
+ const isAllProjects = project === 'all';
171
+ const query = isAllProjects
172
+ ? `
173
+ SELECT
174
+ snapshot_date as date,
175
+ SUM(workers_cost_usd) as workers,
176
+ SUM(d1_cost_usd) as d1,
177
+ SUM(kv_cost_usd) as kv,
178
+ SUM(r2_cost_usd) as r2,
179
+ SUM(do_cost_usd) as durableObjects,
180
+ SUM(vectorize_cost_usd) as vectorize,
181
+ SUM(aigateway_cost_usd) as aiGateway,
182
+ SUM(pages_cost_usd) as pages,
183
+ SUM(queues_cost_usd) as queues,
184
+ SUM(workersai_cost_usd) as workersAI,
185
+ SUM(total_cost_usd) as total,
186
+ MAX(COALESCE(rollup_version, 1)) as rollupVersion
187
+ FROM daily_usage_rollups
188
+ WHERE snapshot_date >= ?
189
+ AND snapshot_date <= ?
190
+ AND project NOT IN ('all', '_unattributed')
191
+ GROUP BY snapshot_date
192
+ ORDER BY snapshot_date ASC
193
+ `
194
+ : `
195
+ SELECT
196
+ snapshot_date as date,
197
+ workers_cost_usd as workers,
198
+ d1_cost_usd as d1,
199
+ kv_cost_usd as kv,
200
+ r2_cost_usd as r2,
201
+ do_cost_usd as durableObjects,
202
+ vectorize_cost_usd as vectorize,
203
+ aigateway_cost_usd as aiGateway,
204
+ pages_cost_usd as pages,
205
+ queues_cost_usd as queues,
206
+ workersai_cost_usd as workersAI,
207
+ total_cost_usd as total,
208
+ COALESCE(rollup_version, 1) as rollupVersion
209
+ FROM daily_usage_rollups
210
+ WHERE snapshot_date >= ?
211
+ AND snapshot_date <= ?
212
+ AND project = ?
213
+ ORDER BY snapshot_date ASC
214
+ `;
215
+
216
+ const result = await env.PLATFORM_DB.prepare(query)
217
+ .bind(startDate, endDate, ...(isAllProjects ? [] : [project]))
218
+ .all<{
219
+ date: string;
220
+ workers: number;
221
+ d1: number;
222
+ kv: number;
223
+ r2: number;
224
+ durableObjects: number;
225
+ vectorize: number;
226
+ aiGateway: number;
227
+ pages: number;
228
+ queues: number;
229
+ workersAI: number;
230
+ total: number;
231
+ rollupVersion: number;
232
+ }>();
233
+
234
+ if (!result.results || result.results.length === 0) {
235
+ return null;
236
+ }
237
+
238
+ const days = result.results.map((r) => ({
239
+ date: r.date,
240
+ workers: r.workers ?? 0,
241
+ d1: r.d1 ?? 0,
242
+ kv: r.kv ?? 0,
243
+ r2: r.r2 ?? 0,
244
+ durableObjects: r.durableObjects ?? 0,
245
+ vectorize: r.vectorize ?? 0,
246
+ aiGateway: r.aiGateway ?? 0,
247
+ workersAI: r.workersAI ?? 0,
248
+ pages: 0,
249
+ queues: r.queues ?? 0,
250
+ workflows: 0,
251
+ total: r.total ?? 0,
252
+ rollupVersion: r.rollupVersion ?? 1,
253
+ }));
254
+
255
+ const hasLegacyData = result.results.some((r) => (r.rollupVersion ?? 1) === 1);
256
+
257
+ const totals = {
258
+ workers: days.reduce((sum, d) => sum + d.workers, 0),
259
+ d1: days.reduce((sum, d) => sum + d.d1, 0),
260
+ kv: days.reduce((sum, d) => sum + d.kv, 0),
261
+ r2: days.reduce((sum, d) => sum + d.r2, 0),
262
+ durableObjects: days.reduce((sum, d) => sum + d.durableObjects, 0),
263
+ vectorize: days.reduce((sum, d) => sum + d.vectorize, 0),
264
+ aiGateway: days.reduce((sum, d) => sum + d.aiGateway, 0),
265
+ workersAI: days.reduce((sum, d) => sum + d.workersAI, 0),
266
+ pages: 0,
267
+ queues: days.reduce((sum, d) => sum + d.queues, 0),
268
+ workflows: 0,
269
+ total: days.reduce((sum, d) => sum + d.total, 0),
270
+ };
271
+
272
+ return {
273
+ days,
274
+ totals,
275
+ period: { start: startDate, end: endDate },
276
+ hasLegacyData,
277
+ };
278
+ } catch {
279
+ return null;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Calculate projected monthly burn based on current month's data.
285
+ */
286
+ export async function calculateProjectedBurn(
287
+ env: Env,
288
+ project: string = 'all'
289
+ ): Promise<ProjectedBurn> {
290
+ const now = new Date();
291
+ const currentMonth = now.toISOString().slice(0, 7);
292
+ const dayOfMonth = now.getDate();
293
+ const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
294
+
295
+ try {
296
+ const currentMonthResult = await env.PLATFORM_DB.prepare(
297
+ `
298
+ SELECT
299
+ SUM(total_cost_usd) as total_cost,
300
+ COUNT(*) as days_count
301
+ FROM daily_usage_rollups
302
+ WHERE snapshot_date LIKE ?
303
+ AND project = ?
304
+ `
305
+ )
306
+ .bind(`${currentMonth}%`, project)
307
+ .first<{ total_cost: number | null; days_count: number }>();
308
+
309
+ const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
310
+ const lastMonthStr = lastMonth.toISOString().slice(0, 7);
311
+ const lastMonthResult = await env.PLATFORM_DB.prepare(
312
+ `
313
+ SELECT SUM(total_cost_usd) as total_cost
314
+ FROM daily_usage_rollups
315
+ WHERE snapshot_date LIKE ?
316
+ AND project = ?
317
+ `
318
+ )
319
+ .bind(`${lastMonthStr}%`, project)
320
+ .first<{ total_cost: number | null }>();
321
+
322
+ const currentDays = currentMonthResult?.days_count ?? 0;
323
+ const currentCost = currentMonthResult?.total_cost ?? 0;
324
+ const lastMonthCost = lastMonthResult?.total_cost ?? null;
325
+
326
+ const dailyBurnRate = currentDays > 0 ? currentCost / currentDays : 0;
327
+ const daysRemaining = daysInMonth - dayOfMonth;
328
+ const projectedMonthlyCost = currentCost + dailyBurnRate * daysRemaining;
329
+
330
+ let projectedVsLastMonthPct: number | null = null;
331
+ if (lastMonthCost && lastMonthCost > 0) {
332
+ projectedVsLastMonthPct = ((projectedMonthlyCost - lastMonthCost) / lastMonthCost) * 100;
333
+ }
334
+
335
+ let confidence: 'low' | 'medium' | 'high';
336
+ if (currentDays >= 20) {
337
+ confidence = 'high';
338
+ } else if (currentDays >= 10) {
339
+ confidence = 'medium';
340
+ } else {
341
+ confidence = 'low';
342
+ }
343
+
344
+ return {
345
+ currentPeriodDays: currentDays,
346
+ currentPeriodCost: currentCost,
347
+ dailyBurnRate,
348
+ projectedMonthlyCost,
349
+ projectedVsLastMonthPct,
350
+ lastMonthCost,
351
+ confidence,
352
+ };
353
+ } catch {
354
+ return {
355
+ currentPeriodDays: 0,
356
+ currentPeriodCost: 0,
357
+ dailyBurnRate: 0,
358
+ projectedMonthlyCost: 0,
359
+ projectedVsLastMonthPct: null,
360
+ lastMonthCost: null,
361
+ confidence: 'low',
362
+ };
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Query AI Gateway aggregated metrics from D1.
368
+ */
369
+ export async function queryAIGatewayMetrics(
370
+ env: Env,
371
+ period: TimePeriod
372
+ ): Promise<AIGatewaySummary | null> {
373
+ try {
374
+ const now = new Date();
375
+ let startDate: string;
376
+
377
+ switch (period) {
378
+ case '24h':
379
+ startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
380
+ break;
381
+ case '7d':
382
+ startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
383
+ break;
384
+ case '30d':
385
+ default:
386
+ startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
387
+ break;
388
+ }
389
+ const endDate = now.toISOString().slice(0, 10);
390
+
391
+ const totalsResult = await env.PLATFORM_DB.prepare(
392
+ `
393
+ SELECT
394
+ COALESCE(SUM(requests), 0) as total_requests,
395
+ COALESCE(SUM(cached_requests), 0) as total_cached,
396
+ COALESCE(SUM(tokens_in), 0) as tokens_in,
397
+ COALESCE(SUM(tokens_out), 0) as tokens_out,
398
+ COALESCE(SUM(cost_usd), 0) as total_cost
399
+ FROM aigateway_model_daily
400
+ WHERE snapshot_date >= ? AND snapshot_date <= ?
401
+ `
402
+ )
403
+ .bind(startDate, endDate)
404
+ .first<{
405
+ total_requests: number;
406
+ total_cached: number;
407
+ tokens_in: number;
408
+ tokens_out: number;
409
+ total_cost: number;
410
+ }>();
411
+
412
+ if (!totalsResult || totalsResult.total_requests === 0) {
413
+ return null;
414
+ }
415
+
416
+ const byProviderResult = await env.PLATFORM_DB.prepare(
417
+ `
418
+ SELECT
419
+ provider,
420
+ SUM(requests) as requests,
421
+ SUM(cached_requests) as cached_requests,
422
+ SUM(tokens_in) as tokens_in,
423
+ SUM(tokens_out) as tokens_out,
424
+ SUM(cost_usd) as cost_usd
425
+ FROM aigateway_model_daily
426
+ WHERE snapshot_date >= ? AND snapshot_date <= ?
427
+ GROUP BY provider
428
+ ORDER BY requests DESC
429
+ `
430
+ )
431
+ .bind(startDate, endDate)
432
+ .all<{
433
+ provider: string;
434
+ requests: number;
435
+ cached_requests: number;
436
+ tokens_in: number;
437
+ tokens_out: number;
438
+ cost_usd: number;
439
+ }>();
440
+
441
+ const byModelResult = await env.PLATFORM_DB.prepare(
442
+ `
443
+ SELECT
444
+ model,
445
+ SUM(requests) as requests,
446
+ SUM(cached_requests) as cached_requests,
447
+ SUM(tokens_in) as tokens_in,
448
+ SUM(tokens_out) as tokens_out,
449
+ SUM(cost_usd) as cost_usd
450
+ FROM aigateway_model_daily
451
+ WHERE snapshot_date >= ? AND snapshot_date <= ?
452
+ GROUP BY model
453
+ ORDER BY requests DESC
454
+ LIMIT 20
455
+ `
456
+ )
457
+ .bind(startDate, endDate)
458
+ .all<{
459
+ model: string;
460
+ requests: number;
461
+ cached_requests: number;
462
+ tokens_in: number;
463
+ tokens_out: number;
464
+ cost_usd: number;
465
+ }>();
466
+
467
+ const byProvider: AIGatewaySummary['byProvider'] = {};
468
+ for (const row of byProviderResult.results ?? []) {
469
+ byProvider[row.provider] = {
470
+ requests: row.requests,
471
+ cachedRequests: row.cached_requests,
472
+ tokensIn: row.tokens_in,
473
+ tokensOut: row.tokens_out,
474
+ costUsd: row.cost_usd,
475
+ };
476
+ }
477
+
478
+ const byModel: AIGatewaySummary['byModel'] = {};
479
+ for (const row of byModelResult.results ?? []) {
480
+ byModel[row.model] = {
481
+ requests: row.requests,
482
+ cachedRequests: row.cached_requests,
483
+ tokensIn: row.tokens_in,
484
+ tokensOut: row.tokens_out,
485
+ costUsd: row.cost_usd,
486
+ };
487
+ }
488
+
489
+ const cacheHitRate =
490
+ totalsResult.total_requests > 0
491
+ ? (totalsResult.total_cached / totalsResult.total_requests) * 100
492
+ : 0;
493
+
494
+ return {
495
+ totalRequests: totalsResult.total_requests,
496
+ totalCachedRequests: totalsResult.total_cached,
497
+ cacheHitRate: Math.round(cacheHitRate * 100) / 100,
498
+ tokensIn: totalsResult.tokens_in,
499
+ tokensOut: totalsResult.tokens_out,
500
+ totalCostUsd: totalsResult.total_cost,
501
+ byProvider,
502
+ byModel,
503
+ };
504
+ } catch {
505
+ return null;
506
+ }
507
+ }