llm-simple-router 0.3.7 → 0.4.1

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 (149) hide show
  1. package/README.md +144 -75
  2. package/dist/admin/constants.d.ts +1 -8
  3. package/dist/admin/constants.js +2 -8
  4. package/dist/admin/logs.js +25 -4
  5. package/dist/admin/metrics.js +7 -3
  6. package/dist/admin/recommended.d.ts +7 -0
  7. package/dist/admin/recommended.js +25 -0
  8. package/dist/admin/router-keys.js +1 -2
  9. package/dist/admin/routes.js +4 -0
  10. package/dist/admin/usage.d.ts +7 -0
  11. package/dist/admin/usage.js +66 -0
  12. package/dist/cli.js +0 -0
  13. package/dist/config/recommended.d.ts +24 -0
  14. package/dist/config/recommended.js +30 -0
  15. package/dist/constants.d.ts +8 -0
  16. package/dist/constants.js +9 -0
  17. package/dist/db/index.d.ts +7 -5
  18. package/dist/db/index.js +4 -3
  19. package/dist/db/logs.d.ts +24 -33
  20. package/dist/db/logs.js +52 -17
  21. package/dist/db/metrics.d.ts +36 -3
  22. package/dist/db/metrics.js +57 -42
  23. package/dist/db/migrations/018_add_failover_field.sql +2 -0
  24. package/dist/db/migrations/019_create_usage_windows.sql +11 -0
  25. package/dist/db/retry-rules.d.ts +0 -5
  26. package/dist/db/retry-rules.js +0 -23
  27. package/dist/db/usage-windows.d.ts +19 -0
  28. package/dist/db/usage-windows.js +37 -0
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.js +11 -8
  31. package/dist/monitor/request-tracker.d.ts +6 -0
  32. package/dist/monitor/request-tracker.js +23 -54
  33. package/dist/monitor/stream-extractor.d.ts +11 -0
  34. package/dist/monitor/stream-extractor.js +51 -0
  35. package/dist/proxy/anthropic.js +19 -32
  36. package/dist/proxy/log-helpers.d.ts +11 -4
  37. package/dist/proxy/log-helpers.js +5 -3
  38. package/dist/proxy/openai.js +18 -34
  39. package/dist/proxy/orchestrator.d.ts +52 -0
  40. package/dist/proxy/orchestrator.js +100 -0
  41. package/dist/proxy/proxy-core.d.ts +14 -26
  42. package/dist/proxy/proxy-core.js +40 -337
  43. package/dist/proxy/proxy-handler.d.ts +18 -0
  44. package/dist/proxy/proxy-handler.js +223 -0
  45. package/dist/proxy/proxy-logging.d.ts +28 -0
  46. package/dist/proxy/proxy-logging.js +122 -0
  47. package/dist/proxy/resilience.d.ts +63 -0
  48. package/dist/proxy/resilience.js +188 -0
  49. package/dist/proxy/scope.d.ts +18 -0
  50. package/dist/proxy/scope.js +37 -0
  51. package/dist/proxy/semaphore.d.ts +9 -2
  52. package/dist/proxy/semaphore.js +34 -7
  53. package/dist/proxy/stream-proxy.d.ts +7 -0
  54. package/dist/proxy/stream-proxy.js +263 -0
  55. package/dist/proxy/{upstream-call.d.ts → transport.d.ts} +25 -18
  56. package/dist/proxy/transport.js +128 -0
  57. package/dist/proxy/types.d.ts +58 -0
  58. package/dist/proxy/types.js +30 -0
  59. package/dist/proxy/usage-window-tracker.d.ts +11 -0
  60. package/dist/proxy/usage-window-tracker.js +75 -0
  61. package/dist/utils/datetime.d.ts +4 -0
  62. package/dist/utils/datetime.js +10 -0
  63. package/frontend-dist/assets/CardContent-fmM_iiuR.js +1 -0
  64. package/frontend-dist/assets/CardHeader-BzzFzZ1B.js +1 -0
  65. package/frontend-dist/assets/CardTitle-09d7O-11.js +1 -0
  66. package/frontend-dist/assets/Checkbox-DH8iqXQd.js +1 -0
  67. package/frontend-dist/assets/CollapsibleTrigger-DCRRORrU.js +1 -0
  68. package/frontend-dist/assets/Collection-DY9-Yue9.js +3 -0
  69. package/frontend-dist/assets/Dashboard-BEzoZuSm.js +3 -0
  70. package/frontend-dist/assets/DialogTitle-BeMGJzYO.js +1 -0
  71. package/frontend-dist/assets/Input-BhvZ-Up7.js +1 -0
  72. package/frontend-dist/assets/Label-DjtouWZ7.js +1 -0
  73. package/frontend-dist/assets/LogDetailDialog-BjRsy_FR.js +3 -0
  74. package/frontend-dist/assets/Login-hOCPB-34.js +1 -0
  75. package/frontend-dist/assets/Logs-C5c3BJsg.js +1 -0
  76. package/frontend-dist/assets/ModelMappings-CDjxwyyz.js +1 -0
  77. package/frontend-dist/assets/Monitor-CPAvIREG.js +1 -0
  78. package/frontend-dist/assets/PopperContent-CHNw_qb6.js +1 -0
  79. package/frontend-dist/assets/Providers-C9ZAqHxO.js +1 -0
  80. package/frontend-dist/assets/ProxyEnhancement-Ct5WbiB7.js +5 -0
  81. package/frontend-dist/assets/RetryRules-CbgyrP6w.js +1 -0
  82. package/frontend-dist/assets/RouterKeys-zmqgFEKp.js +1 -0
  83. package/frontend-dist/assets/SelectValue-CP4Sh7LP.js +1 -0
  84. package/frontend-dist/assets/Setup-BXDEPt4o.js +1 -0
  85. package/frontend-dist/assets/Switch-DF6awXqs.js +1 -0
  86. package/frontend-dist/assets/TableHeader-BKE_yVML.js +1 -0
  87. package/frontend-dist/assets/TabsTrigger-D8R7lxaI.js +1 -0
  88. package/frontend-dist/assets/TooltipTrigger-BjQXeFem.js +1 -0
  89. package/frontend-dist/assets/VisuallyHidden-B_NnkONE.js +1 -0
  90. package/frontend-dist/assets/VisuallyHiddenInput-cjeTgyDe.js +1 -0
  91. package/frontend-dist/assets/alert-dialog-BoGRIC1Q.js +1 -0
  92. package/frontend-dist/assets/badge-DIO8W_W9.js +1 -0
  93. package/frontend-dist/assets/button-qxGNBunr.js +12 -0
  94. package/frontend-dist/assets/{createLucideIcon-CCmQ9QKM.js → createLucideIcon-jHUFhqKn.js} +1 -1
  95. package/frontend-dist/assets/dialog-D8pIXeSs.js +1 -0
  96. package/frontend-dist/assets/format-CPdJtjZ5.js +1 -0
  97. package/frontend-dist/assets/index-C_disqMY.js +1 -0
  98. package/frontend-dist/assets/index-DDp6SHfg.css +1 -0
  99. package/frontend-dist/assets/lib-DjpgwSRA.js +1 -0
  100. package/frontend-dist/assets/{ohash.D__AXeF1-p4vp6Svt.js → ohash.D__AXeF1-nmJ7gFbh.js} +1 -1
  101. package/frontend-dist/assets/{useClipboard-DO-38TXr.js → useClipboard-CmLp2YGk.js} +1 -1
  102. package/frontend-dist/assets/useForwardExpose-awoGXQkg.js +1 -0
  103. package/frontend-dist/assets/useNonce-_2e-GL-A.js +1 -0
  104. package/frontend-dist/assets/x-B0G-wIAB.js +1 -0
  105. package/frontend-dist/index.html +7 -7
  106. package/package.json +1 -1
  107. package/dist/admin/services.d.ts +0 -7
  108. package/dist/admin/services.js +0 -63
  109. package/dist/proxy/retry.d.ts +0 -43
  110. package/dist/proxy/retry.js +0 -121
  111. package/dist/proxy/upstream-call.js +0 -208
  112. package/frontend-dist/assets/CardContent-CucI6u41.js +0 -1
  113. package/frontend-dist/assets/CardHeader-d-DYsWxe.js +0 -1
  114. package/frontend-dist/assets/CardTitle-CIDEQkWB.js +0 -1
  115. package/frontend-dist/assets/Checkbox-CybCw3zS.js +0 -1
  116. package/frontend-dist/assets/CollapsibleTrigger-BFNhb19_.js +0 -1
  117. package/frontend-dist/assets/Collection-DUBb4r6h.js +0 -3
  118. package/frontend-dist/assets/Dashboard-DLB6iqH1.js +0 -3
  119. package/frontend-dist/assets/DialogTitle-Dq-5o7nJ.js +0 -1
  120. package/frontend-dist/assets/Input-HN3Il0-c.js +0 -1
  121. package/frontend-dist/assets/Label-CXAeFn-r.js +0 -1
  122. package/frontend-dist/assets/LogResponseViewer-CyBzv02a.js +0 -3
  123. package/frontend-dist/assets/Login-Br3qsdxf.js +0 -1
  124. package/frontend-dist/assets/Logs-Cu_IftdS.js +0 -1
  125. package/frontend-dist/assets/ModelMappings-DXC0sNH5.js +0 -1
  126. package/frontend-dist/assets/Monitor-CKlid1sC.js +0 -1
  127. package/frontend-dist/assets/PopperContent-CnZejY31.js +0 -1
  128. package/frontend-dist/assets/Providers-8CHhW4uH.js +0 -1
  129. package/frontend-dist/assets/ProxyEnhancement-CkYeXwgH.js +0 -5
  130. package/frontend-dist/assets/RetryRules-Csb7u9W4.js +0 -1
  131. package/frontend-dist/assets/RouterKeys-C6YIufmj.js +0 -1
  132. package/frontend-dist/assets/RovingFocusItem-B7ZIkplZ.js +0 -1
  133. package/frontend-dist/assets/SelectValue-B32pgmTJ.js +0 -1
  134. package/frontend-dist/assets/Setup-Df9IQo2x.js +0 -1
  135. package/frontend-dist/assets/Switch-CLeo7H6d.js +0 -1
  136. package/frontend-dist/assets/TableHeader-BpscAtT3.js +0 -1
  137. package/frontend-dist/assets/TabsTrigger-DErAbTuM.js +0 -1
  138. package/frontend-dist/assets/VisuallyHidden-CJBR3YB3.js +0 -1
  139. package/frontend-dist/assets/VisuallyHiddenInput-Cy0VuE1l.js +0 -1
  140. package/frontend-dist/assets/alert-dialog-BAR1JRmT.js +0 -1
  141. package/frontend-dist/assets/button-D54q76GQ.js +0 -1
  142. package/frontend-dist/assets/client-Mb8fy_bC.js +0 -12
  143. package/frontend-dist/assets/dialog-DSH5k5Kj.js +0 -1
  144. package/frontend-dist/assets/index-BQBtSfem.js +0 -1
  145. package/frontend-dist/assets/index-H-lnTkMr.css +0 -1
  146. package/frontend-dist/assets/lib-BgOqOzXI.js +0 -1
  147. package/frontend-dist/assets/useForwardExpose-CzQFheaD.js +0 -1
  148. package/frontend-dist/assets/useNonce-CU-NirfM.js +0 -1
  149. package/frontend-dist/assets/x-DEJ1xpi5.js +0 -1
package/dist/db/index.js CHANGED
@@ -38,10 +38,11 @@ export function initDatabase(dbPath) {
38
38
  // --- Re-export from per-table modules ---
39
39
  export { getActiveProviders, getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
40
40
  export { getModelMapping, getAllModelMappings, createModelMapping, updateModelMapping, deleteModelMapping, getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getActiveProviderModels, resolveByProviderModel, } from "./mappings.js";
41
- export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, seedDefaultRules, } from "./retry-rules.js";
42
- export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, insertMetrics, } from "./logs.js";
41
+ export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
42
+ export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, } from "./logs.js";
43
43
  export { getRouterKeyByHash, getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "./router-keys.js";
44
- export { getMetricsSummary, getMetricsTimeseries } from "./metrics.js";
44
+ export { getMetricsSummary, getMetricsTimeseries, insertMetrics } from "./metrics.js";
45
45
  export { getStats } from "./stats.js";
46
46
  export { getSetting, setSetting, isInitialized } from "./settings.js";
47
47
  export { getSessionStates, getSessionState, getSessionHistory, upsertSessionState, insertSessionHistory, deleteSessionState, } from "./session-states.js";
48
+ export { insertWindow, getLatestWindow, getWindowsInRange, getWindowUsage, } from "./usage-windows.js";
package/dist/db/logs.d.ts CHANGED
@@ -16,6 +16,7 @@ export interface RequestLog {
16
16
  upstream_response: string | null;
17
17
  client_response: string | null;
18
18
  is_retry: number;
19
+ is_failover: number;
19
20
  original_request_id: string | null;
20
21
  original_model: string | null;
21
22
  }
@@ -24,38 +25,6 @@ export interface RequestLogListRow extends RequestLog {
24
25
  backend_model: string | null;
25
26
  provider_name: string | null;
26
27
  }
27
- export interface MetricsRow {
28
- id: string;
29
- request_log_id: string;
30
- provider_id: string;
31
- backend_model: string;
32
- api_type: string;
33
- input_tokens: number | null;
34
- output_tokens: number | null;
35
- cache_creation_tokens: number | null;
36
- cache_read_tokens: number | null;
37
- ttft_ms: number | null;
38
- total_duration_ms: number | null;
39
- tokens_per_second: number | null;
40
- stop_reason: string | null;
41
- is_complete: number;
42
- created_at: string;
43
- }
44
- export type MetricsInsert = {
45
- request_log_id: string;
46
- provider_id: string;
47
- backend_model: string;
48
- api_type: string;
49
- input_tokens?: number | null;
50
- output_tokens?: number | null;
51
- cache_creation_tokens?: number | null;
52
- cache_read_tokens?: number | null;
53
- ttft_ms?: number | null;
54
- total_duration_ms?: number | null;
55
- tokens_per_second?: number | null;
56
- stop_reason?: string | null;
57
- is_complete?: number;
58
- };
59
28
  export interface RequestLogInsert {
60
29
  id: string;
61
30
  api_type: string;
@@ -73,6 +42,7 @@ export interface RequestLogInsert {
73
42
  upstream_response?: string | null;
74
43
  client_response?: string | null;
75
44
  is_retry?: number;
45
+ is_failover?: number;
76
46
  original_request_id?: string | null;
77
47
  router_key_id?: string | null;
78
48
  original_model?: string | null;
@@ -84,10 +54,31 @@ export declare function getRequestLogs(db: Database.Database, options: {
84
54
  api_type?: string;
85
55
  model?: string;
86
56
  router_key_id?: string;
57
+ provider_id?: string;
58
+ start_time?: string;
59
+ end_time?: string;
87
60
  }): {
88
61
  data: RequestLogListRow[];
89
62
  total: number;
90
63
  };
91
64
  export declare function getRequestLogById(db: Database.Database, id: string): RequestLog | undefined;
92
65
  export declare function deleteLogsBefore(db: Database.Database, beforeDate: string): number;
93
- export declare function insertMetrics(db: Database.Database, m: MetricsInsert): string;
66
+ /** 查询某条日志的子请求(retry/failover 关联),上限 100 */
67
+ export declare function getRequestLogChildren(db: Database.Database, parentId: string): RequestLogListRow[];
68
+ export interface RequestLogGroupedRow extends RequestLogListRow {
69
+ child_count: number;
70
+ }
71
+ /** 只返回根请求(original_request_id IS NULL),附带子请求数量 */
72
+ export declare function getRequestLogsGrouped(db: Database.Database, options: {
73
+ page: number;
74
+ limit: number;
75
+ api_type?: string;
76
+ model?: string;
77
+ router_key_id?: string;
78
+ provider_id?: string;
79
+ start_time?: string;
80
+ end_time?: string;
81
+ }): {
82
+ data: RequestLogGroupedRow[];
83
+ total: number;
84
+ };
package/dist/db/logs.js CHANGED
@@ -1,10 +1,17 @@
1
- import { randomUUID } from "crypto";
1
+ // --- request_logs ---
2
+ /** 三处日志列表查询共享的 SELECT 列 + JOIN 子句 */
3
+ const LOG_LIST_SELECT = `rl.id, rl.api_type, rl.model, rl.provider_id, rl.status_code, rl.latency_ms,
4
+ rl.is_stream, rl.error_message, rl.created_at, rl.is_retry, rl.is_failover, rl.original_request_id, rl.original_model,
5
+ CASE WHEN rl.provider_id = 'router' THEN rl.upstream_request ELSE NULL END AS upstream_request,
6
+ rm.backend_model, COALESCE(p.name, rl.provider_id) AS provider_name`;
7
+ const LOG_LIST_JOIN = `LEFT JOIN request_metrics rm ON rm.request_log_id = rl.id
8
+ LEFT JOIN providers p ON p.id = rl.provider_id`;
2
9
  export function insertRequestLog(db, log) {
3
- db.prepare(`INSERT INTO request_logs (id, api_type, model, provider_id, status_code, latency_ms, is_stream, error_message, created_at, request_body, response_body, client_request, upstream_request, upstream_response, client_response, is_retry, original_request_id, router_key_id, original_model)
4
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(log.id, log.api_type, log.model, log.provider_id, log.status_code, log.latency_ms, log.is_stream, log.error_message, log.created_at, log.request_body ?? null, log.response_body ?? null, log.client_request ?? null, log.upstream_request ?? null, log.upstream_response ?? null, log.client_response ?? null, log.is_retry ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null);
10
+ db.prepare(`INSERT INTO request_logs (id, api_type, model, provider_id, status_code, latency_ms, is_stream, error_message, created_at, request_body, response_body, client_request, upstream_request, upstream_response, client_response, is_retry, is_failover, original_request_id, router_key_id, original_model)
11
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(log.id, log.api_type, log.model, log.provider_id, log.status_code, log.latency_ms, log.is_stream, log.error_message, log.created_at, log.request_body ?? null, log.response_body ?? null, log.client_request ?? null, log.upstream_request ?? null, log.upstream_response ?? null, log.client_response ?? null, log.is_retry ?? 0, log.is_failover ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null);
5
12
  }
6
- export function getRequestLogs(db, options) {
7
- let where = "1=1";
13
+ function buildLogWhereClause(options, baseCondition) {
14
+ let where = baseCondition;
8
15
  const params = [];
9
16
  if (options.api_type) {
10
17
  where += " AND rl.api_type = ?";
@@ -18,16 +25,28 @@ export function getRequestLogs(db, options) {
18
25
  where += " AND rl.router_key_id = ?";
19
26
  params.push(options.router_key_id);
20
27
  }
28
+ if (options.provider_id) {
29
+ where += " AND rl.provider_id = ?";
30
+ params.push(options.provider_id);
31
+ }
32
+ if (options.start_time) {
33
+ where += " AND rl.created_at >= ?";
34
+ params.push(options.start_time);
35
+ }
36
+ if (options.end_time) {
37
+ where += " AND rl.created_at <= ?";
38
+ params.push(options.end_time);
39
+ }
40
+ return { where, params };
41
+ }
42
+ export function getRequestLogs(db, options) {
43
+ const { where, params } = buildLogWhereClause(options, "1=1");
21
44
  const total = db.prepare(`SELECT COUNT(*) as count FROM request_logs rl WHERE ${where}`).get(...params).count;
22
45
  const offset = (options.page - 1) * options.limit;
23
46
  const data = db
24
- .prepare(`SELECT rl.id, rl.api_type, rl.model, rl.provider_id, rl.status_code, rl.latency_ms,
25
- rl.is_stream, rl.error_message, rl.created_at, rl.is_retry, rl.original_request_id, rl.original_model,
26
- CASE WHEN rl.provider_id = 'router' THEN rl.upstream_request ELSE NULL END AS upstream_request,
27
- rm.backend_model, COALESCE(p.name, rl.provider_id) AS provider_name
47
+ .prepare(`SELECT ${LOG_LIST_SELECT}
28
48
  FROM request_logs rl
29
- LEFT JOIN request_metrics rm ON rm.request_log_id = rl.id
30
- LEFT JOIN providers p ON p.id = rl.provider_id
49
+ ${LOG_LIST_JOIN}
31
50
  WHERE ${where} ORDER BY rl.created_at DESC LIMIT ? OFFSET ?`)
32
51
  .all(...params, options.limit, offset);
33
52
  return { data, total };
@@ -38,10 +57,26 @@ export function getRequestLogById(db, id) {
38
57
  export function deleteLogsBefore(db, beforeDate) {
39
58
  return db.prepare("DELETE FROM request_logs WHERE created_at < ?").run(beforeDate).changes;
40
59
  }
41
- // --- request_metrics ---
42
- export function insertMetrics(db, m) {
43
- const id = randomUUID();
44
- db.prepare(`INSERT INTO request_metrics (id, request_log_id, provider_id, backend_model, api_type, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, ttft_ms, total_duration_ms, tokens_per_second, stop_reason, is_complete)
45
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, m.request_log_id, m.provider_id, m.backend_model, m.api_type, m.input_tokens ?? null, m.output_tokens ?? null, m.cache_creation_tokens ?? null, m.cache_read_tokens ?? null, m.ttft_ms ?? null, m.total_duration_ms ?? null, m.tokens_per_second ?? null, m.stop_reason ?? null, m.is_complete ?? 1);
46
- return id;
60
+ /** 查询某条日志的子请求(retry/failover 关联),上限 100 条 */
61
+ export function getRequestLogChildren(db, parentId) {
62
+ return db.prepare(`SELECT ${LOG_LIST_SELECT}
63
+ FROM request_logs rl
64
+ ${LOG_LIST_JOIN}
65
+ WHERE rl.original_request_id = ?
66
+ ORDER BY rl.created_at ASC
67
+ LIMIT 100`).all(parentId);
68
+ }
69
+ /** 只返回根请求(original_request_id IS NULL),附带子请求数量 */
70
+ export function getRequestLogsGrouped(db, options) {
71
+ const { where, params } = buildLogWhereClause(options, "rl.original_request_id IS NULL");
72
+ const total = db.prepare(`SELECT COUNT(*) as count FROM request_logs rl WHERE ${where}`).get(...params).count;
73
+ const offset = (options.page - 1) * options.limit;
74
+ const data = db
75
+ .prepare(`SELECT ${LOG_LIST_SELECT},
76
+ (SELECT COUNT(*) FROM request_logs c WHERE c.original_request_id = rl.id) AS child_count
77
+ FROM request_logs rl
78
+ ${LOG_LIST_JOIN}
79
+ WHERE ${where} ORDER BY rl.created_at DESC LIMIT ? OFFSET ?`)
80
+ .all(...params, options.limit, offset);
81
+ return { data, total };
47
82
  }
@@ -1,6 +1,39 @@
1
1
  import Database from "better-sqlite3";
2
- export type MetricsPeriod = "1h" | "6h" | "24h" | "7d" | "30d";
2
+ export type MetricsPeriod = "1h" | "5h" | "6h" | "24h" | "7d" | "30d";
3
3
  export type MetricsMetric = "ttft" | "tps" | "tokens" | "cache_rate" | "request_count" | "input_tokens" | "output_tokens" | "cache_hit_tokens";
4
+ export interface MetricsRow {
5
+ id: string;
6
+ request_log_id: string;
7
+ provider_id: string;
8
+ backend_model: string;
9
+ api_type: string;
10
+ input_tokens: number | null;
11
+ output_tokens: number | null;
12
+ cache_creation_tokens: number | null;
13
+ cache_read_tokens: number | null;
14
+ ttft_ms: number | null;
15
+ total_duration_ms: number | null;
16
+ tokens_per_second: number | null;
17
+ stop_reason: string | null;
18
+ is_complete: number;
19
+ created_at: string;
20
+ }
21
+ export type MetricsInsert = {
22
+ request_log_id: string;
23
+ provider_id: string;
24
+ backend_model: string;
25
+ api_type: string;
26
+ input_tokens?: number | null;
27
+ output_tokens?: number | null;
28
+ cache_creation_tokens?: number | null;
29
+ cache_read_tokens?: number | null;
30
+ ttft_ms?: number | null;
31
+ total_duration_ms?: number | null;
32
+ tokens_per_second?: number | null;
33
+ stop_reason?: string | null;
34
+ is_complete?: number;
35
+ };
36
+ export declare function insertMetrics(db: Database.Database, m: MetricsInsert): string;
4
37
  export interface MetricsSummaryRow {
5
38
  provider_id: string;
6
39
  provider_name: string;
@@ -15,10 +48,10 @@ export interface MetricsSummaryRow {
15
48
  total_cache_hit_tokens: number;
16
49
  cache_hit_rate: number | null;
17
50
  }
18
- export declare function getMetricsSummary(db: Database.Database, period: MetricsPeriod, providerId?: string, backendModel?: string, routerKeyId?: string): MetricsSummaryRow[];
51
+ export declare function getMetricsSummary(db: Database.Database, period: MetricsPeriod, providerId?: string, backendModel?: string, routerKeyId?: string, startTime?: string, endTime?: string): MetricsSummaryRow[];
19
52
  export interface MetricsTimeseriesRow {
20
53
  time_bucket: string;
21
54
  avg_value: number | null;
22
55
  count: number;
23
56
  }
24
- export declare function getMetricsTimeseries(db: Database.Database, period: MetricsPeriod, metric: MetricsMetric, providerId?: string, backendModel?: string, routerKeyId?: string): MetricsTimeseriesRow[];
57
+ export declare function getMetricsTimeseries(db: Database.Database, period: MetricsPeriod, metric: MetricsMetric, providerId?: string, backendModel?: string, routerKeyId?: string, startTime?: string, endTime?: string): MetricsTimeseriesRow[];
@@ -1,5 +1,13 @@
1
+ import { randomUUID } from "crypto";
2
+ export function insertMetrics(db, m) {
3
+ const id = randomUUID();
4
+ db.prepare(`INSERT INTO request_metrics (id, request_log_id, provider_id, backend_model, api_type, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, ttft_ms, total_duration_ms, tokens_per_second, stop_reason, is_complete)
5
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, m.request_log_id, m.provider_id, m.backend_model, m.api_type, m.input_tokens ?? null, m.output_tokens ?? null, m.cache_creation_tokens ?? null, m.cache_read_tokens ?? null, m.ttft_ms ?? null, m.total_duration_ms ?? null, m.tokens_per_second ?? null, m.stop_reason ?? null, m.is_complete ?? 1);
6
+ return id;
7
+ }
1
8
  const PERIOD_OFFSET = {
2
9
  "1h": "-1 hours",
10
+ "5h": "-5 hours",
3
11
  "6h": "-6 hours",
4
12
  "24h": "-1 day",
5
13
  "7d": "-7 days",
@@ -7,6 +15,7 @@ const PERIOD_OFFSET = {
7
15
  };
8
16
  const BUCKET_SECONDS = {
9
17
  "1h": 60,
18
+ "5h": 300,
10
19
  "6h": 300,
11
20
  "24h": 900,
12
21
  "7d": 3600,
@@ -14,10 +23,39 @@ const BUCKET_SECONDS = {
14
23
  };
15
24
  // unix epoch 秒转毫秒的乘数
16
25
  const MS_PER_SEC = 1000;
17
- export function getMetricsSummary(db, period, providerId, backendModel, routerKeyId) {
18
- const offset = PERIOD_OFFSET[period];
19
- const conditions = ["rm.is_complete = 1", "rm.created_at >= datetime('now', ?)"];
20
- const params = [offset];
26
+ // 时间跨度(秒)→ 桶大小(秒)的阶梯映射,与 BUCKET_SECONDS 保持对齐
27
+ const BUCKET_THRESHOLDS = [
28
+ { maxSec: 3600, bucketSec: 60 }, // ≤1h: 1min
29
+ { maxSec: 21600, bucketSec: 300 }, // ≤6h: 5min
30
+ { maxSec: 86400, bucketSec: 900 }, // ≤1d: 15min
31
+ { maxSec: 604800, bucketSec: 3600 }, // ≤7d: 1h
32
+ ];
33
+ const FALLBACK_BUCKET_SEC = 14400; // >7d: 4h
34
+ function calculateBucketSeconds(startTime, endTime) {
35
+ const ms = new Date(endTime).getTime() - new Date(startTime).getTime();
36
+ const sec = ms / MS_PER_SEC;
37
+ const match = BUCKET_THRESHOLDS.find((t) => sec <= t.maxSec);
38
+ return match ? match.bucketSec : FALLBACK_BUCKET_SEC;
39
+ }
40
+ function buildTimeCondition(period, startTime, endTime) {
41
+ if (startTime && endTime) {
42
+ // request_metrics.created_at 用 datetime('now') 格式 (YYYY-MM-DD HH:MM:SS),
43
+ // 前端传入 ISO 8601,需要转换格式以匹配字符串比较
44
+ return {
45
+ timeWhere: "rm.created_at >= datetime(?) AND rm.created_at < datetime(?)",
46
+ timeParams: [startTime, endTime],
47
+ };
48
+ }
49
+ return {
50
+ timeWhere: "rm.created_at >= datetime('now', ?)",
51
+ timeParams: [PERIOD_OFFSET[period]],
52
+ };
53
+ }
54
+ export function getMetricsSummary(db, period, providerId, backendModel, routerKeyId, startTime, endTime) {
55
+ const { timeWhere, timeParams } = buildTimeCondition(period, startTime, endTime);
56
+ const conditions = ["rm.is_complete = 1", timeWhere];
57
+ const params = [...timeParams];
58
+ const joins = ["LEFT JOIN providers p ON p.id = rm.provider_id"];
21
59
  if (providerId) {
22
60
  conditions.push("rm.provider_id = ?");
23
61
  params.push(providerId);
@@ -26,48 +64,23 @@ export function getMetricsSummary(db, period, providerId, backendModel, routerKe
26
64
  conditions.push("rm.backend_model = ?");
27
65
  params.push(backendModel);
28
66
  }
29
- const joins = "LEFT JOIN providers p ON p.id = rm.provider_id";
30
67
  if (routerKeyId) {
68
+ joins.push("LEFT JOIN request_logs rl ON rl.id = rm.request_log_id");
31
69
  conditions.push("rl.router_key_id = ?");
32
70
  params.push(routerKeyId);
33
- return db.prepare(`
34
- SELECT
35
- rm.provider_id, COALESCE(p.name, rm.provider_id) AS provider_name, rm.backend_model,
36
- COUNT(*) AS request_count, AVG(rm.ttft_ms) AS avg_ttft_ms, NULL AS p50_ttft_ms, NULL AS p95_ttft_ms,
37
- AVG(rm.tokens_per_second) AS avg_tps,
38
- COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens, COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens,
39
- COALESCE(SUM(rm.cache_read_tokens), 0) AS total_cache_hit_tokens,
40
- CASE WHEN SUM(rm.input_tokens) > 0 THEN SUM(rm.cache_read_tokens) * 1.0 / SUM(rm.input_tokens) ELSE NULL END AS cache_hit_rate
41
- FROM request_metrics rm
42
- LEFT JOIN providers p ON p.id = rm.provider_id
43
- LEFT JOIN request_logs rl ON rl.id = rm.request_log_id
44
- WHERE ${conditions.join(" AND ")}
45
- GROUP BY rm.provider_id, rm.backend_model ORDER BY request_count DESC
46
- `).all(...params);
47
71
  }
48
- const where = conditions.join(" AND ");
49
72
  return db.prepare(`
50
73
  SELECT
51
- rm.provider_id,
52
- COALESCE(p.name, rm.provider_id) AS provider_name,
53
- rm.backend_model,
54
- COUNT(*) AS request_count,
55
- AVG(rm.ttft_ms) AS avg_ttft_ms,
56
- NULL AS p50_ttft_ms,
57
- NULL AS p95_ttft_ms,
74
+ rm.provider_id, COALESCE(p.name, rm.provider_id) AS provider_name, rm.backend_model,
75
+ COUNT(*) AS request_count, AVG(rm.ttft_ms) AS avg_ttft_ms, NULL AS p50_ttft_ms, NULL AS p95_ttft_ms,
58
76
  AVG(rm.tokens_per_second) AS avg_tps,
59
- COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens,
60
- COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens,
77
+ COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens, COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens,
61
78
  COALESCE(SUM(rm.cache_read_tokens), 0) AS total_cache_hit_tokens,
62
- CASE WHEN SUM(rm.input_tokens) > 0
63
- THEN SUM(rm.cache_read_tokens) * 1.0 / SUM(rm.input_tokens)
64
- ELSE NULL
65
- END AS cache_hit_rate
79
+ CASE WHEN SUM(rm.input_tokens) > 0 THEN SUM(rm.cache_read_tokens) * 1.0 / SUM(rm.input_tokens) ELSE NULL END AS cache_hit_rate
66
80
  FROM request_metrics rm
67
- ${joins}
68
- WHERE ${where}
69
- GROUP BY rm.provider_id, rm.backend_model
70
- ORDER BY request_count DESC
81
+ ${joins.join(" ")}
82
+ WHERE ${conditions.join(" AND ")}
83
+ GROUP BY rm.provider_id, rm.backend_model ORDER BY request_count DESC
71
84
  `).all(...params);
72
85
  }
73
86
  const METRIC_EXPR = {
@@ -80,11 +93,13 @@ const METRIC_EXPR = {
80
93
  output_tokens: "SUM(rm.output_tokens)",
81
94
  cache_hit_tokens: "SUM(rm.cache_read_tokens)",
82
95
  };
83
- export function getMetricsTimeseries(db, period, metric, providerId, backendModel, routerKeyId) {
84
- const offset = PERIOD_OFFSET[period];
85
- const bucketSec = BUCKET_SECONDS[period];
86
- const conditions = ["rm.is_complete = 1", "rm.created_at >= datetime('now', ?)"];
87
- const params = [offset];
96
+ export function getMetricsTimeseries(db, period, metric, providerId, backendModel, routerKeyId, startTime, endTime) {
97
+ const bucketSec = (startTime && endTime)
98
+ ? calculateBucketSeconds(startTime, endTime)
99
+ : BUCKET_SECONDS[period];
100
+ const { timeWhere, timeParams } = buildTimeCondition(period, startTime, endTime);
101
+ const conditions = ["rm.is_complete = 1", timeWhere];
102
+ const params = [...timeParams];
88
103
  if (providerId) {
89
104
  conditions.push("rm.provider_id = ?");
90
105
  params.push(providerId);
@@ -0,0 +1,2 @@
1
+ ALTER TABLE request_logs ADD COLUMN is_failover INTEGER NOT NULL DEFAULT 0;
2
+ CREATE INDEX IF NOT EXISTS idx_request_logs_original_request_id ON request_logs(original_request_id);
@@ -0,0 +1,11 @@
1
+ CREATE TABLE IF NOT EXISTS usage_windows (
2
+ id TEXT PRIMARY KEY,
3
+ router_key_id TEXT,
4
+ start_time TEXT NOT NULL,
5
+ end_time TEXT NOT NULL,
6
+ created_at TEXT DEFAULT (datetime('now'))
7
+ );
8
+
9
+ CREATE INDEX IF NOT EXISTS idx_usage_windows_start ON usage_windows(start_time);
10
+ CREATE INDEX IF NOT EXISTS idx_usage_windows_router_key ON usage_windows(router_key_id);
11
+ CREATE INDEX IF NOT EXISTS idx_usage_windows_end_time ON usage_windows(end_time);
@@ -25,8 +25,3 @@ export declare function createRetryRule(db: Database.Database, rule: {
25
25
  }): string;
26
26
  export declare function updateRetryRule(db: Database.Database, id: string, fields: Partial<Pick<RetryRule, "name" | "status_code" | "body_pattern" | "is_active" | "retry_strategy" | "retry_delay_ms" | "max_retries" | "max_delay_ms">>): void;
27
27
  export declare function deleteRetryRule(db: Database.Database, id: string): void;
28
- /**
29
- * 首次启动时(表为空)插入默认重试规则。
30
- * 429/503 为通用 HTTP 重试;其余为 ZAI middleware 特定 400 响应。
31
- */
32
- export declare function seedDefaultRules(db: Database.Database): void;
@@ -27,26 +27,3 @@ export function updateRetryRule(db, id, fields) {
27
27
  export function deleteRetryRule(db, id) {
28
28
  deleteById(db, "retry_rules", id);
29
29
  }
30
- // ---------- Default seed rules ----------
31
- const DEFAULT_RULES = [
32
- { name: "429 Too Many Requests", status_code: 429, body_pattern: ".*", is_active: 1, retry_strategy: "exponential", retry_delay_ms: 5000, max_retries: 10, max_delay_ms: 60000 },
33
- { name: "503 Service Unavailable", status_code: 503, body_pattern: ".*", is_active: 1, retry_strategy: "exponential", retry_delay_ms: 5000, max_retries: 10, max_delay_ms: 60000 },
34
- { name: 'ZAI 网络错误 (code 1234)', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*"code"\\s*:\\s*"1234"', is_active: 1, retry_strategy: "exponential", retry_delay_ms: 5000, max_retries: 10, max_delay_ms: 60000 },
35
- { name: 'ZAI 临时不可用', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*请稍后重试', is_active: 1, retry_strategy: "exponential", retry_delay_ms: 5000, max_retries: 10, max_delay_ms: 60000 },
36
- { name: 'ZAI 操作失败 (code 500)', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*"code"\\s*:\\s*"500"', is_active: 1, retry_strategy: "exponential", retry_delay_ms: 5000, max_retries: 10, max_delay_ms: 60000 },
37
- ];
38
- /**
39
- * 首次启动时(表为空)插入默认重试规则。
40
- * 429/503 为通用 HTTP 重试;其余为 ZAI middleware 特定 400 响应。
41
- */
42
- export function seedDefaultRules(db) {
43
- const count = db.prepare("SELECT COUNT(*) as c FROM retry_rules").get().c;
44
- if (count > 0)
45
- return;
46
- const now = new Date().toISOString();
47
- const insert = db.prepare(`INSERT INTO retry_rules (id, name, status_code, body_pattern, is_active, created_at, retry_strategy, retry_delay_ms, max_retries, max_delay_ms)
48
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
49
- for (const rule of DEFAULT_RULES) {
50
- insert.run(randomUUID(), rule.name, rule.status_code, rule.body_pattern, rule.is_active, now, rule.retry_strategy, rule.retry_delay_ms, rule.max_retries, rule.max_delay_ms);
51
- }
52
- }
@@ -0,0 +1,19 @@
1
+ import Database from "better-sqlite3";
2
+ export interface UsageWindow {
3
+ id: string;
4
+ router_key_id: string | null;
5
+ start_time: string;
6
+ end_time: string;
7
+ created_at: string;
8
+ }
9
+ export interface WindowUsage {
10
+ request_count: number;
11
+ total_input_tokens: number;
12
+ total_output_tokens: number;
13
+ }
14
+ export declare function insertWindow(db: Database.Database, w: Omit<UsageWindow, "created_at">): string;
15
+ export declare function getLatestWindow(db: Database.Database, routerKeyId?: string): UsageWindow | null;
16
+ /** 返回与 [start, end) 区间有重叠的窗口 */
17
+ export declare function getWindowsInRange(db: Database.Database, start: string, end: string, routerKeyId?: string): UsageWindow[];
18
+ /** 聚合指定时间窗口内的请求计数和 token 用量 */
19
+ export declare function getWindowUsage(db: Database.Database, startTime: string, endTime: string, routerKeyId?: string): WindowUsage;
@@ -0,0 +1,37 @@
1
+ import { randomUUID } from "crypto";
2
+ export function insertWindow(db, w) {
3
+ const id = w.id || randomUUID();
4
+ db.prepare("INSERT INTO usage_windows (id, router_key_id, start_time, end_time) VALUES (?, ?, ?, ?)").run(id, w.router_key_id ?? null, w.start_time, w.end_time);
5
+ return id;
6
+ }
7
+ export function getLatestWindow(db, routerKeyId) {
8
+ const sql = routerKeyId
9
+ ? "SELECT * FROM usage_windows WHERE router_key_id = ? ORDER BY start_time DESC LIMIT 1"
10
+ : "SELECT * FROM usage_windows ORDER BY start_time DESC LIMIT 1";
11
+ const params = routerKeyId ? [routerKeyId] : [];
12
+ return db.prepare(sql).get(...params) ?? null;
13
+ }
14
+ /** 返回与 [start, end) 区间有重叠的窗口 */
15
+ export function getWindowsInRange(db, start, end, routerKeyId) {
16
+ if (routerKeyId) {
17
+ return db.prepare("SELECT * FROM usage_windows WHERE start_time < ? AND end_time > ? AND router_key_id = ? ORDER BY start_time ASC").all(end, start, routerKeyId);
18
+ }
19
+ return db.prepare("SELECT * FROM usage_windows WHERE start_time < ? AND end_time > ? ORDER BY start_time ASC").all(end, start);
20
+ }
21
+ /** 聚合指定时间窗口内的请求计数和 token 用量 */
22
+ export function getWindowUsage(db, startTime, endTime, routerKeyId) {
23
+ const baseSql = `
24
+ SELECT
25
+ COUNT(*) AS request_count,
26
+ COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens,
27
+ COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens
28
+ FROM request_metrics rm
29
+ JOIN request_logs rl ON rl.id = rm.request_log_id
30
+ WHERE rm.is_complete = 1
31
+ AND rm.created_at >= datetime(?)
32
+ AND rm.created_at < datetime(?)`;
33
+ if (routerKeyId) {
34
+ return db.prepare(`${baseSql} AND rl.router_key_id = ?`).get(startTime, endTime, routerKeyId);
35
+ }
36
+ return db.prepare(baseSql).get(startTime, endTime);
37
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { FastifyInstance } from "fastify";
3
3
  import { Config } from "./config.js";
4
+ import { UsageWindowTracker } from "./proxy/usage-window-tracker.js";
4
5
  import Database from "better-sqlite3";
5
6
  export interface AppOptions {
6
7
  config?: Config;
@@ -9,6 +10,7 @@ export interface AppOptions {
9
10
  export declare function buildApp(options?: AppOptions): Promise<{
10
11
  app: FastifyInstance;
11
12
  db: Database.Database;
13
+ usageWindowTracker: UsageWindowTracker;
12
14
  close: () => Promise<void>;
13
15
  }>;
14
16
  export declare function main(): Promise<void>;
package/dist/index.js CHANGED
@@ -5,9 +5,7 @@ import { existsSync } from "node:fs";
5
5
  import { randomUUID } from "crypto";
6
6
  import Fastify from "fastify";
7
7
  import { insertRequestLog } from "./db/logs.js";
8
- const HTTP_NOT_FOUND = 404;
9
- const HTTP_INTERNAL_ERROR = 500;
10
- const HTTP_BAD_REQUEST = 400;
8
+ import { HTTP_NOT_FOUND, HTTP_INTERNAL_ERROR, HTTP_BAD_REQUEST } from "./constants.js";
11
9
  const PROVIDER_DEFAULT_QUEUE_TIMEOUT_MS = 5000;
12
10
  const PROVIDER_DEFAULT_MAX_QUEUE_SIZE = 100;
13
11
  // 代理路由路径 → api_type,用于在全局 hook/errorHandler 中识别代理请求
@@ -23,7 +21,8 @@ function getProxyApiType(url) {
23
21
  const __filename = fileURLToPath(import.meta.url);
24
22
  const __dirname = path.dirname(__filename);
25
23
  import { getConfig } from "./config.js";
26
- import { initDatabase, seedDefaultRules, getAllProviders } from "./db/index.js";
24
+ import { initDatabase, getAllProviders } from "./db/index.js";
25
+ import { loadRecommendedConfig } from "./config/recommended.js";
27
26
  import { authMiddleware } from "./middleware/auth.js";
28
27
  import { openaiProxy } from "./proxy/openai.js";
29
28
  import { anthropicProxy } from "./proxy/anthropic.js";
@@ -32,6 +31,7 @@ import { RetryRuleMatcher } from "./proxy/retry-rules.js";
32
31
  import { ProviderSemaphoreManager } from "./proxy/semaphore.js";
33
32
  import { RequestTracker } from "./monitor/request-tracker.js";
34
33
  import { modelState } from "./proxy/model-state.js";
34
+ import { UsageWindowTracker } from "./proxy/usage-window-tracker.js";
35
35
  import fastifyStatic from "@fastify/static";
36
36
  export async function buildApp(options) {
37
37
  const config = options?.config ?? getBaseConfig();
@@ -102,15 +102,17 @@ export async function buildApp(options) {
102
102
  }
103
103
  return reply.code(status).send({ error: { message: fastifyError.message } });
104
104
  });
105
- // 首次启动时插入默认重试规则(表为空时)
106
- seedDefaultRules(db);
105
+ loadRecommendedConfig();
107
106
  // 注入 DB 到 modelState 单例,启用会话级持久化
108
107
  modelState.init(db);
109
108
  const matcher = new RetryRuleMatcher();
110
109
  matcher.load(db);
111
110
  const semaphoreManager = new ProviderSemaphoreManager();
112
- const tracker = new RequestTracker({ semaphoreManager });
111
+ const tracker = new RequestTracker({ semaphoreManager, logger: app.log });
113
112
  tracker.startPushInterval();
113
+ // 5h 用量窗口追踪器,启动时自动补齐缺失窗口
114
+ const usageWindowTracker = new UsageWindowTracker(db);
115
+ usageWindowTracker.reconcileOnStartup();
114
116
  // 从 DB 读取已有 provider 的并发配置,初始化信号量管理器和 tracker
115
117
  const allProviders = getAllProviders(db);
116
118
  for (const p of allProviders) {
@@ -162,7 +164,7 @@ export async function buildApp(options) {
162
164
  !request.url.startsWith("/admin/api")) {
163
165
  return reply.sendFile("index.html");
164
166
  }
165
- reply.code(HTTP_NOT_FOUND).send({ error: "Not Found" });
167
+ reply.code(HTTP_NOT_FOUND).send({ error: { message: "Not Found" } });
166
168
  });
167
169
  }
168
170
  else {
@@ -174,6 +176,7 @@ export async function buildApp(options) {
174
176
  return {
175
177
  app,
176
178
  db,
179
+ usageWindowTracker,
177
180
  close: async () => {
178
181
  tracker.stopPushInterval();
179
182
  await app.close();
@@ -3,10 +3,15 @@ import { StatsAggregator } from "./stats-aggregator.js";
3
3
  import { RuntimeCollector } from "./runtime-collector.js";
4
4
  import type { ProviderSemaphoreManager } from "../proxy/semaphore.js";
5
5
  import type { ActiveRequest, ProviderConcurrencySnapshot, RuntimeMetrics, StatsSnapshot } from "./types.js";
6
+ export interface TrackerLogger {
7
+ debug(obj: Record<string, unknown>, msg: string): void;
8
+ warn(obj: Record<string, unknown>, msg: string): void;
9
+ }
6
10
  export declare class RequestTracker {
7
11
  private activeMap;
8
12
  private recentCompleted;
9
13
  private clients;
14
+ private logger?;
10
15
  private providerConfigCache;
11
16
  private pushTimer;
12
17
  private tickCount;
@@ -17,6 +22,7 @@ export declare class RequestTracker {
17
22
  constructor(deps?: {
18
23
  semaphoreManager?: ProviderSemaphoreManager;
19
24
  runtimeCollector?: RuntimeCollector;
25
+ logger?: TrackerLogger;
20
26
  });
21
27
  start(req: ActiveRequest): void;
22
28
  update(id: string, patch: Partial<ActiveRequest>): void;