ldrouter 1.18.2 → 1.19.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/CHANGELOG.md +20 -0
- package/README.md +15 -1
- package/dist/server/backup/container.js +99 -0
- package/dist/server/backup/export.js +99 -0
- package/dist/server/backup/restore-upload.js +333 -0
- package/dist/server/backup/stats.js +15 -0
- package/dist/server/config/index.js +4 -0
- package/dist/server/db/schema.js +18 -1
- package/dist/server/gateway/runner.js +73 -59
- package/dist/server/routes/admin/backup.js +177 -191
- package/dist/server/routes/admin/stats.js +129 -61
- package/dist/web/assets/index-BdbskFHA.js +349 -0
- package/dist/web/index.html +1 -1
- package/migrations/0011_daily_aggregate_stats.sql +60 -0
- package/package.json +1 -1
- package/dist/web/assets/index-DYrnLQwk.js +0 -349
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { and, eq, gte, lte, sql, desc } from 'drizzle-orm';
|
|
3
3
|
import { getDb, schema } from '../../db/index.js';
|
|
4
4
|
import { requireAdminAuth } from '../../auth/middleware.js';
|
|
5
|
+
import { GatewayError } from '../../errors.js';
|
|
5
6
|
import { toSummary, loadSummaryMaps } from './requests.js';
|
|
6
7
|
const PRESETS = {
|
|
7
8
|
today: () => {
|
|
@@ -62,6 +63,19 @@ export async function registerStatsRoutes(app) {
|
|
|
62
63
|
const fromIso = from.toISOString();
|
|
63
64
|
const toIso = to.toISOString();
|
|
64
65
|
const conds = [gte(schema.requests.createdAt, fromIso), lte(schema.requests.createdAt, toIso)];
|
|
66
|
+
const utcToday = new Date().toISOString().slice(0, 10);
|
|
67
|
+
const aggregateRows = db.select().from(schema.dailyModelStats).where(and(gte(schema.dailyModelStats.day, fromIso.slice(0, 10)), lte(schema.dailyModelStats.day, toIso.slice(0, 10)))).all();
|
|
68
|
+
const overlappingAggregateDays = [...new Set(aggregateRows.map((r) => r.day))].filter((day) => day < utcToday && `${day}T00:00:00.000Z` <= toIso && `${day}T23:59:59.999Z` >= fromIso);
|
|
69
|
+
const aggregateDays = overlappingAggregateDays.filter((day) => `${day}T00:00:00.000Z` >= fromIso && `${day}T23:59:59.999Z` <= toIso);
|
|
70
|
+
if (aggregateDays.length !== overlappingAggregateDays.length) {
|
|
71
|
+
throw new GatewayError('invalid_request_error', 'Statistics ranges overlapping compacted history must include complete UTC days.', { status: 400 });
|
|
72
|
+
}
|
|
73
|
+
const percentilesAvailable = aggregateDays.length === 0;
|
|
74
|
+
const aggregateOnly = aggregateDays.length > 0;
|
|
75
|
+
const rawConds = aggregateDays.length
|
|
76
|
+
? and(...conds, sql `substr(created_at,1,10) NOT IN (${sql.join(aggregateDays.map((day) => sql `${day}`), sql `, `)})`)
|
|
77
|
+
: and(...conds);
|
|
78
|
+
const aggregateTotals = aggregateRows.filter((r) => aggregateDays.includes(r.day)).reduce((a, r) => ({ total: a.total + r.requests, success: a.success + r.successfulRequests, failed: a.failed + r.failedRequests, input: a.input + r.inputTokens, output: a.output + r.outputTokens, cacheRead: a.cacheRead + r.cacheReadTokens, cacheWrite: a.cacheWrite + r.cacheWriteTokens, reasoning: a.reasoning + r.reasoningTokens, gateway: a.gateway + r.gatewayCacheHits, fallback: a.fallback + r.fallbackRequests, latencySum: a.latencySum + r.latencySuccessSumMs, latencyCount: a.latencyCount + r.latencySuccessCount, ttftSum: a.ttftSum + r.ttftSumMs, ttftCount: a.ttftCount + r.ttftCount }), { total: 0, success: 0, failed: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, gateway: 0, fallback: 0, latencySum: 0, latencyCount: 0, ttftSum: 0, ttftCount: 0 });
|
|
65
79
|
const summary = db
|
|
66
80
|
.select({
|
|
67
81
|
total: sql `COUNT(*)`,
|
|
@@ -76,13 +90,14 @@ export async function registerStatsRoutes(app) {
|
|
|
76
90
|
fallbacks: sql `SUM(CASE WHEN attempts_count > 1 THEN 1 ELSE 0 END)`,
|
|
77
91
|
})
|
|
78
92
|
.from(schema.requests)
|
|
79
|
-
.where(
|
|
93
|
+
.where(rawConds)
|
|
80
94
|
.get();
|
|
81
|
-
|
|
95
|
+
// Aggregate rows retain cache counts but not provider-specific prompt denominator semantics.
|
|
96
|
+
const cacheHitRate = aggregateDays.length ? null : cacheHitRateFor(db, conds);
|
|
82
97
|
const latRows = db
|
|
83
98
|
.select({ v: schema.requests.totalLatencyMs })
|
|
84
99
|
.from(schema.requests)
|
|
85
|
-
.where(and(
|
|
100
|
+
.where(and(rawConds, eq(schema.requests.success, true)))
|
|
86
101
|
.all();
|
|
87
102
|
const latencies = latRows.map((r) => r.v).filter((v) => v > 0).sort((a, b) => a - b);
|
|
88
103
|
const avg = latencies.length ? latencies.reduce((a, b) => a + b, 0) / latencies.length : 0;
|
|
@@ -90,32 +105,32 @@ export async function registerStatsRoutes(app) {
|
|
|
90
105
|
const ttftRows = db
|
|
91
106
|
.select({ v: schema.requests.ttftMs })
|
|
92
107
|
.from(schema.requests)
|
|
93
|
-
.where(and(
|
|
108
|
+
.where(and(rawConds, sql `ttft_ms IS NOT NULL`))
|
|
94
109
|
.all();
|
|
95
110
|
const ttfts = ttftRows.map((r) => r.v).filter((v) => v !== null).sort((a, b) => a - b);
|
|
96
111
|
const avgTtft = ttfts.length ? ttfts.reduce((a, b) => a + b, 0) / ttfts.length : null;
|
|
97
112
|
const p95Ttft = ttfts.length ? percentile(ttfts, 95) : null;
|
|
98
|
-
const total = Number(summary?.total ?? 0);
|
|
99
|
-
const success = Number(summary?.success ?? 0);
|
|
100
|
-
const failed = Number(summary?.failed ?? 0);
|
|
113
|
+
const total = Number(summary?.total ?? 0) + aggregateTotals.total;
|
|
114
|
+
const success = Number(summary?.success ?? 0) + aggregateTotals.success;
|
|
115
|
+
const failed = Number(summary?.failed ?? 0) + aggregateTotals.failed;
|
|
101
116
|
const statsSummary = {
|
|
102
117
|
totalRequests: total,
|
|
103
118
|
successfulRequests: success,
|
|
104
119
|
failedRequests: failed,
|
|
105
120
|
successRate: total ? success / total : 0,
|
|
106
|
-
inputTokens: Number(summary?.inputTokens ?? 0),
|
|
107
|
-
outputTokens: Number(summary?.outputTokens ?? 0),
|
|
108
|
-
totalTokens: Number(summary?.inputTokens ?? 0) + Number(summary?.outputTokens ?? 0),
|
|
109
|
-
cacheReadTokens: Number(summary?.cacheRead ?? 0),
|
|
110
|
-
cacheWriteTokens: Number(summary?.cacheWrite ?? 0),
|
|
111
|
-
reasoningTokens: Number(summary?.reasoning ?? 0),
|
|
112
|
-
averageLatencyMs: avg,
|
|
113
|
-
p95LatencyMs: p95,
|
|
114
|
-
averageTtftMs: avgTtft,
|
|
115
|
-
p95TtftMs: p95Ttft,
|
|
121
|
+
inputTokens: Number(summary?.inputTokens ?? 0) + aggregateTotals.input,
|
|
122
|
+
outputTokens: Number(summary?.outputTokens ?? 0) + aggregateTotals.output,
|
|
123
|
+
totalTokens: Number(summary?.inputTokens ?? 0) + aggregateTotals.input + Number(summary?.outputTokens ?? 0) + aggregateTotals.output,
|
|
124
|
+
cacheReadTokens: Number(summary?.cacheRead ?? 0) + aggregateTotals.cacheRead,
|
|
125
|
+
cacheWriteTokens: Number(summary?.cacheWrite ?? 0) + aggregateTotals.cacheWrite,
|
|
126
|
+
reasoningTokens: Number(summary?.reasoning ?? 0) + aggregateTotals.reasoning,
|
|
127
|
+
averageLatencyMs: aggregateOnly ? (aggregateTotals.latencySum + latencies.reduce((a, b) => a + b, 0)) / (aggregateTotals.latencyCount + latencies.length || 1) : avg,
|
|
128
|
+
p95LatencyMs: percentilesAvailable ? p95 : null,
|
|
129
|
+
averageTtftMs: aggregateOnly ? (aggregateTotals.ttftSum + ttfts.reduce((a, b) => a + b, 0)) / (aggregateTotals.ttftCount + ttfts.length || 1) : avgTtft,
|
|
130
|
+
p95TtftMs: percentilesAvailable ? p95Ttft : null,
|
|
116
131
|
cacheHitRate,
|
|
117
|
-
gatewayCacheHitRate: total ? Number(summary?.gatewayCacheHits ?? 0) / total : 0,
|
|
118
|
-
fallbackRate: total ? Number(summary?.fallbacks ?? 0) / total : 0,
|
|
132
|
+
gatewayCacheHitRate: total ? (Number(summary?.gatewayCacheHits ?? 0) + aggregateTotals.gateway) / total : 0,
|
|
133
|
+
fallbackRate: total ? (Number(summary?.fallbacks ?? 0) + aggregateTotals.fallback) / total : 0,
|
|
119
134
|
};
|
|
120
135
|
// Time-bucketed series
|
|
121
136
|
const bucketExpr = bucket === 'hour' ? sql `strftime('%Y-%m-%dT%H:00:00Z', created_at)` : sql `strftime('%Y-%m-%dT00:00:00Z', created_at)`;
|
|
@@ -130,10 +145,18 @@ export async function registerStatsRoutes(app) {
|
|
|
130
145
|
cacheRead: sql `COALESCE(SUM(cache_read_tokens),0)`,
|
|
131
146
|
})
|
|
132
147
|
.from(schema.requests)
|
|
133
|
-
.where(
|
|
148
|
+
.where(rawConds)
|
|
134
149
|
.groupBy(bucketExpr)
|
|
135
150
|
.orderBy(bucketExpr)
|
|
136
151
|
.all();
|
|
152
|
+
const seriesMap = new Map(seriesRows.map((r) => [r.t, { t: r.t, requests: Number(r.requests), errors: Number(r.errors), inputTokens: Number(r.inputTokens), outputTokens: Number(r.outputTokens), avgLatency: Number(r.avgLatency ?? 0), cacheRead: Number(r.cacheRead ?? 0) }]));
|
|
153
|
+
for (const r of aggregateRows.filter((x) => aggregateDays.includes(x.day))) {
|
|
154
|
+
const t = `${r.day}T00:00:00Z`;
|
|
155
|
+
const old = seriesMap.get(t) ?? { t, requests: 0, errors: 0, inputTokens: 0, outputTokens: 0, avgLatency: 0, cacheRead: 0 };
|
|
156
|
+
const oldSuccesses = old.requests - old.errors;
|
|
157
|
+
const count = oldSuccesses + r.latencySuccessCount;
|
|
158
|
+
seriesMap.set(t, { t, requests: old.requests + r.requests, errors: old.errors + r.failedRequests, inputTokens: old.inputTokens + r.inputTokens, outputTokens: old.outputTokens + r.outputTokens, avgLatency: count ? (old.avgLatency * oldSuccesses + r.latencySuccessSumMs) / count : 0, cacheRead: old.cacheRead + r.cacheReadTokens });
|
|
159
|
+
}
|
|
137
160
|
// Top models
|
|
138
161
|
const topModels = db
|
|
139
162
|
.select({
|
|
@@ -143,7 +166,7 @@ export async function registerStatsRoutes(app) {
|
|
|
143
166
|
tokens: sql `COALESCE(SUM(total_tokens),0)`,
|
|
144
167
|
})
|
|
145
168
|
.from(schema.requests)
|
|
146
|
-
.where(
|
|
169
|
+
.where(rawConds)
|
|
147
170
|
.groupBy(schema.requests.finalModelId)
|
|
148
171
|
// Order by the aggregate expression itself: SQLite rejects ORDER BY on a
|
|
149
172
|
// quoted select alias ("no such column: c").
|
|
@@ -152,6 +175,23 @@ export async function registerStatsRoutes(app) {
|
|
|
152
175
|
.all();
|
|
153
176
|
const models = db.select().from(schema.models).all();
|
|
154
177
|
const modelMap = new Map(models.map((m) => [m.id, m]));
|
|
178
|
+
const mergedModels = new Map();
|
|
179
|
+
for (const r of topModels) {
|
|
180
|
+
const id = r.modelId ? modelMap.get(r.modelId)?.publicModelId ?? r.modelId : 'unknown';
|
|
181
|
+
const v = mergedModels.get(id) ?? { requests: 0, errors: 0, tokens: 0 };
|
|
182
|
+
v.requests += Number(r.c);
|
|
183
|
+
v.errors += Number(r.err);
|
|
184
|
+
v.tokens += Number(r.tokens);
|
|
185
|
+
mergedModels.set(id, v);
|
|
186
|
+
}
|
|
187
|
+
for (const r of aggregateRows.filter((x) => aggregateDays.includes(x.day))) {
|
|
188
|
+
const id = r.modelId ? modelMap.get(r.modelId)?.publicModelId ?? r.modelId : 'unknown';
|
|
189
|
+
const v = mergedModels.get(id) ?? { requests: 0, errors: 0, tokens: 0 };
|
|
190
|
+
v.requests += r.requests;
|
|
191
|
+
v.errors += r.failedRequests;
|
|
192
|
+
v.tokens += r.inputTokens + r.outputTokens;
|
|
193
|
+
mergedModels.set(id, v);
|
|
194
|
+
}
|
|
155
195
|
const topKeys = db
|
|
156
196
|
.select({
|
|
157
197
|
apiKeyId: schema.requests.apiKeyId,
|
|
@@ -159,13 +199,29 @@ export async function registerStatsRoutes(app) {
|
|
|
159
199
|
tokens: sql `COALESCE(SUM(total_tokens),0)`,
|
|
160
200
|
})
|
|
161
201
|
.from(schema.requests)
|
|
162
|
-
.where(
|
|
202
|
+
.where(rawConds)
|
|
163
203
|
.groupBy(schema.requests.apiKeyId)
|
|
164
204
|
.orderBy(desc(sql `COUNT(*)`))
|
|
165
205
|
.limit(10)
|
|
166
206
|
.all();
|
|
167
207
|
const keys = db.select().from(schema.apiKeys).all();
|
|
168
208
|
const keyMap = new Map(keys.map((k) => [k.id, k]));
|
|
209
|
+
const mergedKeys = new Map();
|
|
210
|
+
for (const r of topKeys) {
|
|
211
|
+
const id = r.apiKeyId ? keyMap.get(r.apiKeyId)?.name ?? 'deleted' : 'unknown';
|
|
212
|
+
const v = mergedKeys.get(id) ?? { requests: 0, tokens: 0 };
|
|
213
|
+
v.requests += Number(r.c);
|
|
214
|
+
v.tokens += Number(r.tokens);
|
|
215
|
+
mergedKeys.set(id, v);
|
|
216
|
+
}
|
|
217
|
+
const keyAggregateRows = db.select().from(schema.dailyApiKeyStats).where(and(gte(schema.dailyApiKeyStats.day, fromIso.slice(0, 10)), lte(schema.dailyApiKeyStats.day, toIso.slice(0, 10)))).all().filter((r) => aggregateDays.includes(r.day));
|
|
218
|
+
for (const r of keyAggregateRows) {
|
|
219
|
+
const id = r.apiKeyId ? keyMap.get(r.apiKeyId)?.name ?? 'deleted' : 'unknown';
|
|
220
|
+
const v = mergedKeys.get(id) ?? { requests: 0, tokens: 0 };
|
|
221
|
+
v.requests += r.requests;
|
|
222
|
+
v.tokens += r.totalTokens;
|
|
223
|
+
mergedKeys.set(id, v);
|
|
224
|
+
}
|
|
169
225
|
// ─── Previous period (for delta badges in summary cards) ───────────────
|
|
170
226
|
const durationMs = to.getTime() - from.getTime();
|
|
171
227
|
const prevFrom = new Date(from.getTime() - durationMs).toISOString();
|
|
@@ -200,43 +256,44 @@ export async function registerStatsRoutes(app) {
|
|
|
200
256
|
.groupBy(schema.models.providerId)
|
|
201
257
|
.all();
|
|
202
258
|
const countMap = new Map(modelCounts.map((r) => [r.providerId, Number(r.c)]));
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
259
|
+
const providerTotals = new Map(providerAggs.map((r) => [r.providerId, { total: Number(r.total), errors: Number(r.err), latencySum: Number(r.avgLat) * (Number(r.total) - Number(r.err)), latencyCount: Number(r.total) - Number(r.err) }]));
|
|
260
|
+
const providerAggregateRows = db.select().from(schema.dailyProviderStats).where(and(gte(schema.dailyProviderStats.day, fromIso.slice(0, 10)), lte(schema.dailyProviderStats.day, toIso.slice(0, 10)))).all().filter((r) => aggregateDays.includes(r.day));
|
|
261
|
+
for (const r of providerAggregateRows) {
|
|
262
|
+
const a = providerTotals.get(r.providerId) ?? { total: 0, errors: 0, latencySum: 0, latencyCount: 0 };
|
|
263
|
+
a.total += r.attempts;
|
|
264
|
+
a.errors += r.failedAttempts;
|
|
265
|
+
a.latencySum += r.latencySuccessSumMs;
|
|
266
|
+
a.latencyCount += r.latencySuccessCount;
|
|
267
|
+
providerTotals.set(r.providerId, a);
|
|
268
|
+
}
|
|
269
|
+
const providers = [...providerTotals].map(([providerId, r]) => {
|
|
270
|
+
const p = providerLookup.get(providerId);
|
|
206
271
|
if (!p)
|
|
207
272
|
return null;
|
|
208
|
-
const total =
|
|
273
|
+
const total = r.total;
|
|
209
274
|
return {
|
|
210
|
-
id:
|
|
275
|
+
id: providerId,
|
|
211
276
|
name: p.name,
|
|
212
277
|
slug: p.slug,
|
|
213
278
|
health: p.healthState,
|
|
214
279
|
enabled: p.enabled,
|
|
215
|
-
modelCount: countMap.get(
|
|
280
|
+
modelCount: countMap.get(providerId) ?? 0,
|
|
216
281
|
requests: total,
|
|
217
|
-
errorRate: total > 0 ?
|
|
218
|
-
avgLatencyMs:
|
|
282
|
+
errorRate: total > 0 ? r.errors / total : 0,
|
|
283
|
+
avgLatencyMs: r.latencyCount ? r.latencySum / r.latencyCount : 0,
|
|
219
284
|
};
|
|
220
285
|
})
|
|
221
286
|
.filter((x) => x !== null)
|
|
222
287
|
.sort((a, b) => b.requests - a.requests);
|
|
223
288
|
const range = { from: fromIso, to: toIso, bucket };
|
|
224
289
|
return {
|
|
290
|
+
percentilesAvailable,
|
|
225
291
|
range,
|
|
226
292
|
summary: statsSummary,
|
|
227
293
|
previous: previousSummary,
|
|
228
|
-
series:
|
|
229
|
-
topModels:
|
|
230
|
-
|
|
231
|
-
requests: Number(r.c),
|
|
232
|
-
errorRate: Number(r.c) > 0 ? Number(r.err) / Number(r.c) : 0,
|
|
233
|
-
totalTokens: Number(r.tokens),
|
|
234
|
-
})),
|
|
235
|
-
topApiKeys: topKeys.map((r) => ({
|
|
236
|
-
name: r.apiKeyId ? keyMap.get(r.apiKeyId)?.name ?? 'deleted' : 'unknown',
|
|
237
|
-
requests: Number(r.c),
|
|
238
|
-
totalTokens: Number(r.tokens),
|
|
239
|
-
})),
|
|
294
|
+
series: [...seriesMap.values()].sort((a, b) => a.t.localeCompare(b.t)),
|
|
295
|
+
topModels: [...mergedModels].map(([publicId, v]) => ({ publicId, requests: v.requests, errorRate: v.requests ? v.errors / v.requests : 0, totalTokens: v.tokens })).sort((a, b) => b.requests - a.requests).slice(0, 10),
|
|
296
|
+
topApiKeys: [...mergedKeys].map(([name, v]) => ({ name, requests: v.requests, totalTokens: v.tokens })).sort((a, b) => b.requests - a.requests).slice(0, 10),
|
|
240
297
|
recent,
|
|
241
298
|
providers,
|
|
242
299
|
};
|
|
@@ -247,6 +304,12 @@ export async function registerStatsRoutes(app) {
|
|
|
247
304
|
* incremental aggregation). Returns counts sufficient to derive ratios.
|
|
248
305
|
*/
|
|
249
306
|
function buildSummary(db, fromIso, toIso) {
|
|
307
|
+
// For previous period, use the same aggregate/raw partition as the primary range.
|
|
308
|
+
const days = db.select().from(schema.dailyModelStats).where(and(gte(schema.dailyModelStats.day, fromIso.slice(0, 10)), lte(schema.dailyModelStats.day, toIso.slice(0, 10)))).all();
|
|
309
|
+
const daySet = [...new Set(days.map((x) => x.day))].filter((day) => day < new Date().toISOString().slice(0, 10) && `${day}T00:00:00.000Z` >= fromIso && `${day}T23:59:59.999Z` <= toIso);
|
|
310
|
+
const aggregates = days.filter((x) => daySet.includes(x.day));
|
|
311
|
+
const rawOnly = daySet.length ? and(gte(schema.requests.createdAt, fromIso), lte(schema.requests.createdAt, toIso), sql `substr(created_at,1,10) NOT IN (${sql.join(daySet.map((day) => sql `${day}`), sql `, `)})`) : and(gte(schema.requests.createdAt, fromIso), lte(schema.requests.createdAt, toIso));
|
|
312
|
+
const agg = aggregates.reduce((a, x) => ({ total: a.total + x.requests, success: a.success + x.successfulRequests, failed: a.failed + x.failedRequests, input: a.input + x.inputTokens, output: a.output + x.outputTokens, cacheRead: a.cacheRead + x.cacheReadTokens, cacheWrite: a.cacheWrite + x.cacheWriteTokens, reasoning: a.reasoning + x.reasoningTokens, gateway: a.gateway + x.gatewayCacheHits, fallback: a.fallback + x.fallbackRequests, latency: a.latency + x.latencySuccessSumMs, latencyCount: a.latencyCount + x.latencySuccessCount, ttft: a.ttft + x.ttftSumMs, ttftCount: a.ttftCount + x.ttftCount }), { total: 0, success: 0, failed: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, gateway: 0, fallback: 0, latency: 0, latencyCount: 0, ttft: 0, ttftCount: 0 });
|
|
250
313
|
const conds = [gte(schema.requests.createdAt, fromIso), lte(schema.requests.createdAt, toIso)];
|
|
251
314
|
const r = db
|
|
252
315
|
.select({
|
|
@@ -258,37 +321,42 @@ function buildSummary(db, fromIso, toIso) {
|
|
|
258
321
|
cacheRead: sql `COALESCE(SUM(cache_read_tokens),0)`,
|
|
259
322
|
cacheWrite: sql `COALESCE(SUM(cache_write_tokens),0)`,
|
|
260
323
|
reasoning: sql `COALESCE(SUM(reasoning_tokens),0)`,
|
|
261
|
-
|
|
262
|
-
|
|
324
|
+
latencySum: sql `COALESCE(SUM(CASE WHEN success=1 THEN total_latency_ms ELSE 0 END),0)`,
|
|
325
|
+
latencyCount: sql `SUM(CASE WHEN success=1 AND total_latency_ms > 0 THEN 1 ELSE 0 END)`,
|
|
326
|
+
ttftSum: sql `COALESCE(SUM(CASE WHEN success=1 THEN COALESCE(ttft_ms,0) ELSE 0 END),0)`,
|
|
327
|
+
ttftCount: sql `SUM(CASE WHEN success=1 AND ttft_ms IS NOT NULL THEN 1 ELSE 0 END)`,
|
|
263
328
|
gatewayCacheHits: sql `SUM(CASE WHEN gateway_cache_hit=1 THEN 1 ELSE 0 END)`,
|
|
264
329
|
fallbacks: sql `SUM(CASE WHEN attempts_count > 1 THEN 1 ELSE 0 END)`,
|
|
265
330
|
})
|
|
266
331
|
.from(schema.requests)
|
|
267
|
-
.where(
|
|
332
|
+
.where(rawOnly)
|
|
268
333
|
.get();
|
|
269
|
-
const total = Number(r?.total ?? 0);
|
|
270
|
-
const success = Number(r?.success ?? 0);
|
|
271
|
-
const
|
|
272
|
-
const inputTokens = Number(r?.inputTokens ?? 0);
|
|
273
|
-
const
|
|
334
|
+
const total = Number(r?.total ?? 0) + agg.total;
|
|
335
|
+
const success = Number(r?.success ?? 0) + agg.success;
|
|
336
|
+
const failed = Number(r?.failed ?? 0) + agg.failed;
|
|
337
|
+
const inputTokens = Number(r?.inputTokens ?? 0) + agg.input;
|
|
338
|
+
const outputTokens = Number(r?.outputTokens ?? 0) + agg.output;
|
|
339
|
+
const latencyCount = Number(r?.latencyCount ?? 0) + agg.latencyCount;
|
|
340
|
+
const ttftCount = Number(r?.ttftCount ?? 0) + agg.ttftCount;
|
|
341
|
+
const cacheHitRate = daySet.length ? null : cacheHitRateFor(db, conds);
|
|
274
342
|
return {
|
|
275
343
|
totalRequests: total,
|
|
276
344
|
successfulRequests: success,
|
|
277
|
-
failedRequests:
|
|
345
|
+
failedRequests: failed,
|
|
278
346
|
successRate: total ? success / total : 0,
|
|
279
347
|
inputTokens,
|
|
280
|
-
outputTokens
|
|
281
|
-
totalTokens: inputTokens +
|
|
282
|
-
cacheReadTokens: cacheRead,
|
|
283
|
-
cacheWriteTokens: Number(r?.cacheWrite ?? 0),
|
|
284
|
-
reasoningTokens: Number(r?.reasoning ?? 0),
|
|
285
|
-
averageLatencyMs: Number(r?.
|
|
286
|
-
p95LatencyMs:
|
|
287
|
-
averageTtftMs: (r?.
|
|
348
|
+
outputTokens,
|
|
349
|
+
totalTokens: inputTokens + outputTokens,
|
|
350
|
+
cacheReadTokens: Number(r?.cacheRead ?? 0) + agg.cacheRead,
|
|
351
|
+
cacheWriteTokens: Number(r?.cacheWrite ?? 0) + agg.cacheWrite,
|
|
352
|
+
reasoningTokens: Number(r?.reasoning ?? 0) + agg.reasoning,
|
|
353
|
+
averageLatencyMs: latencyCount ? (Number(r?.latencySum ?? 0) + agg.latency) / latencyCount : 0,
|
|
354
|
+
p95LatencyMs: null,
|
|
355
|
+
averageTtftMs: ttftCount ? (Number(r?.ttftSum ?? 0) + agg.ttft) / ttftCount : null,
|
|
288
356
|
p95TtftMs: null,
|
|
289
357
|
cacheHitRate,
|
|
290
|
-
gatewayCacheHitRate: total ? Number(r?.gatewayCacheHits ?? 0) / total : 0,
|
|
291
|
-
fallbackRate: total ? Number(r?.fallbacks ?? 0) / total : 0,
|
|
358
|
+
gatewayCacheHitRate: total ? (Number(r?.gatewayCacheHits ?? 0) + agg.gateway) / total : 0,
|
|
359
|
+
fallbackRate: total ? (Number(r?.fallbacks ?? 0) + agg.fallback) / total : 0,
|
|
292
360
|
};
|
|
293
361
|
}
|
|
294
362
|
function percentile(sorted, p) {
|