@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.
- package/README.md +112 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +89 -0
- package/dist/prompts.d.ts +27 -0
- package/dist/prompts.js +80 -0
- package/dist/scaffold.d.ts +5 -0
- package/dist/scaffold.js +65 -0
- package/dist/templates.d.ts +16 -0
- package/dist/templates.js +131 -0
- package/package.json +46 -0
- package/templates/full/migrations/006_pattern_discovery.sql +199 -0
- package/templates/full/migrations/007_notifications_search.sql +127 -0
- package/templates/full/workers/lib/pattern-discovery/ai-prompt.ts +644 -0
- package/templates/full/workers/lib/pattern-discovery/clustering.ts +278 -0
- package/templates/full/workers/lib/pattern-discovery/shadow-evaluation.ts +603 -0
- package/templates/full/workers/lib/pattern-discovery/storage.ts +806 -0
- package/templates/full/workers/lib/pattern-discovery/types.ts +159 -0
- package/templates/full/workers/lib/pattern-discovery/validation.ts +278 -0
- package/templates/full/workers/pattern-discovery.ts +661 -0
- package/templates/full/workers/platform-alert-router.ts +1809 -0
- package/templates/full/workers/platform-notifications.ts +424 -0
- package/templates/full/workers/platform-search.ts +480 -0
- package/templates/full/workers/platform-settings.ts +436 -0
- package/templates/full/wrangler.alert-router.jsonc.hbs +34 -0
- package/templates/full/wrangler.notifications.jsonc.hbs +23 -0
- package/templates/full/wrangler.pattern-discovery.jsonc.hbs +33 -0
- package/templates/full/wrangler.search.jsonc.hbs +16 -0
- package/templates/full/wrangler.settings.jsonc.hbs +23 -0
- package/templates/shared/README.md.hbs +69 -0
- package/templates/shared/config/budgets.yaml.hbs +72 -0
- package/templates/shared/config/services.yaml.hbs +45 -0
- package/templates/shared/migrations/001_core_tables.sql +117 -0
- package/templates/shared/migrations/002_usage_warehouse.sql +830 -0
- package/templates/shared/migrations/003_feature_tracking.sql +250 -0
- package/templates/shared/migrations/004_settings_alerts.sql +452 -0
- package/templates/shared/migrations/seed.sql.hbs +4 -0
- package/templates/shared/package.json.hbs +21 -0
- package/templates/shared/scripts/sync-config.ts +242 -0
- package/templates/shared/tsconfig.json +12 -0
- package/templates/shared/workers/lib/analytics-engine.ts +357 -0
- package/templates/shared/workers/lib/billing.ts +293 -0
- package/templates/shared/workers/lib/circuit-breaker-middleware.ts +25 -0
- package/templates/shared/workers/lib/control.ts +292 -0
- package/templates/shared/workers/lib/economics.ts +368 -0
- package/templates/shared/workers/lib/metrics.ts +103 -0
- package/templates/shared/workers/lib/platform-settings.ts +407 -0
- package/templates/shared/workers/lib/shared/allowances.ts +333 -0
- package/templates/shared/workers/lib/shared/cloudflare.ts +1362 -0
- package/templates/shared/workers/lib/shared/types.ts +58 -0
- package/templates/shared/workers/lib/telemetry-sampling.ts +360 -0
- package/templates/shared/workers/lib/usage/collectors/example.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +128 -0
- package/templates/shared/workers/lib/usage/handlers/audit.ts +306 -0
- package/templates/shared/workers/lib/usage/handlers/backfill.ts +845 -0
- package/templates/shared/workers/lib/usage/handlers/behavioral.ts +429 -0
- package/templates/shared/workers/lib/usage/handlers/data-queries.ts +507 -0
- package/templates/shared/workers/lib/usage/handlers/dlq-admin.ts +364 -0
- package/templates/shared/workers/lib/usage/handlers/health-trends.ts +222 -0
- package/templates/shared/workers/lib/usage/handlers/index.ts +35 -0
- package/templates/shared/workers/lib/usage/handlers/usage-admin.ts +421 -0
- package/templates/shared/workers/lib/usage/handlers/usage-features.ts +1262 -0
- package/templates/shared/workers/lib/usage/handlers/usage-metrics.ts +2420 -0
- package/templates/shared/workers/lib/usage/handlers/usage-settings.ts +610 -0
- package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +1032 -0
- package/templates/shared/workers/lib/usage/queue/cost-budget-enforcement.ts +128 -0
- package/templates/shared/workers/lib/usage/queue/cost-calculator.ts +77 -0
- package/templates/shared/workers/lib/usage/queue/dlq-handler.ts +161 -0
- package/templates/shared/workers/lib/usage/queue/index.ts +19 -0
- package/templates/shared/workers/lib/usage/queue/telemetry-processor.ts +790 -0
- package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +732 -0
- package/templates/shared/workers/lib/usage/scheduled/data-collection.ts +956 -0
- package/templates/shared/workers/lib/usage/scheduled/error-digest.ts +343 -0
- package/templates/shared/workers/lib/usage/scheduled/index.ts +18 -0
- package/templates/shared/workers/lib/usage/scheduled/rollups.ts +1561 -0
- package/templates/shared/workers/lib/usage/shared/constants.ts +362 -0
- package/templates/shared/workers/lib/usage/shared/index.ts +14 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +1066 -0
- package/templates/shared/workers/lib/usage/shared/utils.ts +795 -0
- package/templates/shared/workers/platform-usage.ts +1915 -0
- package/templates/shared/wrangler.usage.jsonc.hbs +58 -0
- package/templates/standard/migrations/005_error_collection.sql +162 -0
- package/templates/standard/workers/error-collector.ts +2670 -0
- package/templates/standard/workers/lib/error-collector/capture.ts +213 -0
- package/templates/standard/workers/lib/error-collector/digest.ts +448 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +262 -0
- package/templates/standard/workers/lib/error-collector/fingerprint.ts +258 -0
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +293 -0
- package/templates/standard/workers/lib/error-collector/github.ts +329 -0
- package/templates/standard/workers/lib/error-collector/types.ts +262 -0
- package/templates/standard/workers/lib/sentinel/gap-detection.ts +734 -0
- package/templates/standard/workers/lib/shared/slack-alerts.ts +585 -0
- package/templates/standard/workers/platform-sentinel.ts +1744 -0
- package/templates/standard/wrangler.error-collector.jsonc.hbs +44 -0
- 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
|
+
}
|