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.
Files changed (146) hide show
  1. package/dist/admin/dashboard.d.ts +7 -0
  2. package/dist/admin/dashboard.js +93 -0
  3. package/dist/admin/metrics.js +13 -0
  4. package/dist/admin/routes.js +2 -0
  5. package/dist/admin/settings.js +17 -1
  6. package/dist/admin/stats.js +2 -1
  7. package/dist/admin/usage.js +69 -1
  8. package/dist/core/constants.d.ts +1 -0
  9. package/dist/core/constants.js +1 -0
  10. package/dist/db/index.d.ts +2 -1
  11. package/dist/db/index.js +1 -1
  12. package/dist/db/log-cleaner.js +9 -2
  13. package/dist/db/logs.d.ts +1 -0
  14. package/dist/db/logs.js +4 -4
  15. package/dist/db/metrics-10min.d.ts +43 -0
  16. package/dist/db/metrics-10min.js +197 -0
  17. package/dist/db/metrics-aggregator.d.ts +8 -0
  18. package/dist/db/metrics-aggregator.js +125 -0
  19. package/dist/db/metrics.d.ts +2 -1
  20. package/dist/db/metrics.js +175 -5
  21. package/dist/db/migrations/054_add_backend_model_index.sql +5 -0
  22. package/dist/db/migrations/055_metrics_10min.sql +25 -0
  23. package/dist/db/migrations/056_backfill_metrics_10min.sql +67 -0
  24. package/dist/db/settings.d.ts +2 -0
  25. package/dist/db/settings.js +9 -0
  26. package/dist/db/stats.d.ts +3 -2
  27. package/dist/db/stats.js +62 -1
  28. package/dist/index.js +3 -0
  29. package/dist/proxy/handler/failover-loop.js +3 -0
  30. package/dist/proxy/hooks/builtin/error-logging.js +2 -0
  31. package/dist/proxy/log-helpers.d.ts +2 -0
  32. package/dist/proxy/log-helpers.js +4 -2
  33. package/dist/proxy/proxy-logging.js +4 -0
  34. package/dist/proxy/transport/transport-fn.js +6 -2
  35. package/frontend-dist/assets/AuthLayout-BNvGjJv3.js +1 -0
  36. package/frontend-dist/assets/{Card-BVxPbRVS.js → Card-BNz2IkLE.js} +1 -1
  37. package/frontend-dist/assets/CardContent-DszVPJgx.js +1 -0
  38. package/frontend-dist/assets/CardTitle-BeBkx0ns.js +1 -0
  39. package/frontend-dist/assets/CascadingModelSelect-Z7-ur9_p.js +1 -0
  40. package/frontend-dist/assets/Checkbox-CAQnc0qV.js +1 -0
  41. package/frontend-dist/assets/CollapsibleContent-CHlSINCS.js +1 -0
  42. package/frontend-dist/assets/CollapsibleTrigger-CJMUoPRa.js +1 -0
  43. package/frontend-dist/assets/ConcurrencyControl-BcUA816m.js +1 -0
  44. package/frontend-dist/assets/Dashboard-DXvcAfAx.js +3 -0
  45. package/frontend-dist/assets/{Input-qtzKspPF.js → Input-Dr2XqfSh.js} +1 -1
  46. package/frontend-dist/assets/Label-Ct_DR7Ui.js +1 -0
  47. package/frontend-dist/assets/Login-C8_p8684.js +1 -0
  48. package/frontend-dist/assets/Logs-BDrXnYb1.js +1 -0
  49. package/frontend-dist/assets/ModelMappings-CeLps4LM.js +1 -0
  50. package/frontend-dist/assets/Monitor-By1ywNWD.js +1 -0
  51. package/frontend-dist/assets/Providers-Cp-gomQY.js +1 -0
  52. package/frontend-dist/assets/ProxyEnhancement-D5jsMnL3.js +1 -0
  53. package/frontend-dist/assets/QuickSetup-1ADLBexY.js +1 -0
  54. package/frontend-dist/assets/RetryRules-Dk0OxVLs.js +1 -0
  55. package/frontend-dist/assets/{RouterKeys-DEMKBc7g.css → RouterKeys-B0enpkQK.css} +1 -1
  56. package/frontend-dist/assets/RouterKeys-BvD4POUA.js +1 -0
  57. package/frontend-dist/assets/RovingFocusItem-DlOsi-lb.js +1 -0
  58. package/frontend-dist/assets/Schedules-CVJcpHlE.js +1 -0
  59. package/frontend-dist/assets/Separator-BO94Jk9p.js +1 -0
  60. package/frontend-dist/assets/Settings-MYcQoEZw.js +6 -0
  61. package/frontend-dist/assets/Setup-mAyd7FhD.js +1 -0
  62. package/frontend-dist/assets/Skeleton-Di0WMQf-.js +1 -0
  63. package/frontend-dist/assets/Switch-BS1G55M2.js +1 -0
  64. package/frontend-dist/assets/TableHeader-jCTjghgy.js +1 -0
  65. package/frontend-dist/assets/TabsTrigger-B5HyH2xO.js +1 -0
  66. package/frontend-dist/assets/UnifiedRequestDialog-CMbYIrxT.css +1 -0
  67. package/frontend-dist/assets/UnifiedRequestDialog-CQ5Rzs11.js +3 -0
  68. package/frontend-dist/assets/VisuallyHiddenInput-DiW4Si0N.js +1 -0
  69. package/frontend-dist/assets/arrow-down-BsHwq97B.js +1 -0
  70. package/frontend-dist/assets/badge-FksLt30f.js +1 -0
  71. package/frontend-dist/assets/{button-BJEwiB8A.js → button-BCxYWEaM.js} +7 -7
  72. package/frontend-dist/assets/chevron-right-C3U2yjrf.js +1 -0
  73. package/frontend-dist/assets/common-WzFPzDR_.js +1 -0
  74. package/frontend-dist/assets/common-vLJ_SFUM.js +1 -0
  75. package/frontend-dist/assets/dashboard-C50KHy8J.js +1 -0
  76. package/frontend-dist/assets/dashboard-DxQEVaH8.js +1 -0
  77. package/frontend-dist/assets/dialog-B0wgNzud.js +1 -0
  78. package/frontend-dist/assets/{image-DWAFh4CH.js → image-DlPnda0z.js} +1 -1
  79. package/frontend-dist/assets/index-BTfjdJDq.css +1 -0
  80. package/frontend-dist/assets/index-CwjudU09.js +3 -0
  81. package/frontend-dist/assets/logs-BUYq0WgN.js +1 -0
  82. package/frontend-dist/assets/logs-toInbfD3.js +1 -0
  83. package/frontend-dist/assets/model-patches-Dv2jfKSp.js +1 -0
  84. package/frontend-dist/assets/{pencil-BAYWJXMP.js → pencil-Y6MJERvc.js} +1 -1
  85. package/frontend-dist/assets/plus-FNO9X5JG.js +1 -0
  86. package/frontend-dist/assets/search-DpWQ2wvO.js +1 -0
  87. package/frontend-dist/assets/settings-C1AmEyXr.js +1 -0
  88. package/frontend-dist/assets/settings-DHufHb9v.js +1 -0
  89. package/frontend-dist/assets/{sparkles-BXqZs-3m.js → sparkles-C44_iMrx.js} +1 -1
  90. package/frontend-dist/assets/transform-domain-FnCfUKrh.js +1 -0
  91. package/frontend-dist/assets/{trash-2-AdzXWmCq.js → trash-2-BEOdZk69.js} +1 -1
  92. package/frontend-dist/assets/{useClipboard-uOuEel2P.js → useClipboard-Bw6GLR6I.js} +1 -1
  93. package/frontend-dist/assets/useLogRetention-CQHD99Fy.js +1 -0
  94. package/frontend-dist/assets/{useProviderGroups-BCau8ZEY.js → useProviderGroups-Bz7Yz1b7.js} +1 -1
  95. package/frontend-dist/index.html +3 -3
  96. package/package.json +1 -1
  97. package/frontend-dist/assets/AuthLayout-CrDZ0S1l.js +0 -1
  98. package/frontend-dist/assets/CardContent-mtMPJeBk.js +0 -1
  99. package/frontend-dist/assets/CardTitle-B57j9XPI.js +0 -1
  100. package/frontend-dist/assets/CascadingModelSelect-CEA1IWYp.js +0 -1
  101. package/frontend-dist/assets/Checkbox-Dy91V6uO.js +0 -1
  102. package/frontend-dist/assets/CollapsibleContent-BNYRQZDC.js +0 -1
  103. package/frontend-dist/assets/CollapsibleTrigger-BsRqX2Cu.js +0 -1
  104. package/frontend-dist/assets/ConcurrencyControl-CYMId2CN.js +0 -1
  105. package/frontend-dist/assets/Dashboard-CdyqA2IJ.js +0 -3
  106. package/frontend-dist/assets/Label-C-4yGDcj.js +0 -1
  107. package/frontend-dist/assets/Login-Djwy2tf8.js +0 -1
  108. package/frontend-dist/assets/Logs-CPjawsdI.js +0 -1
  109. package/frontend-dist/assets/ModelMappings-BPAkfDsD.js +0 -1
  110. package/frontend-dist/assets/Monitor-C9evAeEj.js +0 -1
  111. package/frontend-dist/assets/Providers-FYxhWwD9.js +0 -1
  112. package/frontend-dist/assets/ProxyEnhancement-D7ZP80Lz.js +0 -1
  113. package/frontend-dist/assets/QuickSetup-DGt0nShS.js +0 -1
  114. package/frontend-dist/assets/RetryRules-3-6_iVb0.js +0 -1
  115. package/frontend-dist/assets/RouterKeys-ISCjK-Ct.js +0 -1
  116. package/frontend-dist/assets/RovingFocusItem-Ch8O0jki.js +0 -1
  117. package/frontend-dist/assets/Schedules-DQgLcSFw.js +0 -1
  118. package/frontend-dist/assets/Settings-BU3ivwpF.js +0 -6
  119. package/frontend-dist/assets/Setup-CMZiW_BK.js +0 -1
  120. package/frontend-dist/assets/Skeleton-Lckr7Vzh.js +0 -1
  121. package/frontend-dist/assets/Switch-vb3eQrUY.js +0 -1
  122. package/frontend-dist/assets/TableHeader-DvgxagEq.js +0 -1
  123. package/frontend-dist/assets/TabsTrigger-vvniTekl.js +0 -1
  124. package/frontend-dist/assets/TooltipTrigger-B_7uc54_.js +0 -1
  125. package/frontend-dist/assets/UnifiedRequestDialog-CHh7SjPE.js +0 -3
  126. package/frontend-dist/assets/UnifiedRequestDialog-DaZodSzm.css +0 -1
  127. package/frontend-dist/assets/VisuallyHiddenInput-DPDB7WoD.js +0 -1
  128. package/frontend-dist/assets/arrow-down-BYVAWREL.js +0 -1
  129. package/frontend-dist/assets/badge-BAIzzgzC.js +0 -1
  130. package/frontend-dist/assets/chevron-right-CR3YFy_O.js +0 -1
  131. package/frontend-dist/assets/common-ZLJwmI0c.js +0 -1
  132. package/frontend-dist/assets/common-qa3O5YJX.js +0 -1
  133. package/frontend-dist/assets/dashboard-BlCt9ud6.js +0 -1
  134. package/frontend-dist/assets/dashboard-CqwGLWoK.js +0 -1
  135. package/frontend-dist/assets/dialog-BwSmcgaV.js +0 -1
  136. package/frontend-dist/assets/index-3DIQxot0.js +0 -3
  137. package/frontend-dist/assets/index-BayP76hx.css +0 -1
  138. package/frontend-dist/assets/logs-BI_TLeBe.js +0 -1
  139. package/frontend-dist/assets/logs-DZyb4-Ew.js +0 -1
  140. package/frontend-dist/assets/model-patches-COK1yfWm.js +0 -1
  141. package/frontend-dist/assets/plus-B_GSrrcs.js +0 -1
  142. package/frontend-dist/assets/search-B87CsLO4.js +0 -1
  143. package/frontend-dist/assets/settings-av7tA8wt.js +0 -1
  144. package/frontend-dist/assets/settings-dzu7sg6b.js +0 -1
  145. package/frontend-dist/assets/transform-domain-DLWadwZU.js +0 -1
  146. package/frontend-dist/assets/useLogRetention-DstR1E-X.js +0 -1
@@ -0,0 +1,7 @@
1
+ import { FastifyPluginCallback } from "fastify";
2
+ import Database from "better-sqlite3";
3
+ interface DashboardRoutesOptions {
4
+ db: Database.Database;
5
+ }
6
+ export declare const adminDashboardRoutes: FastifyPluginCallback<DashboardRoutesOptions>;
7
+ export {};
@@ -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
+ }
@@ -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
  };
@@ -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 () => {
@@ -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
  /** 递归计算目录下所有文件的总大小(字节) */
@@ -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();
@@ -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
- function getDailyUsage(db, startTime, endTime, routerKeyId, providerId) {
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;
@@ -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>;
@@ -24,6 +24,7 @@ export function getProxyApiType(url) {
24
24
  return PROXY_API_TYPES[path] ?? null;
25
25
  }
26
26
  export const MS_PER_SECOND = 1000;
27
+ export const SECONDS_PER_DAY = 86_400;
27
28
  // 上游成功状态码
28
29
  export const UPSTREAM_SUCCESS = 200;
29
30
  /** 过滤掉不应转发给下游的 hop-by-hop headers */
@@ -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";
@@ -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
- return logDeleted + toolErrorDeleted;
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
@@ -64,6 +64,7 @@ export interface RequestLogInsert {
64
64
  upstream_api_type?: string | null;
65
65
  upstream_base_url?: string | null;
66
66
  thinking_level?: string;
67
+ backend_model?: string | null;
67
68
  }
68
69
  export interface LogWriteContext {
69
70
  matcher?: RetryMatcher | null;
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 LIKE ?)";
79
- params.push(`%${options.backend_model}%`);
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;