llm-simple-router 1.0.8 → 1.0.11
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/dist/admin/dashboard.d.ts +7 -0
- package/dist/admin/dashboard.js +93 -0
- package/dist/admin/metrics.js +13 -0
- package/dist/admin/routes.js +2 -0
- package/dist/admin/settings.js +17 -1
- package/dist/admin/stats.js +2 -1
- package/dist/admin/usage.js +69 -1
- package/dist/core/constants.d.ts +1 -0
- package/dist/core/constants.js +1 -0
- package/dist/db/index.d.ts +2 -1
- package/dist/db/index.js +1 -1
- package/dist/db/log-cleaner.js +9 -2
- package/dist/db/logs.d.ts +1 -0
- package/dist/db/logs.js +4 -4
- package/dist/db/metrics-10min.d.ts +43 -0
- package/dist/db/metrics-10min.js +197 -0
- package/dist/db/metrics-aggregator.d.ts +8 -0
- package/dist/db/metrics-aggregator.js +125 -0
- package/dist/db/metrics.d.ts +2 -1
- package/dist/db/metrics.js +175 -5
- package/dist/db/migrations/054_add_backend_model_index.sql +5 -0
- package/dist/db/migrations/055_metrics_10min.sql +25 -0
- package/dist/db/migrations/056_backfill_metrics_10min.sql +67 -0
- package/dist/db/settings.d.ts +2 -0
- package/dist/db/settings.js +9 -0
- package/dist/db/stats.d.ts +3 -2
- package/dist/db/stats.js +62 -1
- package/dist/index.js +3 -0
- package/dist/proxy/handler/failover-loop.js +3 -0
- package/dist/proxy/hooks/builtin/error-logging.js +2 -0
- package/dist/proxy/log-helpers.d.ts +2 -0
- package/dist/proxy/log-helpers.js +4 -2
- package/dist/proxy/proxy-logging.js +4 -0
- package/dist/proxy/transport/transport-fn.js +6 -2
- package/frontend-dist/assets/AuthLayout-BNvGjJv3.js +1 -0
- package/frontend-dist/assets/{Card-BVxPbRVS.js → Card-BNz2IkLE.js} +1 -1
- package/frontend-dist/assets/CardContent-DszVPJgx.js +1 -0
- package/frontend-dist/assets/CardTitle-BeBkx0ns.js +1 -0
- package/frontend-dist/assets/CascadingModelSelect-Z7-ur9_p.js +1 -0
- package/frontend-dist/assets/Checkbox-CAQnc0qV.js +1 -0
- package/frontend-dist/assets/CollapsibleContent-CHlSINCS.js +1 -0
- package/frontend-dist/assets/CollapsibleTrigger-CJMUoPRa.js +1 -0
- package/frontend-dist/assets/ConcurrencyControl-BcUA816m.js +1 -0
- package/frontend-dist/assets/Dashboard-DXvcAfAx.js +3 -0
- package/frontend-dist/assets/{Input-qtzKspPF.js → Input-Dr2XqfSh.js} +1 -1
- package/frontend-dist/assets/Label-Ct_DR7Ui.js +1 -0
- package/frontend-dist/assets/Login-C8_p8684.js +1 -0
- package/frontend-dist/assets/Logs-BDrXnYb1.js +1 -0
- package/frontend-dist/assets/ModelMappings-CeLps4LM.js +1 -0
- package/frontend-dist/assets/Monitor-By1ywNWD.js +1 -0
- package/frontend-dist/assets/Providers-Cp-gomQY.js +1 -0
- package/frontend-dist/assets/ProxyEnhancement-D5jsMnL3.js +1 -0
- package/frontend-dist/assets/QuickSetup-1ADLBexY.js +1 -0
- package/frontend-dist/assets/RetryRules-Dk0OxVLs.js +1 -0
- package/frontend-dist/assets/{RouterKeys-DEMKBc7g.css → RouterKeys-B0enpkQK.css} +1 -1
- package/frontend-dist/assets/RouterKeys-BvD4POUA.js +1 -0
- package/frontend-dist/assets/RovingFocusItem-DlOsi-lb.js +1 -0
- package/frontend-dist/assets/Schedules-CVJcpHlE.js +1 -0
- package/frontend-dist/assets/Separator-BO94Jk9p.js +1 -0
- package/frontend-dist/assets/Settings-MYcQoEZw.js +6 -0
- package/frontend-dist/assets/Setup-mAyd7FhD.js +1 -0
- package/frontend-dist/assets/Skeleton-Di0WMQf-.js +1 -0
- package/frontend-dist/assets/Switch-BS1G55M2.js +1 -0
- package/frontend-dist/assets/TableHeader-jCTjghgy.js +1 -0
- package/frontend-dist/assets/TabsTrigger-B5HyH2xO.js +1 -0
- package/frontend-dist/assets/UnifiedRequestDialog-CMbYIrxT.css +1 -0
- package/frontend-dist/assets/UnifiedRequestDialog-CQ5Rzs11.js +3 -0
- package/frontend-dist/assets/VisuallyHiddenInput-DiW4Si0N.js +1 -0
- package/frontend-dist/assets/arrow-down-BsHwq97B.js +1 -0
- package/frontend-dist/assets/badge-FksLt30f.js +1 -0
- package/frontend-dist/assets/{button-BJEwiB8A.js → button-BCxYWEaM.js} +7 -7
- package/frontend-dist/assets/chevron-right-C3U2yjrf.js +1 -0
- package/frontend-dist/assets/common-WzFPzDR_.js +1 -0
- package/frontend-dist/assets/common-vLJ_SFUM.js +1 -0
- package/frontend-dist/assets/dashboard-C50KHy8J.js +1 -0
- package/frontend-dist/assets/dashboard-DxQEVaH8.js +1 -0
- package/frontend-dist/assets/dialog-B0wgNzud.js +1 -0
- package/frontend-dist/assets/{image-DWAFh4CH.js → image-DlPnda0z.js} +1 -1
- package/frontend-dist/assets/index-BTfjdJDq.css +1 -0
- package/frontend-dist/assets/index-CwjudU09.js +3 -0
- package/frontend-dist/assets/logs-BUYq0WgN.js +1 -0
- package/frontend-dist/assets/logs-toInbfD3.js +1 -0
- package/frontend-dist/assets/model-patches-Dv2jfKSp.js +1 -0
- package/frontend-dist/assets/{pencil-BAYWJXMP.js → pencil-Y6MJERvc.js} +1 -1
- package/frontend-dist/assets/plus-FNO9X5JG.js +1 -0
- package/frontend-dist/assets/search-DpWQ2wvO.js +1 -0
- package/frontend-dist/assets/settings-C1AmEyXr.js +1 -0
- package/frontend-dist/assets/settings-DHufHb9v.js +1 -0
- package/frontend-dist/assets/{sparkles-BXqZs-3m.js → sparkles-C44_iMrx.js} +1 -1
- package/frontend-dist/assets/transform-domain-FnCfUKrh.js +1 -0
- package/frontend-dist/assets/{trash-2-AdzXWmCq.js → trash-2-BEOdZk69.js} +1 -1
- package/frontend-dist/assets/{useClipboard-uOuEel2P.js → useClipboard-Bw6GLR6I.js} +1 -1
- package/frontend-dist/assets/useLogRetention-CQHD99Fy.js +1 -0
- package/frontend-dist/assets/{useProviderGroups-BCau8ZEY.js → useProviderGroups-Bz7Yz1b7.js} +1 -1
- package/frontend-dist/index.html +3 -3
- package/package.json +1 -1
- package/frontend-dist/assets/AuthLayout-CrDZ0S1l.js +0 -1
- package/frontend-dist/assets/CardContent-mtMPJeBk.js +0 -1
- package/frontend-dist/assets/CardTitle-B57j9XPI.js +0 -1
- package/frontend-dist/assets/CascadingModelSelect-CEA1IWYp.js +0 -1
- package/frontend-dist/assets/Checkbox-Dy91V6uO.js +0 -1
- package/frontend-dist/assets/CollapsibleContent-BNYRQZDC.js +0 -1
- package/frontend-dist/assets/CollapsibleTrigger-BsRqX2Cu.js +0 -1
- package/frontend-dist/assets/ConcurrencyControl-CYMId2CN.js +0 -1
- package/frontend-dist/assets/Dashboard-CdyqA2IJ.js +0 -3
- package/frontend-dist/assets/Label-C-4yGDcj.js +0 -1
- package/frontend-dist/assets/Login-Djwy2tf8.js +0 -1
- package/frontend-dist/assets/Logs-CPjawsdI.js +0 -1
- package/frontend-dist/assets/ModelMappings-BPAkfDsD.js +0 -1
- package/frontend-dist/assets/Monitor-C9evAeEj.js +0 -1
- package/frontend-dist/assets/Providers-FYxhWwD9.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-D7ZP80Lz.js +0 -1
- package/frontend-dist/assets/QuickSetup-DGt0nShS.js +0 -1
- package/frontend-dist/assets/RetryRules-3-6_iVb0.js +0 -1
- package/frontend-dist/assets/RouterKeys-ISCjK-Ct.js +0 -1
- package/frontend-dist/assets/RovingFocusItem-Ch8O0jki.js +0 -1
- package/frontend-dist/assets/Schedules-DQgLcSFw.js +0 -1
- package/frontend-dist/assets/Settings-BU3ivwpF.js +0 -6
- package/frontend-dist/assets/Setup-CMZiW_BK.js +0 -1
- package/frontend-dist/assets/Skeleton-Lckr7Vzh.js +0 -1
- package/frontend-dist/assets/Switch-vb3eQrUY.js +0 -1
- package/frontend-dist/assets/TableHeader-DvgxagEq.js +0 -1
- package/frontend-dist/assets/TabsTrigger-vvniTekl.js +0 -1
- package/frontend-dist/assets/TooltipTrigger-B_7uc54_.js +0 -1
- package/frontend-dist/assets/UnifiedRequestDialog-CHh7SjPE.js +0 -3
- package/frontend-dist/assets/UnifiedRequestDialog-DaZodSzm.css +0 -1
- package/frontend-dist/assets/VisuallyHiddenInput-DPDB7WoD.js +0 -1
- package/frontend-dist/assets/arrow-down-BYVAWREL.js +0 -1
- package/frontend-dist/assets/badge-BAIzzgzC.js +0 -1
- package/frontend-dist/assets/chevron-right-CR3YFy_O.js +0 -1
- package/frontend-dist/assets/common-ZLJwmI0c.js +0 -1
- package/frontend-dist/assets/common-qa3O5YJX.js +0 -1
- package/frontend-dist/assets/dashboard-BlCt9ud6.js +0 -1
- package/frontend-dist/assets/dashboard-CqwGLWoK.js +0 -1
- package/frontend-dist/assets/dialog-BwSmcgaV.js +0 -1
- package/frontend-dist/assets/index-3DIQxot0.js +0 -3
- package/frontend-dist/assets/index-BayP76hx.css +0 -1
- package/frontend-dist/assets/logs-BI_TLeBe.js +0 -1
- package/frontend-dist/assets/logs-DZyb4-Ew.js +0 -1
- package/frontend-dist/assets/model-patches-COK1yfWm.js +0 -1
- package/frontend-dist/assets/plus-B_GSrrcs.js +0 -1
- package/frontend-dist/assets/search-B87CsLO4.js +0 -1
- package/frontend-dist/assets/settings-av7tA8wt.js +0 -1
- package/frontend-dist/assets/settings-dzu7sg6b.js +0 -1
- package/frontend-dist/assets/transform-domain-DLWadwZU.js +0 -1
- package/frontend-dist/assets/useLogRetention-DstR1E-X.js +0 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { getStats, getMetricsSummary, getMetricsTimeseries, getClientTypeBreakdown } from "../db/index.js";
|
|
3
|
+
const OverviewQuerySchema = Type.Object({
|
|
4
|
+
provider_id: Type.Optional(Type.String()),
|
|
5
|
+
backend_model: Type.Optional(Type.String()),
|
|
6
|
+
router_key_id: Type.Optional(Type.String()),
|
|
7
|
+
client_type: Type.Optional(Type.String()),
|
|
8
|
+
start_time: Type.String(),
|
|
9
|
+
end_time: Type.String(),
|
|
10
|
+
});
|
|
11
|
+
const PERCENT_MULTIPLIER = 100;
|
|
12
|
+
const PCT_ROUND_DIGITS = 10;
|
|
13
|
+
const PROVIDER_TOKEN_LOOKBACK_DAYS = 30;
|
|
14
|
+
export const adminDashboardRoutes = (app, options, done) => {
|
|
15
|
+
const { db } = options;
|
|
16
|
+
app.get("/admin/api/dashboard/overview", { schema: { querystring: OverviewQuerySchema } }, async (request, reply) => {
|
|
17
|
+
const query = request.query;
|
|
18
|
+
const startTime = query.start_time;
|
|
19
|
+
const endTime = query.end_time;
|
|
20
|
+
const providerId = query.provider_id || undefined;
|
|
21
|
+
const backendModel = query.backend_model || undefined;
|
|
22
|
+
const routerKeyId = query.router_key_id || undefined;
|
|
23
|
+
const clientType = query.client_type || undefined;
|
|
24
|
+
// 1. Current period stats
|
|
25
|
+
const stats = getStats(db, startTime, endTime, routerKeyId, providerId, backendModel, clientType);
|
|
26
|
+
// 2. Previous period stats (same duration, immediately before current window)
|
|
27
|
+
const durationMs = new Date(endTime).getTime() - new Date(startTime).getTime();
|
|
28
|
+
const prevEnd = startTime;
|
|
29
|
+
const prevStart = new Date(new Date(startTime).getTime() - durationMs).toISOString();
|
|
30
|
+
const prevStats = durationMs > 0
|
|
31
|
+
? getStats(db, prevStart, prevEnd, routerKeyId, providerId, backendModel, clientType)
|
|
32
|
+
: null;
|
|
33
|
+
// 3. Timeseries (tps, input_tokens, output_tokens)
|
|
34
|
+
const legacyPeriod = "30d";
|
|
35
|
+
const [tpsRes, inputRes, outputRes] = await Promise.allSettled([
|
|
36
|
+
getMetricsTimeseries(db, legacyPeriod, "total_tps", providerId, backendModel, routerKeyId, startTime, endTime, clientType),
|
|
37
|
+
getMetricsTimeseries(db, legacyPeriod, "input_tokens", providerId, backendModel, routerKeyId, startTime, endTime, clientType),
|
|
38
|
+
getMetricsTimeseries(db, legacyPeriod, "output_tokens", providerId, backendModel, routerKeyId, startTime, endTime, clientType),
|
|
39
|
+
]);
|
|
40
|
+
const tpsRes_ = tpsRes.status === "fulfilled" ? tpsRes.value : [];
|
|
41
|
+
const inputRes_ = inputRes.status === "fulfilled" ? inputRes.value : [];
|
|
42
|
+
const outputRes_ = outputRes.status === "fulfilled" ? outputRes.value : [];
|
|
43
|
+
// 4. Cache hit rate + client type breakdown
|
|
44
|
+
const summary = getMetricsSummary(db, legacyPeriod, providerId, backendModel, routerKeyId, startTime, endTime, clientType);
|
|
45
|
+
const totalInputTokens = summary.reduce((sum, r) => sum + r.total_input_tokens, 0);
|
|
46
|
+
const totalCacheHitTokens = summary.reduce((sum, r) => sum + r.total_cache_hit_tokens, 0);
|
|
47
|
+
const cacheHitRate = totalInputTokens > 0
|
|
48
|
+
? Math.round(totalCacheHitTokens * PERCENT_MULTIPLIER / totalInputTokens * PCT_ROUND_DIGITS) / PCT_ROUND_DIGITS
|
|
49
|
+
: 0;
|
|
50
|
+
const breakdown = getClientTypeBreakdown(db, legacyPeriod, providerId, backendModel, routerKeyId, startTime, endTime);
|
|
51
|
+
// 5. Provider token summary (for provider sorting/labels in frontend)
|
|
52
|
+
const providerTokenSummary = getProviderTokenSummary(db);
|
|
53
|
+
return reply.send({
|
|
54
|
+
stats: {
|
|
55
|
+
totalRequests: stats.totalRequests,
|
|
56
|
+
successRate: stats.successRate,
|
|
57
|
+
avgTps: stats.avgTps,
|
|
58
|
+
totalInputTokens: stats.totalInputTokens,
|
|
59
|
+
totalOutputTokens: stats.totalOutputTokens,
|
|
60
|
+
startTime,
|
|
61
|
+
endTime,
|
|
62
|
+
},
|
|
63
|
+
prev_stats: prevStats ? {
|
|
64
|
+
totalRequests: prevStats.totalRequests,
|
|
65
|
+
successRate: prevStats.successRate,
|
|
66
|
+
avgTps: prevStats.avgTps,
|
|
67
|
+
totalInputTokens: prevStats.totalInputTokens,
|
|
68
|
+
totalOutputTokens: prevStats.totalOutputTokens,
|
|
69
|
+
} : null,
|
|
70
|
+
cache_hit_rate: cacheHitRate,
|
|
71
|
+
client_type_breakdown: breakdown,
|
|
72
|
+
timeseries: {
|
|
73
|
+
tps: tpsRes_,
|
|
74
|
+
input_tokens: inputRes_,
|
|
75
|
+
output_tokens: outputRes_,
|
|
76
|
+
},
|
|
77
|
+
provider_token_summary: providerTokenSummary,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
done();
|
|
81
|
+
};
|
|
82
|
+
/** Get per-provider total input tokens for last 30 days (for sorting/labels) */
|
|
83
|
+
function getProviderTokenSummary(db) {
|
|
84
|
+
const rows = db.prepare(`SELECT provider_id, COALESCE(SUM(sum_input_tokens), 0) AS total_input_tokens
|
|
85
|
+
FROM metrics_10min
|
|
86
|
+
WHERE bucket_time >= datetime('now', '-' || ? || ' days')
|
|
87
|
+
GROUP BY provider_id`).all(PROVIDER_TOKEN_LOOKBACK_DAYS);
|
|
88
|
+
const result = {};
|
|
89
|
+
for (const r of rows) {
|
|
90
|
+
result[r.provider_id] = r.total_input_tokens;
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
package/dist/admin/metrics.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getMetricsSummary, getMetricsTimeseries, getClientTypeBreakdown } from "../db/index.js";
|
|
3
|
+
import { queryAggActivity } from "../db/metrics-10min.js";
|
|
3
4
|
import { resolveTimeRange } from "../utils/time-range.js";
|
|
4
5
|
const LegacyPeriodEnum = Type.Union([
|
|
5
6
|
Type.Literal("1h"), Type.Literal("5h"), Type.Literal("6h"), Type.Literal("24h"),
|
|
@@ -34,6 +35,10 @@ const TimeseriesQuerySchema = Type.Object({
|
|
|
34
35
|
start_time: Type.Optional(Type.String()),
|
|
35
36
|
end_time: Type.Optional(Type.String()),
|
|
36
37
|
});
|
|
38
|
+
const ActivityQuerySchema = Type.Object({
|
|
39
|
+
router_key_id: Type.Optional(Type.String()),
|
|
40
|
+
provider_id: Type.Optional(Type.String()),
|
|
41
|
+
});
|
|
37
42
|
const DASHBOARD_PERIODS = new Set(["window", "weekly", "monthly"]);
|
|
38
43
|
const PCT_FACTOR = 100;
|
|
39
44
|
const PCT_ROUND_DIGITS = 10;
|
|
@@ -67,5 +72,13 @@ export const adminMetricsRoutes = (app, options, done) => {
|
|
|
67
72
|
const timeseries = getMetricsTimeseries(db, legacyPeriod, metric, query.provider_id, query.backend_model, query.router_key_id, startTime, endTime);
|
|
68
73
|
return reply.send(timeseries);
|
|
69
74
|
});
|
|
75
|
+
app.get("/admin/api/metrics/activity", { schema: { querystring: ActivityQuerySchema } }, async (request, reply) => {
|
|
76
|
+
const query = request.query;
|
|
77
|
+
const buckets = queryAggActivity(db, {
|
|
78
|
+
routerKeyId: query.router_key_id,
|
|
79
|
+
providerId: query.provider_id,
|
|
80
|
+
});
|
|
81
|
+
return reply.send({ buckets });
|
|
82
|
+
});
|
|
70
83
|
done();
|
|
71
84
|
};
|
package/dist/admin/routes.js
CHANGED
|
@@ -17,6 +17,7 @@ import { adminUpgradeRoutes } from "./upgrade.js";
|
|
|
17
17
|
import { adminQuickSetupRoutes } from "./quick-setup.js";
|
|
18
18
|
import { adminImportExportRoutes } from "./settings-import-export.js";
|
|
19
19
|
import { adminTransformRuleRoutes } from "./transform-rules.js";
|
|
20
|
+
import { adminDashboardRoutes } from "./dashboard.js";
|
|
20
21
|
import { adminScheduleRoutes } from "./schedules.js";
|
|
21
22
|
import { hookRegistry } from "../proxy/pipeline/hook-registry.js";
|
|
22
23
|
export const adminRoutes = (app, options, done) => {
|
|
@@ -41,6 +42,7 @@ export const adminRoutes = (app, options, done) => {
|
|
|
41
42
|
app.register(adminUsageRoutes, { db: options.db });
|
|
42
43
|
app.register(adminQuickSetupRoutes, { db: options.db, stateRegistry: options.stateRegistry, tracker: options.tracker, adaptiveController: options.adaptiveController });
|
|
43
44
|
app.register(adminUpgradeRoutes, { db: options.db, closeFn: options.closeFn ?? (async () => { }) });
|
|
45
|
+
app.register(adminDashboardRoutes, { db: options.db });
|
|
44
46
|
app.register(adminTransformRuleRoutes, { db: options.db, pluginRegistry: options.pluginRegistry });
|
|
45
47
|
// Pipeline hooks 查询
|
|
46
48
|
app.get("/admin/api/pipeline/hooks", async () => {
|
package/dist/admin/settings.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { statSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { getLogRetentionDays, setLogRetentionDays, getDbMaxSizeMb, setDbMaxSizeMb, getLogTableMaxSizeMb, setLogTableMaxSizeMb, getSetting, getTokenEstimationEnabled, setTokenEstimationEnabled, getClientSessionHeaders, setClientSessionHeaders, } from "../db/settings.js";
|
|
3
|
+
import { getLogRetentionDays, setLogRetentionDays, getDbMaxSizeMb, setDbMaxSizeMb, getLogTableMaxSizeMb, setLogTableMaxSizeMb, getSetting, getTokenEstimationEnabled, setTokenEstimationEnabled, getClientSessionHeaders, setClientSessionHeaders, getMetricsDetailDays, setMetricsDetailDays, } from "../db/settings.js";
|
|
4
4
|
import { HTTP_BAD_REQUEST } from "./constants.js";
|
|
5
5
|
import { API_CODE, apiError } from "./api-response.js";
|
|
6
6
|
export const adminSettingsRoutes = (app, options, done) => {
|
|
@@ -98,6 +98,22 @@ export const adminSettingsRoutes = (app, options, done) => {
|
|
|
98
98
|
setClientSessionHeaders(db, entries);
|
|
99
99
|
return { success: true };
|
|
100
100
|
});
|
|
101
|
+
// --- Metrics Detail Days ---
|
|
102
|
+
app.get("/admin/api/settings/metrics-detail-days", async () => {
|
|
103
|
+
return { days: getMetricsDetailDays(db) };
|
|
104
|
+
});
|
|
105
|
+
const MAX_METRICS_DETAIL_DAYS = 30;
|
|
106
|
+
app.put("/admin/api/settings/metrics-detail-days", async (request, reply) => {
|
|
107
|
+
const { days } = request.body;
|
|
108
|
+
if (!Number.isInteger(days) || days < 1 || days > MAX_METRICS_DETAIL_DAYS) {
|
|
109
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "days must be integer 1-30"));
|
|
110
|
+
}
|
|
111
|
+
if (days > getLogRetentionDays(db)) {
|
|
112
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "metrics_detail_days must not exceed log_retention_days"));
|
|
113
|
+
}
|
|
114
|
+
setMetricsDetailDays(db, days);
|
|
115
|
+
return { days };
|
|
116
|
+
});
|
|
101
117
|
done();
|
|
102
118
|
};
|
|
103
119
|
/** 递归计算目录下所有文件的总大小(字节) */
|
package/dist/admin/stats.js
CHANGED
|
@@ -12,6 +12,7 @@ const StatsQuerySchema = Type.Object({
|
|
|
12
12
|
router_key_id: Type.Optional(Type.String()),
|
|
13
13
|
provider_id: Type.Optional(Type.String()),
|
|
14
14
|
backend_model: Type.Optional(Type.String()),
|
|
15
|
+
client_type: Type.Optional(Type.String()),
|
|
15
16
|
});
|
|
16
17
|
export const adminStatsRoutes = (app, options, done) => {
|
|
17
18
|
app.get("/admin/api/stats", { schema: { querystring: StatsQuerySchema } }, async (request, reply) => {
|
|
@@ -27,7 +28,7 @@ export const adminStatsRoutes = (app, options, done) => {
|
|
|
27
28
|
startTime = range.startTime;
|
|
28
29
|
endTime = range.endTime;
|
|
29
30
|
}
|
|
30
|
-
const stats = getStats(options.db, startTime, endTime, query.router_key_id, query.provider_id, query.backend_model);
|
|
31
|
+
const stats = getStats(options.db, startTime, endTime, query.router_key_id, query.provider_id, query.backend_model, query.client_type);
|
|
31
32
|
return reply.send({ ...stats, startTime, endTime });
|
|
32
33
|
});
|
|
33
34
|
done();
|
package/dist/admin/usage.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getWindowsInRange, getWindowUsage } from "../db/usage-windows.js";
|
|
3
3
|
import { getProviderById } from "../db/index.js";
|
|
4
|
+
import { MS_PER_SECOND } from "../core/constants.js";
|
|
5
|
+
import { BUCKET_SECONDS } from "../db/metrics-10min.js";
|
|
4
6
|
import { resolveTimeRange } from "../utils/time-range.js";
|
|
5
7
|
const UsageQuerySchema = Type.Object({
|
|
6
8
|
router_key_id: Type.Optional(Type.String()),
|
|
@@ -8,7 +10,39 @@ const UsageQuerySchema = Type.Object({
|
|
|
8
10
|
start_time: Type.Optional(Type.String()),
|
|
9
11
|
end_time: Type.Optional(Type.String()),
|
|
10
12
|
});
|
|
11
|
-
|
|
13
|
+
// Use shared bucket boundary from metrics-10min
|
|
14
|
+
// BUCKET_SECONDS imported from metrics-10min
|
|
15
|
+
function computeBucketBoundary() {
|
|
16
|
+
const bucketStartSec = Math.floor(Date.now() / MS_PER_SECOND / BUCKET_SECONDS) * BUCKET_SECONDS;
|
|
17
|
+
return new Date(bucketStartSec * MS_PER_SECOND).toISOString();
|
|
18
|
+
}
|
|
19
|
+
function queryAggDailyUsage(db, startTime, endTime, routerKeyId, providerId) {
|
|
20
|
+
const conditions = [
|
|
21
|
+
"m.bucket_time >= datetime(?)",
|
|
22
|
+
"m.bucket_time < datetime(?)",
|
|
23
|
+
];
|
|
24
|
+
const params = [startTime, endTime];
|
|
25
|
+
if (routerKeyId) {
|
|
26
|
+
conditions.push("m.router_key_id = ?");
|
|
27
|
+
params.push(routerKeyId);
|
|
28
|
+
}
|
|
29
|
+
if (providerId) {
|
|
30
|
+
conditions.push("m.provider_id = ?");
|
|
31
|
+
params.push(providerId);
|
|
32
|
+
}
|
|
33
|
+
return db.prepare(`
|
|
34
|
+
SELECT
|
|
35
|
+
date(m.bucket_time) AS date,
|
|
36
|
+
SUM(m.request_count) AS request_count,
|
|
37
|
+
SUM(m.sum_input_tokens) AS total_input_tokens,
|
|
38
|
+
SUM(m.sum_output_tokens) AS total_output_tokens
|
|
39
|
+
FROM metrics_10min m
|
|
40
|
+
WHERE ${conditions.join(" AND ")}
|
|
41
|
+
GROUP BY date(m.bucket_time)
|
|
42
|
+
ORDER BY date ASC
|
|
43
|
+
`).all(...params);
|
|
44
|
+
}
|
|
45
|
+
function queryDetailDailyUsage(db, startTime, endTime, routerKeyId, providerId) {
|
|
12
46
|
const conditions = [
|
|
13
47
|
"rm.created_at >= datetime(?)",
|
|
14
48
|
"rm.created_at < datetime(?)",
|
|
@@ -34,6 +68,40 @@ function getDailyUsage(db, startTime, endTime, routerKeyId, providerId) {
|
|
|
34
68
|
ORDER BY date ASC
|
|
35
69
|
`).all(...params);
|
|
36
70
|
}
|
|
71
|
+
function mergeDailyUsageResults(a, b) {
|
|
72
|
+
if (a.length === 0)
|
|
73
|
+
return b;
|
|
74
|
+
if (b.length === 0)
|
|
75
|
+
return a;
|
|
76
|
+
const dateMap = new Map();
|
|
77
|
+
for (const row of [...a, ...b]) {
|
|
78
|
+
const existing = dateMap.get(row.date);
|
|
79
|
+
if (existing) {
|
|
80
|
+
existing.request_count += row.request_count;
|
|
81
|
+
existing.total_input_tokens += row.total_input_tokens;
|
|
82
|
+
existing.total_output_tokens += row.total_output_tokens;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
dateMap.set(row.date, { ...row });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return Array.from(dateMap.values()).sort((a, b) => a.date.localeCompare(b.date));
|
|
89
|
+
}
|
|
90
|
+
function getDailyUsage(db, startTime, endTime, routerKeyId, providerId) {
|
|
91
|
+
const boundary = computeBucketBoundary();
|
|
92
|
+
// Pure agg: entire range is before the current bucket
|
|
93
|
+
if (endTime <= boundary) {
|
|
94
|
+
return queryAggDailyUsage(db, startTime, endTime, routerKeyId, providerId);
|
|
95
|
+
}
|
|
96
|
+
// Pure detail: entire range is within the current bucket
|
|
97
|
+
if (startTime >= boundary) {
|
|
98
|
+
return queryDetailDailyUsage(db, startTime, endTime, routerKeyId, providerId);
|
|
99
|
+
}
|
|
100
|
+
// Cross-boundary: agg segment [start, boundary) + detail segment [boundary, end)
|
|
101
|
+
const aggResult = queryAggDailyUsage(db, startTime, boundary, routerKeyId, providerId);
|
|
102
|
+
const detailResult = queryDetailDailyUsage(db, boundary, endTime, routerKeyId, providerId);
|
|
103
|
+
return mergeDailyUsageResults(detailResult, aggResult);
|
|
104
|
+
}
|
|
37
105
|
function resolveProviderName(db, providerId) {
|
|
38
106
|
if (!providerId)
|
|
39
107
|
return null;
|
package/dist/core/constants.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare const HTTP_SERVICE_UNAVAILABLE = 503;
|
|
|
12
12
|
export declare const PROXY_API_TYPES: Record<string, string>;
|
|
13
13
|
export declare function getProxyApiType(url: string): string | null;
|
|
14
14
|
export declare const MS_PER_SECOND = 1000;
|
|
15
|
+
export declare const SECONDS_PER_DAY = 86400;
|
|
15
16
|
export declare const UPSTREAM_SUCCESS = 200;
|
|
16
17
|
/** 过滤掉不应转发给下游的 hop-by-hop headers */
|
|
17
18
|
export declare function filterHeaders(raw: import("./types.js").RawHeaders): Record<string, string>;
|
package/dist/core/constants.js
CHANGED
package/dist/db/index.d.ts
CHANGED
|
@@ -10,8 +10,9 @@ export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore,
|
|
|
10
10
|
export type { RequestLog, RequestLogGroupedRow, RequestLogListRow } from "./logs.js";
|
|
11
11
|
export { getRouterKeyByHash, getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "./router-keys.js";
|
|
12
12
|
export type { RouterKey } from "./router-keys.js";
|
|
13
|
-
export { getMetricsSummary, getMetricsTimeseries, insertMetrics, getClientTypeBreakdown } from "./metrics.js";
|
|
13
|
+
export { getMetricsSummary, getMetricsTimeseries, insertMetrics, getClientTypeBreakdown, deleteMetricsBefore } from "./metrics.js";
|
|
14
14
|
export type { MetricsSummaryRow, MetricsTimeseriesRow, MetricsPeriod, MetricsMetric, MetricsRow, MetricsInsert, ClientTypeBreakdown } from "./metrics.js";
|
|
15
|
+
export type { Metrics10minRow } from "./metrics-10min.js";
|
|
15
16
|
export { getStats, getLatestMetricTime } from "./stats.js";
|
|
16
17
|
export type { Stats } from "./stats.js";
|
|
17
18
|
export { getSetting, setSetting, isInitialized } from "./settings.js";
|
package/dist/db/index.js
CHANGED
|
@@ -186,7 +186,7 @@ export { getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappin
|
|
|
186
186
|
export { getActiveRetryRules, getAllRetryRules, getRetryRuleById, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
|
|
187
187
|
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, updateLogStreamContent, updateLogClientStatus, estimateLogTableSize, deleteOldestLogs, getLogCount, updateLogPipelineSnapshot, extractThinkingLevel, } from "./logs.js";
|
|
188
188
|
export { getRouterKeyByHash, getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "./router-keys.js";
|
|
189
|
-
export { getMetricsSummary, getMetricsTimeseries, insertMetrics, getClientTypeBreakdown } from "./metrics.js";
|
|
189
|
+
export { getMetricsSummary, getMetricsTimeseries, insertMetrics, getClientTypeBreakdown, deleteMetricsBefore } from "./metrics.js";
|
|
190
190
|
export { getStats, getLatestMetricTime } from "./stats.js";
|
|
191
191
|
export { getSetting, setSetting, isInitialized } from "./settings.js";
|
|
192
192
|
export { getDbMaxSizeMb, setDbMaxSizeMb, getLogTableMaxSizeMb, setLogTableMaxSizeMb, } from "./settings.js";
|
package/dist/db/log-cleaner.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { deleteLogsBefore } from "./logs.js";
|
|
2
|
-
import { getLogRetentionDays } from "./settings.js";
|
|
2
|
+
import { getLogRetentionDays, getMetricsDetailDays } from "./settings.js";
|
|
3
|
+
import { deleteMetricsBefore } from "./metrics.js";
|
|
3
4
|
import { deleteToolErrorLogsBefore } from "./tool-error-logs.js";
|
|
4
5
|
const MS_PER_DAY = 86_400_000;
|
|
5
6
|
const CLEANUP_INTERVAL_MS = 3_600_000; // 1 小时
|
|
@@ -11,7 +12,13 @@ export function runLogCleanup(db) {
|
|
|
11
12
|
const cutoff = new Date(Date.now() - days * MS_PER_DAY).toISOString();
|
|
12
13
|
const logDeleted = deleteLogsBefore(db, cutoff);
|
|
13
14
|
const toolErrorDeleted = deleteToolErrorLogsBefore(db, cutoff);
|
|
14
|
-
|
|
15
|
+
let metricsDeleted = 0;
|
|
16
|
+
const detailDays = getMetricsDetailDays(db);
|
|
17
|
+
if (detailDays > 0) {
|
|
18
|
+
const metricsCutoff = new Date(Date.now() - detailDays * MS_PER_DAY).toISOString();
|
|
19
|
+
metricsDeleted = deleteMetricsBefore(db, metricsCutoff);
|
|
20
|
+
}
|
|
21
|
+
return logDeleted + toolErrorDeleted + metricsDeleted;
|
|
15
22
|
}
|
|
16
23
|
/** 启动定时清理,返回 handle 用于停止 */
|
|
17
24
|
export function scheduleLogCleanup(db, log) {
|
package/dist/db/logs.d.ts
CHANGED
package/dist/db/logs.js
CHANGED
|
@@ -38,9 +38,9 @@ function rawInsertRequestLog(db, log, writeContext) {
|
|
|
38
38
|
is_stream, error_message, created_at, client_request, upstream_request, upstream_response,
|
|
39
39
|
is_retry, is_failover, original_request_id, router_key_id, original_model, session_id, pipeline_snapshot,
|
|
40
40
|
transport_kind, abort_reason, error_code, headers_sent, resilience_action, resilience_reason, mapping_reason, failover_trigger,
|
|
41
|
-
upstream_api_type, upstream_base_url, thinking_level)
|
|
41
|
+
upstream_api_type, upstream_base_url, thinking_level, backend_model)
|
|
42
42
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
43
|
-
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(log.id, log.api_type, log.model, log.provider_id, log.status_code, log.client_status_code ?? null, log.latency_ms, log.is_stream, log.error_message, log.created_at, log.client_request ?? null, preserveDetail ? (log.upstream_request ?? null) : null, preserveDetail ? (log.upstream_response ?? null) : null, log.is_retry ?? 0, log.is_failover ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null, log.session_id ?? null, log.pipeline_snapshot ?? null, log.transport_kind ?? null, log.abort_reason ?? null, log.error_code ?? null, log.headers_sent ?? null, log.resilience_action ?? null, log.resilience_reason ?? null, log.mapping_reason ?? null, log.failover_trigger ?? null, log.upstream_api_type ?? null, log.upstream_base_url ?? null, log.thinking_level ?? extractThinkingLevel(log.api_type, log.client_request ?? null));
|
|
43
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(log.id, log.api_type, log.model, log.provider_id, log.status_code, log.client_status_code ?? null, log.latency_ms, log.is_stream, log.error_message, log.created_at, log.client_request ?? null, preserveDetail ? (log.upstream_request ?? null) : null, preserveDetail ? (log.upstream_response ?? null) : null, log.is_retry ?? 0, log.is_failover ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null, log.session_id ?? null, log.pipeline_snapshot ?? null, log.transport_kind ?? null, log.abort_reason ?? null, log.error_code ?? null, log.headers_sent ?? null, log.resilience_action ?? null, log.resilience_reason ?? null, log.mapping_reason ?? null, log.failover_trigger ?? null, log.upstream_api_type ?? null, log.upstream_base_url ?? null, log.thinking_level ?? extractThinkingLevel(log.api_type, log.client_request ?? null), log.backend_model ?? null);
|
|
44
44
|
}
|
|
45
45
|
export function insertRequestLog(db, log, writeContext) {
|
|
46
46
|
// 文件写入:始终同步调用(WriteStream 内部异步,不阻塞事件循环)
|
|
@@ -75,8 +75,8 @@ function buildLogWhereClause(options, baseCondition) {
|
|
|
75
75
|
params.push(`%${options.client_model}%`);
|
|
76
76
|
}
|
|
77
77
|
if (options.backend_model) {
|
|
78
|
-
where += " AND rl.id IN (SELECT request_log_id FROM request_metrics WHERE backend_model
|
|
79
|
-
params.push(
|
|
78
|
+
where += " AND rl.id IN (SELECT request_log_id FROM request_metrics WHERE backend_model = ?)";
|
|
79
|
+
params.push(options.backend_model);
|
|
80
80
|
}
|
|
81
81
|
if (options.router_key_id) {
|
|
82
82
|
where += " AND rl.router_key_id = ?";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import type { MetricsPeriod, MetricsMetric, MetricsSummaryRow, MetricsTimeseriesRow } from "./metrics.js";
|
|
3
|
+
declare const MILLISECONDS_PER_SECOND = 1000;
|
|
4
|
+
export declare const BUCKET_SECONDS = 600;
|
|
5
|
+
export { MILLISECONDS_PER_SECOND };
|
|
6
|
+
export interface Metrics10minRow {
|
|
7
|
+
bucket_time: string;
|
|
8
|
+
router_key_id: string;
|
|
9
|
+
provider_id: string;
|
|
10
|
+
backend_model: string;
|
|
11
|
+
client_type: string;
|
|
12
|
+
api_type: string;
|
|
13
|
+
request_count: number;
|
|
14
|
+
sum_input_tokens: number;
|
|
15
|
+
sum_output_tokens: number;
|
|
16
|
+
sum_cache_read_tokens: number;
|
|
17
|
+
sum_cache_creation_tokens: number;
|
|
18
|
+
sum_total_duration_ms: number;
|
|
19
|
+
sum_ttft_ms: number;
|
|
20
|
+
sum_thinking_tokens: number;
|
|
21
|
+
sum_text_tokens: number;
|
|
22
|
+
sum_tool_use_tokens: number;
|
|
23
|
+
sum_thinking_duration_ms: number;
|
|
24
|
+
sum_text_duration_ms: number;
|
|
25
|
+
sum_tool_use_duration_ms: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function queryAggActivity(db: Database.Database, filters?: {
|
|
28
|
+
routerKeyId?: string;
|
|
29
|
+
providerId?: string;
|
|
30
|
+
}): {
|
|
31
|
+
bucket_time: string;
|
|
32
|
+
request_count: number;
|
|
33
|
+
}[];
|
|
34
|
+
export declare function queryAggSummary(db: Database.Database, period: MetricsPeriod, providerId?: string, backendModel?: string, routerKeyId?: string, startTime?: string, endTime?: string, clientType?: string): MetricsSummaryRow[];
|
|
35
|
+
export declare const AGG_METRIC_EXPR: Record<MetricsMetric, string>;
|
|
36
|
+
export declare function queryAggTimeseries(db: Database.Database, period: MetricsPeriod, metric: MetricsMetric, providerId?: string, backendModel?: string, routerKeyId?: string, startTime?: string, endTime?: string, clientType?: string): MetricsTimeseriesRow[];
|
|
37
|
+
export interface AggStats {
|
|
38
|
+
totalRequests: number;
|
|
39
|
+
avgTps: number;
|
|
40
|
+
totalInputTokens: number;
|
|
41
|
+
totalOutputTokens: number;
|
|
42
|
+
}
|
|
43
|
+
export declare function queryAggStats(db: Database.Database, startTime: string, endTime: string, routerKeyId?: string, providerId?: string, backendModel?: string, clientType?: string): AggStats;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { getCachedStmt } from "./helpers.js";
|
|
2
|
+
const MILLISECONDS_PER_SECOND = 1000;
|
|
3
|
+
// Shared with metrics.ts — bucket boundary computation constants
|
|
4
|
+
export const BUCKET_SECONDS = 600;
|
|
5
|
+
export { MILLISECONDS_PER_SECOND };
|
|
6
|
+
// 单条 upsert 已废弃,聚合逻辑统一由 metrics-aggregator.ts 批量 INSERT...SELECT 处理
|
|
7
|
+
// --- Activity chart query ---
|
|
8
|
+
export function queryAggActivity(db, filters) {
|
|
9
|
+
const conditions = ["bucket_time >= datetime('now', '-30 days')"];
|
|
10
|
+
const params = [];
|
|
11
|
+
if (filters?.routerKeyId) {
|
|
12
|
+
conditions.push("router_key_id = ?");
|
|
13
|
+
params.push(filters.routerKeyId);
|
|
14
|
+
}
|
|
15
|
+
if (filters?.providerId) {
|
|
16
|
+
conditions.push("provider_id = ?");
|
|
17
|
+
params.push(filters.providerId);
|
|
18
|
+
}
|
|
19
|
+
const where = conditions.join(" AND ");
|
|
20
|
+
return getCachedStmt(db, `SELECT bucket_time, SUM(request_count) AS request_count
|
|
21
|
+
FROM metrics_10min
|
|
22
|
+
WHERE ${where}
|
|
23
|
+
GROUP BY bucket_time
|
|
24
|
+
ORDER BY bucket_time ASC`).all(...params);
|
|
25
|
+
}
|
|
26
|
+
// --- Time condition builder for 10min aggregation table ---
|
|
27
|
+
const AGG_PERIOD_OFFSET = {
|
|
28
|
+
"1h": "-1 hours",
|
|
29
|
+
"5h": "-5 hours",
|
|
30
|
+
"6h": "-6 hours",
|
|
31
|
+
"24h": "-1 day",
|
|
32
|
+
"7d": "-7 days",
|
|
33
|
+
"30d": "-30 days",
|
|
34
|
+
};
|
|
35
|
+
function buildAggTimeCondition(period, startTime, endTime) {
|
|
36
|
+
if (startTime && endTime) {
|
|
37
|
+
return {
|
|
38
|
+
timeWhere: "m.bucket_time >= datetime(?) AND m.bucket_time < datetime(?)",
|
|
39
|
+
timeParams: [startTime, endTime],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
timeWhere: "m.bucket_time >= datetime('now', ?)",
|
|
44
|
+
timeParams: [AGG_PERIOD_OFFSET[period]],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// --- Summary query (供 BG3 使用) ---
|
|
48
|
+
export function queryAggSummary(db, period, providerId, backendModel, routerKeyId, startTime, endTime, clientType) {
|
|
49
|
+
const { timeWhere, timeParams } = buildAggTimeCondition(period, startTime, endTime);
|
|
50
|
+
const conditions = [timeWhere];
|
|
51
|
+
const params = [...timeParams];
|
|
52
|
+
if (providerId) {
|
|
53
|
+
conditions.push("m.provider_id = ?");
|
|
54
|
+
params.push(providerId);
|
|
55
|
+
}
|
|
56
|
+
if (backendModel) {
|
|
57
|
+
conditions.push("m.backend_model = ?");
|
|
58
|
+
params.push(backendModel);
|
|
59
|
+
}
|
|
60
|
+
if (routerKeyId) {
|
|
61
|
+
conditions.push("m.router_key_id = ?");
|
|
62
|
+
params.push(routerKeyId);
|
|
63
|
+
}
|
|
64
|
+
if (clientType) {
|
|
65
|
+
conditions.push("m.client_type = ?");
|
|
66
|
+
params.push(clientType);
|
|
67
|
+
}
|
|
68
|
+
const where = conditions.join(" AND ");
|
|
69
|
+
return getCachedStmt(db, `SELECT
|
|
70
|
+
m.provider_id, COALESCE(p.name, m.provider_id) AS provider_name,
|
|
71
|
+
m.backend_model, m.client_type,
|
|
72
|
+
SUM(m.request_count) AS request_count,
|
|
73
|
+
CASE WHEN SUM(m.request_count) > 0 THEN SUM(m.sum_ttft_ms) / SUM(m.request_count) ELSE NULL END AS avg_ttft_ms,
|
|
74
|
+
NULL AS p50_ttft_ms, NULL AS p95_ttft_ms,
|
|
75
|
+
CASE WHEN SUM(m.sum_total_duration_ms) > 0
|
|
76
|
+
THEN CAST(SUM(m.sum_output_tokens) AS REAL) * 1000.0 / SUM(m.sum_total_duration_ms)
|
|
77
|
+
ELSE NULL END AS avg_tps,
|
|
78
|
+
SUM(m.sum_input_tokens) AS total_input_tokens,
|
|
79
|
+
SUM(m.sum_output_tokens) AS total_output_tokens,
|
|
80
|
+
SUM(m.sum_cache_read_tokens) AS total_cache_hit_tokens,
|
|
81
|
+
CASE WHEN SUM(m.sum_input_tokens) > 0
|
|
82
|
+
THEN SUM(m.sum_cache_read_tokens) * 100.0 / SUM(m.sum_input_tokens)
|
|
83
|
+
ELSE NULL END AS cache_hit_rate
|
|
84
|
+
FROM metrics_10min m
|
|
85
|
+
LEFT JOIN providers p ON p.id = m.provider_id
|
|
86
|
+
WHERE ${where}
|
|
87
|
+
GROUP BY m.provider_id, m.backend_model, m.client_type
|
|
88
|
+
ORDER BY request_count DESC`).all(...params);
|
|
89
|
+
}
|
|
90
|
+
// --- Timeseries query (供 BG3 使用) ---
|
|
91
|
+
export const AGG_METRIC_EXPR = {
|
|
92
|
+
ttft: "SUM(m.sum_ttft_ms) / CASE WHEN SUM(m.request_count) > 0 THEN SUM(m.request_count) ELSE 1 END",
|
|
93
|
+
tps: "CASE WHEN SUM(m.sum_total_duration_ms) > 0 THEN CAST(SUM(m.sum_output_tokens) AS REAL) * 1000.0 / SUM(m.sum_total_duration_ms) ELSE NULL END",
|
|
94
|
+
text_tps: "CASE WHEN SUM(m.sum_text_duration_ms) > 0 THEN CAST(SUM(m.sum_text_tokens) AS REAL) * 1000.0 / SUM(m.sum_text_duration_ms) ELSE NULL END",
|
|
95
|
+
thinking_tps: "CASE WHEN SUM(m.sum_thinking_duration_ms) > 0 THEN CAST(SUM(m.sum_thinking_tokens) AS REAL) * 1000.0 / SUM(m.sum_thinking_duration_ms) ELSE NULL END",
|
|
96
|
+
tool_use_tps: "CASE WHEN SUM(m.sum_tool_use_duration_ms) > 0 THEN CAST(SUM(m.sum_tool_use_tokens) AS REAL) * 1000.0 / SUM(m.sum_tool_use_duration_ms) ELSE NULL END",
|
|
97
|
+
non_thinking_tps: "CASE WHEN SUM(m.sum_text_duration_ms) + SUM(m.sum_tool_use_duration_ms) > 0 THEN CAST(SUM(m.sum_text_tokens) + SUM(m.sum_tool_use_tokens) AS REAL) * 1000.0 / (SUM(m.sum_text_duration_ms) + SUM(m.sum_tool_use_duration_ms)) ELSE NULL END",
|
|
98
|
+
total_tps: "CASE WHEN SUM(m.sum_total_duration_ms) > 0 THEN CAST(SUM(m.sum_output_tokens) AS REAL) * 1000.0 / SUM(m.sum_total_duration_ms) ELSE NULL END",
|
|
99
|
+
tokens: "SUM(m.sum_output_tokens)",
|
|
100
|
+
cache_rate: "CASE WHEN SUM(m.sum_input_tokens) > 0 THEN SUM(m.sum_cache_read_tokens) * 1.0 / SUM(m.sum_input_tokens) ELSE NULL END",
|
|
101
|
+
request_count: "SUM(m.request_count)",
|
|
102
|
+
input_tokens: "SUM(m.sum_input_tokens)",
|
|
103
|
+
output_tokens: "SUM(m.sum_output_tokens)",
|
|
104
|
+
cache_hit_tokens: "SUM(m.sum_cache_read_tokens)",
|
|
105
|
+
};
|
|
106
|
+
const AGG_PERIOD_TOTAL_SEC = {
|
|
107
|
+
"1h": 3600,
|
|
108
|
+
"5h": 18000,
|
|
109
|
+
"6h": 21600,
|
|
110
|
+
"24h": 86400,
|
|
111
|
+
"7d": 604800,
|
|
112
|
+
"30d": 2592000,
|
|
113
|
+
};
|
|
114
|
+
const MIN_BUCKET_SEC = 60;
|
|
115
|
+
const DATA_POINT_COUNT = 10;
|
|
116
|
+
function calcBucketSec(totalSec) {
|
|
117
|
+
return Math.max(MIN_BUCKET_SEC, Math.round(totalSec / DATA_POINT_COUNT));
|
|
118
|
+
}
|
|
119
|
+
export function queryAggTimeseries(db, period, metric, providerId, backendModel, routerKeyId, startTime, endTime, clientType) {
|
|
120
|
+
const totalSec = (startTime && endTime)
|
|
121
|
+
? (new Date(endTime).getTime() - new Date(startTime).getTime()) / MILLISECONDS_PER_SECOND
|
|
122
|
+
: AGG_PERIOD_TOTAL_SEC[period];
|
|
123
|
+
const bucketSec = calcBucketSec(totalSec);
|
|
124
|
+
const { timeWhere, timeParams } = buildAggTimeCondition(period, startTime, endTime);
|
|
125
|
+
const conditions = [timeWhere];
|
|
126
|
+
const params = [...timeParams];
|
|
127
|
+
if (providerId) {
|
|
128
|
+
conditions.push("m.provider_id = ?");
|
|
129
|
+
params.push(providerId);
|
|
130
|
+
}
|
|
131
|
+
if (backendModel) {
|
|
132
|
+
conditions.push("m.backend_model = ?");
|
|
133
|
+
params.push(backendModel);
|
|
134
|
+
}
|
|
135
|
+
if (routerKeyId) {
|
|
136
|
+
conditions.push("m.router_key_id = ?");
|
|
137
|
+
params.push(routerKeyId);
|
|
138
|
+
}
|
|
139
|
+
if (clientType) {
|
|
140
|
+
conditions.push("m.client_type = ?");
|
|
141
|
+
params.push(clientType);
|
|
142
|
+
}
|
|
143
|
+
const where = conditions.join(" AND ");
|
|
144
|
+
const expr = AGG_METRIC_EXPR[metric];
|
|
145
|
+
const rows = getCachedStmt(db, `SELECT
|
|
146
|
+
(unixepoch(m.bucket_time) / CAST(? AS INTEGER)) * CAST(? AS INTEGER) AS bucket_key,
|
|
147
|
+
${expr} AS avg_value,
|
|
148
|
+
SUM(m.request_count) AS count
|
|
149
|
+
FROM metrics_10min m
|
|
150
|
+
WHERE ${where}
|
|
151
|
+
GROUP BY bucket_key
|
|
152
|
+
ORDER BY bucket_key ASC`).all(bucketSec, bucketSec, ...params);
|
|
153
|
+
return rows.map((r) => ({
|
|
154
|
+
time_bucket: new Date(r.bucket_key * MILLISECONDS_PER_SECOND).toISOString(),
|
|
155
|
+
avg_value: r.avg_value,
|
|
156
|
+
count: r.count,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
export function queryAggStats(db, startTime, endTime, routerKeyId, providerId, backendModel, clientType) {
|
|
160
|
+
const conditions = [
|
|
161
|
+
"m.bucket_time >= datetime(?)",
|
|
162
|
+
"m.bucket_time < datetime(?)",
|
|
163
|
+
];
|
|
164
|
+
const params = [startTime, endTime];
|
|
165
|
+
if (routerKeyId) {
|
|
166
|
+
conditions.push("m.router_key_id = ?");
|
|
167
|
+
params.push(routerKeyId);
|
|
168
|
+
}
|
|
169
|
+
if (providerId) {
|
|
170
|
+
conditions.push("m.provider_id = ?");
|
|
171
|
+
params.push(providerId);
|
|
172
|
+
}
|
|
173
|
+
if (backendModel) {
|
|
174
|
+
conditions.push("m.backend_model = ?");
|
|
175
|
+
params.push(backendModel);
|
|
176
|
+
}
|
|
177
|
+
if (clientType) {
|
|
178
|
+
conditions.push("m.client_type = ?");
|
|
179
|
+
params.push(clientType);
|
|
180
|
+
}
|
|
181
|
+
const where = conditions.join(" AND ");
|
|
182
|
+
const row = getCachedStmt(db, `SELECT
|
|
183
|
+
SUM(m.request_count) AS total_requests,
|
|
184
|
+
CASE WHEN SUM(m.sum_total_duration_ms) > 0
|
|
185
|
+
THEN CAST(SUM(m.sum_output_tokens) AS REAL) * 1000.0 / SUM(m.sum_total_duration_ms)
|
|
186
|
+
ELSE NULL END AS avg_tps,
|
|
187
|
+
COALESCE(SUM(m.sum_input_tokens), 0) AS total_input_tokens,
|
|
188
|
+
COALESCE(SUM(m.sum_output_tokens), 0) AS total_output_tokens
|
|
189
|
+
FROM metrics_10min m
|
|
190
|
+
WHERE ${where}`).get(...params);
|
|
191
|
+
return {
|
|
192
|
+
totalRequests: row?.total_requests ?? 0,
|
|
193
|
+
avgTps: row?.avg_tps ?? 0,
|
|
194
|
+
totalInputTokens: row?.total_input_tokens ?? 0,
|
|
195
|
+
totalOutputTokens: row?.total_output_tokens ?? 0,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
export interface MetricsAggregatorHandle {
|
|
3
|
+
stop: () => void;
|
|
4
|
+
}
|
|
5
|
+
export declare function scheduleMetricsAggregator(db: Database.Database, log: {
|
|
6
|
+
warn: (msg: string) => void;
|
|
7
|
+
info: (msg: string) => void;
|
|
8
|
+
}): MetricsAggregatorHandle;
|