llm-simple-router 0.10.6 → 0.10.8

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 (114) hide show
  1. package/dist/admin/providers.js +1 -0
  2. package/dist/config/model-context.d.ts +2 -0
  3. package/dist/config/model-context.js +15 -4
  4. package/dist/core/container.d.ts +1 -0
  5. package/dist/core/container.js +1 -0
  6. package/dist/core/monitor/request-tracker.d.ts +1 -0
  7. package/dist/core/monitor/request-tracker.js +7 -1
  8. package/dist/core/types.d.ts +2 -0
  9. package/dist/db/helpers.d.ts +1 -0
  10. package/dist/db/helpers.js +15 -0
  11. package/dist/db/index.js +6 -0
  12. package/dist/db/log-write-buffer.d.ts +43 -0
  13. package/dist/db/log-write-buffer.js +91 -0
  14. package/dist/db/logs.d.ts +9 -1
  15. package/dist/db/logs.js +66 -24
  16. package/dist/db/metrics.d.ts +10 -0
  17. package/dist/db/metrics.js +26 -4
  18. package/dist/db/migrations/044_add_performance_indexes.sql +8 -0
  19. package/dist/db/migrations/045_add_metrics_composite_indexes.sql +8 -0
  20. package/dist/db/settings.js +22 -5
  21. package/dist/index.js +18 -0
  22. package/dist/metrics/metrics-extractor.d.ts +8 -3
  23. package/dist/metrics/metrics-extractor.js +31 -20
  24. package/dist/metrics/sse-parser.js +5 -2
  25. package/dist/middleware/auth.d.ts +1 -1
  26. package/dist/middleware/auth.js +12 -20
  27. package/dist/proxy/handler/create-proxy-handler.js +6 -2
  28. package/dist/proxy/handler/failover-loop.js +40 -22
  29. package/dist/proxy/hooks/builtin/allowed-models.js +8 -18
  30. package/dist/proxy/hooks/builtin/cache-estimation.js +4 -0
  31. package/dist/proxy/orchestration/resilience.js +4 -2
  32. package/dist/proxy/proxy-logging.d.ts +1 -1
  33. package/dist/proxy/proxy-logging.js +32 -17
  34. package/dist/proxy/routing/enhancement-config.d.ts +2 -0
  35. package/dist/proxy/routing/enhancement-config.js +21 -4
  36. package/dist/proxy/routing/mapping-resolver.d.ts +3 -1
  37. package/dist/proxy/routing/mapping-resolver.js +4 -2
  38. package/dist/proxy/transport/proxy-agent.d.ts +8 -0
  39. package/dist/proxy/transport/proxy-agent.js +21 -0
  40. package/dist/proxy/transport/stream.js +26 -8
  41. package/dist/proxy/transport/transport-fn.js +3 -1
  42. package/dist/storage/log-file-writer.d.ts +8 -1
  43. package/dist/storage/log-file-writer.js +41 -3
  44. package/dist/utils/token-counter.d.ts +5 -0
  45. package/dist/utils/token-counter.js +23 -0
  46. package/frontend-dist/assets/{CardContent-B3BkvaAc.js → CardContent-B4vB3Kxw.js} +1 -1
  47. package/frontend-dist/assets/{CardTitle-_AfAmHWW.js → CardTitle-BSki67ff.js} +1 -1
  48. package/frontend-dist/assets/{Checkbox-Bq_JpeJR.js → Checkbox-dvUbwMMH.js} +1 -1
  49. package/frontend-dist/assets/CollapsibleContent-DRIfmabo.js +1 -0
  50. package/frontend-dist/assets/CollapsibleTrigger-jbSdCpiC.js +1 -0
  51. package/frontend-dist/assets/Dashboard-eXf2n3wh.js +3 -0
  52. package/frontend-dist/assets/{Input-BwMPjZew.js → Input-CbiXfK8n.js} +1 -1
  53. package/frontend-dist/assets/Label-CoK65l1t.js +1 -0
  54. package/frontend-dist/assets/Login-NhjpJfPs.js +1 -0
  55. package/frontend-dist/assets/Logs-X2RH5tcC.js +1 -0
  56. package/frontend-dist/assets/MappingEntryEditor-CjHMOGsf.js +1 -0
  57. package/frontend-dist/assets/ModelCard-DJwfR5SC.js +1 -0
  58. package/frontend-dist/assets/ModelMappings-C1yFuU0K.js +1 -0
  59. package/frontend-dist/assets/Monitor-4HZvjdBX.js +1 -0
  60. package/frontend-dist/assets/Providers-Cw9Ba8PT.js +1 -0
  61. package/frontend-dist/assets/ProxyEnhancement-PXI_P4Da.js +1 -0
  62. package/frontend-dist/assets/QuickSetup-CAmUIwv2.js +1 -0
  63. package/frontend-dist/assets/RetryRules-D2ykGSLa.js +1 -0
  64. package/frontend-dist/assets/RouterKeys-BvvL8qcy.js +1 -0
  65. package/frontend-dist/assets/{RovingFocusItem-CmGUQVbI.js → RovingFocusItem-gP6WU5WD.js} +1 -1
  66. package/frontend-dist/assets/Schedules-DvfShW7S.js +1 -0
  67. package/frontend-dist/assets/Settings-BnUEzcvF.js +6 -0
  68. package/frontend-dist/assets/Setup-BIZZg-XO.js +1 -0
  69. package/frontend-dist/assets/{Switch-CWJEJhAE.js → Switch-CXemfUY5.js} +1 -1
  70. package/frontend-dist/assets/{TooltipTrigger-BirqVXYf.js → TooltipTrigger-BwLkoBZK.js} +1 -1
  71. package/frontend-dist/assets/TransformRulesForm-0xoRLIOz.js +1 -0
  72. package/frontend-dist/assets/UnifiedRequestDialog-CFrJXdaw.js +3 -0
  73. package/frontend-dist/assets/{VisuallyHiddenInput-D-mGxG1B.js → VisuallyHiddenInput-CvUbgFmg.js} +1 -1
  74. package/frontend-dist/assets/{button-DtYZp433.js → button-BqYUfybJ.js} +2 -2
  75. package/frontend-dist/assets/{copy-DEIL_qqy.js → copy-AYfsh6Pt.js} +1 -1
  76. package/frontend-dist/assets/{dashboard-B1pq4be7.js → dashboard-COCyp2p_.js} +1 -1
  77. package/frontend-dist/assets/{dashboard-BVRlMB_W.js → dashboard-DjgmcUG5.js} +1 -1
  78. package/frontend-dist/assets/dialog-CqEJe3JV.js +1 -0
  79. package/frontend-dist/assets/index-DcD6M87r.js +3 -0
  80. package/frontend-dist/assets/index-DeeDpH_W.css +1 -0
  81. package/frontend-dist/assets/mappings-6w7mc8YK.js +1 -0
  82. package/frontend-dist/assets/mappings-C1fK_e70.js +1 -0
  83. package/frontend-dist/assets/{schedules-d2NQ-xEH.js → schedules-Bd66RL7P.js} +1 -1
  84. package/frontend-dist/assets/{schedules-Dul_xl7u.js → schedules-HDwMuDgX.js} +1 -1
  85. package/frontend-dist/assets/{trash-2-CYe-L1uQ.js → trash-2-BNKlpO30.js} +1 -1
  86. package/frontend-dist/assets/{useClipboard-DojwGFBn.js → useClipboard-ByGWVweg.js} +1 -1
  87. package/frontend-dist/assets/{useLogRetention-BrYP2mf7.js → useLogRetention-BASEOb38.js} +1 -1
  88. package/frontend-dist/index.html +6 -3
  89. package/package.json +1 -1
  90. package/frontend-dist/assets/CollapsibleContent-DqPh91QX.js +0 -1
  91. package/frontend-dist/assets/CollapsibleTrigger-LG3l2pdm.js +0 -1
  92. package/frontend-dist/assets/Dashboard-0LPjTck9.js +0 -3
  93. package/frontend-dist/assets/Label-rIqXe61w.js +0 -1
  94. package/frontend-dist/assets/Login-W85mNIn5.js +0 -1
  95. package/frontend-dist/assets/Logs-ahc8KSDe.js +0 -1
  96. package/frontend-dist/assets/MappingEntryEditor-7Kf2-J2B.js +0 -1
  97. package/frontend-dist/assets/ModelCard-BfAUo6un.js +0 -1
  98. package/frontend-dist/assets/ModelMappings-BSbzeof5.js +0 -1
  99. package/frontend-dist/assets/Monitor-dCya3SFN.js +0 -1
  100. package/frontend-dist/assets/Providers-1wDl4D_R.js +0 -1
  101. package/frontend-dist/assets/ProxyEnhancement-D_IU9PcA.js +0 -1
  102. package/frontend-dist/assets/QuickSetup-xS9ROA_-.js +0 -1
  103. package/frontend-dist/assets/RetryRules-16bxf7eE.js +0 -1
  104. package/frontend-dist/assets/RouterKeys-busp00XZ.js +0 -1
  105. package/frontend-dist/assets/Schedules-DIOQSB85.js +0 -1
  106. package/frontend-dist/assets/Settings-BZ40lTsk.js +0 -6
  107. package/frontend-dist/assets/Setup-kCvg6E-U.js +0 -1
  108. package/frontend-dist/assets/TransformRulesForm-CxfgQX02.js +0 -1
  109. package/frontend-dist/assets/UnifiedRequestDialog-CDQ17q1s.js +0 -3
  110. package/frontend-dist/assets/dialog-CKP56XIn.js +0 -1
  111. package/frontend-dist/assets/index-C6lfMcX8.css +0 -1
  112. package/frontend-dist/assets/index-CWlf_u-I.js +0 -3
  113. package/frontend-dist/assets/mappings-Cazz3EF4.js +0 -1
  114. package/frontend-dist/assets/mappings-DQRteuwa.js +0 -1
@@ -138,6 +138,7 @@ export const adminProviderRoutes = (app, options, done) => {
138
138
  name: s.name,
139
139
  api_type: s.api_type,
140
140
  base_url: s.base_url,
141
+ upstream_path: s.upstream_path,
141
142
  api_key: s.api_key ? decrypt(s.api_key, encryptionKey) : "",
142
143
  models: buildModelInfoList(modelEntries, overrides),
143
144
  is_active: s.is_active,
@@ -16,5 +16,7 @@ export declare const OVERFLOW_THRESHOLD = 1000000;
16
16
  export declare function lookupContextWindow(modelName: string): number;
17
17
  /** 标准化 patch 名称:连字符 → 下划线 */
18
18
  export declare function normalizePatchName(name: string): string;
19
+ /** 清除缓存(仅供测试使用) */
20
+ export declare function clearModelsCache(): void;
19
21
  export declare function parseModels(raw: string): ModelEntry[];
20
22
  export declare function buildModelInfoList(modelEntries: ModelEntry[], overrides: Map<string, number>): ModelInfo[];
@@ -104,14 +104,23 @@ const PATCH_ID_MIGRATION = {
104
104
  non_ds_tools: "thinking_consistency",
105
105
  cache_control: "thinking_consistency",
106
106
  };
107
+ // parseModels 缓存,key 为 raw 字符串引用
108
+ const modelsCache = new Map();
109
+ /** 清除缓存(仅供测试使用) */
110
+ export function clearModelsCache() {
111
+ modelsCache.clear();
112
+ }
107
113
  export function parseModels(raw) {
108
114
  if (!raw)
109
115
  return [];
116
+ const cached = modelsCache.get(raw);
117
+ if (cached)
118
+ return cached;
110
119
  try {
111
120
  const parsed = JSON.parse(raw);
112
121
  if (!Array.isArray(parsed))
113
122
  return [];
114
- return parsed.map((item) => {
123
+ const result = parsed.map((item) => {
115
124
  if (typeof item === 'string') {
116
125
  return item ? { name: item, patches: [] } : null;
117
126
  }
@@ -124,14 +133,16 @@ export function parseModels(raw) {
124
133
  const rawPatches = (obj.patches ?? []).map(normalizePatchName);
125
134
  const migrated = rawPatches.map(p => PATCH_ID_MIGRATION[p] ?? p);
126
135
  const patches = [...new Set(migrated)];
127
- const result = {
136
+ const entry = {
128
137
  name: modelName,
129
138
  patches,
130
139
  };
131
140
  if (obj.stream_timeout_ms != null)
132
- result.stream_timeout_ms = obj.stream_timeout_ms;
133
- return result;
141
+ entry.stream_timeout_ms = obj.stream_timeout_ms;
142
+ return entry;
134
143
  }).filter((e) => e !== null);
144
+ modelsCache.set(raw, result);
145
+ return result;
135
146
  }
136
147
  catch {
137
148
  return [];
@@ -10,6 +10,7 @@ export declare const SERVICE_KEYS: {
10
10
  readonly pluginRegistry: "pluginRegistry";
11
11
  readonly formatRegistry: "formatRegistry";
12
12
  readonly logFileWriter: "logFileWriter";
13
+ readonly logWriteBuffer: "logWriteBuffer";
13
14
  readonly proxyAgentFactory: "proxyAgentFactory";
14
15
  };
15
16
  export type ServiceKey = (typeof SERVICE_KEYS)[keyof typeof SERVICE_KEYS];
@@ -10,6 +10,7 @@ export const SERVICE_KEYS = {
10
10
  pluginRegistry: "pluginRegistry",
11
11
  formatRegistry: "formatRegistry",
12
12
  logFileWriter: "logFileWriter",
13
+ logWriteBuffer: "logWriteBuffer",
13
14
  proxyAgentFactory: "proxyAgentFactory",
14
15
  };
15
16
  /**
@@ -23,6 +23,7 @@ export declare class RequestTracker {
23
23
  private providerConfigCache;
24
24
  private pushTimer;
25
25
  private tickCount;
26
+ private requestUpdateDirty;
26
27
  private streamAccumulators;
27
28
  private streamContentPending;
28
29
  private streamContentTimer;
@@ -15,6 +15,7 @@ export class RequestTracker {
15
15
  providerConfigCache = new Map();
16
16
  pushTimer = null;
17
17
  tickCount = 0;
18
+ requestUpdateDirty = true;
18
19
  streamAccumulators = new Map();
19
20
  streamContentPending = new Set();
20
21
  streamContentTimer = null;
@@ -38,6 +39,7 @@ export class RequestTracker {
38
39
  // --- Core methods ---
39
40
  start(req) {
40
41
  this.activeMap.set(req.id, { ...req });
42
+ this.requestUpdateDirty = true;
41
43
  this.logger?.debug?.({ reqId: req.id, model: req.model, providerId: req.providerId, activeCount: this.activeMap.size }, "Tracker: start");
42
44
  this.broadcast("request_start", req);
43
45
  }
@@ -133,6 +135,7 @@ export class RequestTracker {
133
135
  this.recentCompleted.length = RECENT_COMPLETED_MAX;
134
136
  }
135
137
  this.logger?.debug?.({ reqId: id, status: result.status, statusCode, latency, activeCount: this.activeMap.size }, "Tracker: complete");
138
+ this.requestUpdateDirty = true;
136
139
  this.broadcast("request_complete", completed);
137
140
  }
138
141
  /** Update stream metrics for a completed request (e.g., after cache estimation) */
@@ -278,7 +281,10 @@ export class RequestTracker {
278
281
  this.tickCount++;
279
282
  this.cleanupRecent();
280
283
  this.cleanupStaleActive();
281
- this.broadcast("request_update", this.getActive());
284
+ if (this.requestUpdateDirty) {
285
+ this.broadcast("request_update", this.getActive());
286
+ this.requestUpdateDirty = false;
287
+ }
282
288
  this.broadcast("concurrency_update", this.getConcurrency());
283
289
  this.broadcast("stats_update", this.getStats());
284
290
  // Every 10s (every 2nd tick)
@@ -26,6 +26,8 @@ export interface ResolveResult {
26
26
  concurrency_override?: ConcurrencyOverride;
27
27
  /** 活跃规则(schedule 或 base)中的 target 总数,用于 failover 判断 */
28
28
  targetCount: number;
29
+ /** 排除前的完整 target 列表,用于请求级缓存(BP-H2) */
30
+ allTargets?: Target[];
29
31
  }
30
32
  export interface MetricsResult {
31
33
  input_tokens: number | null;
@@ -1,4 +1,5 @@
1
1
  import Database from "better-sqlite3";
2
+ export declare function getCachedStmt(db: Database.Database, sql: string): Database.Statement;
2
3
  /**
3
4
  * 通用 UPDATE 构建器。
4
5
  * 用白名单过滤安全字段,拼接 SET 子句。
@@ -1,3 +1,18 @@
1
+ /** WeakMap 按 db 实例缓存 prepared statements,避免重复 prepare() */
2
+ const stmtCache = new WeakMap();
3
+ export function getCachedStmt(db, sql) {
4
+ let cache = stmtCache.get(db);
5
+ if (!cache) {
6
+ cache = new Map();
7
+ stmtCache.set(db, cache);
8
+ }
9
+ let stmt = cache.get(sql);
10
+ if (!stmt) {
11
+ stmt = db.prepare(sql);
12
+ cache.set(sql, stmt);
13
+ }
14
+ return stmt;
15
+ }
1
16
  /**
2
17
  * 通用 UPDATE 构建器。
3
18
  * 用白名单过滤安全字段,拼接 SET 子句。
package/dist/db/index.js CHANGED
@@ -28,6 +28,12 @@ export function initDatabase(dbPath) {
28
28
  db.pragma("journal_mode = WAL");
29
29
  db.pragma("auto_vacuum = INCREMENTAL");
30
30
  db.pragma("foreign_keys = ON");
31
+ db.pragma("synchronous = NORMAL");
32
+ db.pragma("cache_size = -16000");
33
+ db.pragma("busy_timeout = 5000");
34
+ db.pragma("temp_store = MEMORY");
35
+ db.pragma("mmap_size = 67108864");
36
+ db.pragma("journal_size_limit = 67108864");
31
37
  db.exec(`
32
38
  CREATE TABLE IF NOT EXISTS migrations (
33
39
  name TEXT PRIMARY KEY,
@@ -0,0 +1,43 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { RequestLogInsert, LogWriteContext } from "./logs.js";
3
+ import type { MetricsInsert } from "./metrics.js";
4
+ export interface LogWriteBufferOptions {
5
+ flushIntervalMs?: number;
6
+ maxBufferSize?: number;
7
+ }
8
+ /**
9
+ * 批量缓冲 DB 日志写入,减少 SQLite 事务频率。
10
+ *
11
+ * 透明使用:logs.ts / metrics.ts 内部判断 buffer 是否存在,
12
+ * 存在则 push 到缓冲,否则走原始同步路径。
13
+ *
14
+ * flush 策略:定时(100ms)+ 阈值(50 条)+ stop() 时同步 flush。
15
+ */
16
+ export declare class LogWriteBuffer {
17
+ private readonly db;
18
+ private readonly rawInsertLog;
19
+ private readonly rawInsertMetrics;
20
+ private buffer;
21
+ private flushTimer;
22
+ private readonly flushIntervalMs;
23
+ private readonly maxBufferSize;
24
+ private stopped;
25
+ constructor(db: Database.Database, rawInsertLog: (db: Database.Database, data: RequestLogInsert, ctx?: LogWriteContext) => void, rawInsertMetrics: (db: Database.Database, data: MetricsInsert & {
26
+ id: string;
27
+ }) => void, options?: LogWriteBufferOptions);
28
+ /** 推入日志条目到缓冲,达到阈值时立即 flush */
29
+ pushLog(data: RequestLogInsert, context?: LogWriteContext): void;
30
+ /**
31
+ * 推入 metrics 条目到缓冲。
32
+ * 调用方需预生成 UUID 并在 data.id 中传入。
33
+ */
34
+ pushMetrics(data: MetricsInsert & {
35
+ id: string;
36
+ }): void;
37
+ /** 同步批量写入所有缓冲条目(db.transaction 包裹) */
38
+ flush(): void;
39
+ /** 停止定时器 + 同步 flush 剩余数据。stop 后的 push 直接走同步写入。 */
40
+ stop(): void;
41
+ /** 当前缓冲区中的条目数(测试用) */
42
+ get pendingCount(): number;
43
+ }
@@ -0,0 +1,91 @@
1
+ const DEFAULT_FLUSH_INTERVAL_MS = 100;
2
+ const DEFAULT_MAX_BUFFER_SIZE = 50;
3
+ /**
4
+ * 批量缓冲 DB 日志写入,减少 SQLite 事务频率。
5
+ *
6
+ * 透明使用:logs.ts / metrics.ts 内部判断 buffer 是否存在,
7
+ * 存在则 push 到缓冲,否则走原始同步路径。
8
+ *
9
+ * flush 策略:定时(100ms)+ 阈值(50 条)+ stop() 时同步 flush。
10
+ */
11
+ export class LogWriteBuffer {
12
+ db;
13
+ rawInsertLog;
14
+ rawInsertMetrics;
15
+ buffer = [];
16
+ flushTimer = null;
17
+ flushIntervalMs;
18
+ maxBufferSize;
19
+ stopped = false;
20
+ constructor(db, rawInsertLog, rawInsertMetrics, options) {
21
+ this.db = db;
22
+ this.rawInsertLog = rawInsertLog;
23
+ this.rawInsertMetrics = rawInsertMetrics;
24
+ this.flushIntervalMs = options?.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
25
+ this.maxBufferSize = options?.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;
26
+ this.flushTimer = setInterval(() => {
27
+ this.flush();
28
+ }, this.flushIntervalMs);
29
+ // 不阻止进程退出
30
+ if (this.flushTimer && typeof this.flushTimer === "object" && "unref" in this.flushTimer) {
31
+ this.flushTimer.unref();
32
+ }
33
+ }
34
+ /** 推入日志条目到缓冲,达到阈值时立即 flush */
35
+ pushLog(data, context) {
36
+ if (this.stopped) {
37
+ this.rawInsertLog(this.db, data, context);
38
+ return;
39
+ }
40
+ this.buffer.push({ type: "log", data, context });
41
+ if (this.buffer.length >= this.maxBufferSize) {
42
+ this.flush();
43
+ }
44
+ }
45
+ /**
46
+ * 推入 metrics 条目到缓冲。
47
+ * 调用方需预生成 UUID 并在 data.id 中传入。
48
+ */
49
+ pushMetrics(data) {
50
+ if (this.stopped) {
51
+ this.rawInsertMetrics(this.db, data);
52
+ return;
53
+ }
54
+ this.buffer.push({ type: "metrics", data });
55
+ if (this.buffer.length >= this.maxBufferSize) {
56
+ this.flush();
57
+ }
58
+ }
59
+ /** 同步批量写入所有缓冲条目(db.transaction 包裹) */
60
+ flush() {
61
+ if (this.buffer.length === 0)
62
+ return;
63
+ // 取出当前缓冲,重置数组,避免 flush 期间新 push 的条目丢失
64
+ const entries = this.buffer;
65
+ this.buffer = [];
66
+ const transaction = this.db.transaction(() => {
67
+ for (const entry of entries) {
68
+ if (entry.type === "log") {
69
+ this.rawInsertLog(this.db, entry.data, entry.context);
70
+ }
71
+ else {
72
+ this.rawInsertMetrics(this.db, entry.data);
73
+ }
74
+ }
75
+ });
76
+ transaction();
77
+ }
78
+ /** 停止定时器 + 同步 flush 剩余数据。stop 后的 push 直接走同步写入。 */
79
+ stop() {
80
+ this.stopped = true;
81
+ if (this.flushTimer) {
82
+ clearInterval(this.flushTimer);
83
+ this.flushTimer = null;
84
+ }
85
+ this.flush();
86
+ }
87
+ /** 当前缓冲区中的条目数(测试用) */
88
+ get pendingCount() {
89
+ return this.buffer.length;
90
+ }
91
+ }
package/dist/db/logs.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import Database from "better-sqlite3";
2
2
  import type { LogFileWriter } from "../storage/log-file-writer.js";
3
3
  import { type RetryMatcher } from "../proxy/log-detail-policy.js";
4
+ import type { LogWriteBuffer } from "./log-write-buffer.js";
4
5
  export interface RequestLog {
5
6
  id: string;
6
7
  api_type: string;
@@ -54,6 +55,13 @@ export interface LogWriteContext {
54
55
  logFileWriter?: LogFileWriter | null;
55
56
  responseBody?: string | null;
56
57
  }
58
+ /** 初始化日志缓冲(buildApp 时调用) */
59
+ export declare function initLogBuffer(buffer: LogWriteBuffer): void;
60
+ /** 停止日志缓冲,同步 flush 剩余数据(close 时调用) */
61
+ export declare function stopLogBuffer(): void;
62
+ /** 原始 DB INSERT 逻辑(无缓冲) */
63
+ declare function rawInsertRequestLog(db: Database.Database, log: RequestLogInsert, writeContext?: LogWriteContext): void;
64
+ export { rawInsertRequestLog };
57
65
  export declare function insertRequestLog(db: Database.Database, log: RequestLogInsert, writeContext?: LogWriteContext): void;
58
66
  export declare function getRequestLogs(db: Database.Database, options: {
59
67
  page: number;
@@ -74,7 +82,7 @@ export declare function updateLogStreamContent(db: Database.Database, logId: str
74
82
  /** 当 router 返回给客户端的 status code 与上游不同时,记录实际发送的 status */
75
83
  export declare function updateLogClientStatus(db: Database.Database, logId: string, clientStatusCode: number): void;
76
84
  export declare function deleteLogsBefore(db: Database.Database, beforeDate: string): number;
77
- /** 估算 request_logs 表占用字节数 */
85
+ /** 采样估算 request_logs 表占用字节数(避免全表 SUM 扫描) */
78
86
  export declare function estimateLogTableSize(db: Database.Database): number;
79
87
  /** 删除最旧的日志,保留 keepCount 条,返回实际删除条数。分批删除避免长时间锁表 */
80
88
  export declare function deleteOldestLogs(db: Database.Database, keepCount: number): number;
package/dist/db/logs.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { shouldPreserveDetail } from "../proxy/log-detail-policy.js";
2
+ import { getCachedStmt } from "./helpers.js";
2
3
  // --- request_logs ---
3
4
  const LOG_LIST_SELECT = `rl.id, rl.api_type, rl.model, rl.provider_id, rl.status_code, rl.client_status_code, rl.latency_ms,
4
5
  rl.is_stream, rl.error_message, rl.created_at, rl.is_retry, rl.is_failover, rl.original_request_id, rl.original_model,
@@ -9,8 +10,32 @@ const LOG_LIST_SELECT = `rl.id, rl.api_type, rl.model, rl.provider_id, rl.status
9
10
  rm.input_tokens_estimated, rm.client_type, rm.cache_read_tokens_estimated,
10
11
  COALESCE(p.name, rl.provider_id) AS provider_name`;
11
12
  const LOG_LIST_JOIN = `LEFT JOIN providers p ON p.id = rl.provider_id LEFT JOIN request_metrics rm ON rm.request_log_id = rl.id`;
13
+ /** 模块级缓冲实例,由 initLogBuffer 设置 */
14
+ let logBuffer = null;
15
+ /** 初始化日志缓冲(buildApp 时调用) */
16
+ export function initLogBuffer(buffer) {
17
+ logBuffer = buffer;
18
+ }
19
+ /** 停止日志缓冲,同步 flush 剩余数据(close 时调用) */
20
+ export function stopLogBuffer() {
21
+ if (logBuffer) {
22
+ logBuffer.stop();
23
+ logBuffer = null;
24
+ }
25
+ }
26
+ /** 原始 DB INSERT 逻辑(无缓冲) */
27
+ function rawInsertRequestLog(db, log, writeContext) {
28
+ // 详情保留判定
29
+ const preserveDetail = shouldPreserveDetail(log.status_code, writeContext?.responseBody ?? null, writeContext?.matcher ?? null, !!writeContext?.logFileWriter);
30
+ getCachedStmt(db, `INSERT INTO request_logs (id, api_type, model, provider_id, status_code, client_status_code, latency_ms,
31
+ is_stream, error_message, created_at, client_request, upstream_request, upstream_response,
32
+ is_retry, is_failover, original_request_id, router_key_id, original_model, session_id, pipeline_snapshot)
33
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).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, preserveDetail ? (log.client_request ?? null) : 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);
34
+ }
35
+ // 导出给 LogWriteBuffer 的原始插入函数引用
36
+ export { rawInsertRequestLog };
12
37
  export function insertRequestLog(db, log, writeContext) {
13
- // 文件写入:始终写入全文
38
+ // 文件写入:始终同步调用(WriteStream 内部异步,不阻塞事件循环)
14
39
  if (writeContext?.logFileWriter) {
15
40
  writeContext.logFileWriter.write({
16
41
  id: log.id,
@@ -24,12 +49,13 @@ export function insertRequestLog(db, log, writeContext) {
24
49
  pipeline_snapshot: log.pipeline_snapshot ?? null,
25
50
  });
26
51
  }
27
- // 详情保留判定
28
- const preserveDetail = shouldPreserveDetail(log.status_code, writeContext?.responseBody ?? null, writeContext?.matcher ?? null, !!writeContext?.logFileWriter);
29
- db.prepare(`INSERT INTO request_logs (id, api_type, model, provider_id, status_code, client_status_code, latency_ms,
30
- is_stream, error_message, created_at, client_request, upstream_request, upstream_response,
31
- is_retry, is_failover, original_request_id, router_key_id, original_model, session_id, pipeline_snapshot)
32
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).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, preserveDetail ? (log.client_request ?? null) : 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);
52
+ // DB INSERT:缓冲已初始化则走缓冲,否则走同步
53
+ if (logBuffer) {
54
+ logBuffer.pushLog(log, writeContext);
55
+ }
56
+ else {
57
+ rawInsertRequestLog(db, log, writeContext);
58
+ }
33
59
  }
34
60
  function buildLogWhereClause(options, baseCondition) {
35
61
  let where = baseCondition;
@@ -92,11 +118,11 @@ export function getRequestLogById(db, id) {
92
118
  }
93
119
  /** 流式请求完成后,将 tracker 中累积的文本内容写入 request_logs */
94
120
  export function updateLogStreamContent(db, logId, textContent) {
95
- db.prepare("UPDATE request_logs SET stream_text_content = ? WHERE id = ?").run(textContent, logId);
121
+ getCachedStmt(db, "UPDATE request_logs SET stream_text_content = ? WHERE id = ?").run(textContent, logId);
96
122
  }
97
123
  /** 当 router 返回给客户端的 status code 与上游不同时,记录实际发送的 status */
98
124
  export function updateLogClientStatus(db, logId, clientStatusCode) {
99
- db.prepare("UPDATE request_logs SET client_status_code = ? WHERE id = ?").run(clientStatusCode, logId);
125
+ getCachedStmt(db, "UPDATE request_logs SET client_status_code = ? WHERE id = ?").run(clientStatusCode, logId);
100
126
  }
101
127
  export function deleteLogsBefore(db, beforeDate) {
102
128
  const changes = db.prepare("DELETE FROM request_logs WHERE created_at < ?").run(beforeDate).changes;
@@ -107,17 +133,20 @@ export function deleteLogsBefore(db, beforeDate) {
107
133
  }
108
134
  /** 每行元数据(数字列+索引)的估算字节数 */
109
135
  const ROW_METADATA_BYTES = 500;
110
- /** 估算 request_logs 表占用字节数 */
136
+ /** 采样估算 request_logs 表占用字节数(避免全表 SUM 扫描) */
111
137
  export function estimateLogTableSize(db) {
112
- const row = db.prepare(`
113
- SELECT COALESCE(SUM(
114
- COALESCE(length(client_request), 0) + COALESCE(length(upstream_request), 0) +
115
- COALESCE(length(upstream_response), 0) + COALESCE(length(stream_text_content), 0) +
116
- COALESCE(length(error_message), 0) + COALESCE(length(pipeline_snapshot), 0) + ?
117
- ), 0) as size
118
- FROM request_logs
119
- `).get(ROW_METADATA_BYTES);
120
- return row.size;
138
+ const countRow = db.prepare("SELECT COUNT(*) as cnt FROM request_logs").get();
139
+ if (countRow.cnt === 0)
140
+ return 0;
141
+ // 采样最近 100 行,计算平均行大小
142
+ const samples = db.prepare(`
143
+ SELECT COALESCE(length(client_request), 0) + COALESCE(length(upstream_request), 0) +
144
+ COALESCE(length(upstream_response), 0) + COALESCE(length(stream_text_content), 0) +
145
+ COALESCE(length(error_message), 0) + COALESCE(length(pipeline_snapshot), 0) + ? AS row_size
146
+ FROM request_logs ORDER BY created_at DESC LIMIT 100
147
+ `).all(ROW_METADATA_BYTES);
148
+ const avgRowSize = samples.reduce((s, r) => s + r.row_size, 0) / samples.length;
149
+ return Math.round(avgRowSize * countRow.cnt);
121
150
  }
122
151
  const DELETE_BATCH_SIZE = 1000;
123
152
  /** 删除最旧的日志,保留 keepCount 条,返回实际删除条数。分批删除避免长时间锁表 */
@@ -164,15 +193,28 @@ export function getRequestLogsGrouped(db, options) {
164
193
  const total = db.prepare(`SELECT COUNT(*) as count FROM request_logs rl WHERE ${where}`).get(...params).count;
165
194
  const offset = (options.page - 1) * options.limit;
166
195
  const data = db
167
- .prepare(`SELECT ${LOG_LIST_SELECT},
168
- (SELECT COUNT(*) FROM request_logs c WHERE c.original_request_id = rl.id) AS child_count
169
- FROM request_logs rl
196
+ .prepare(`WITH page_ids AS (
197
+ SELECT rl.id FROM request_logs rl
198
+ ${LOG_LIST_JOIN}
199
+ WHERE ${where}
200
+ ORDER BY rl.created_at DESC LIMIT ? OFFSET ?
201
+ )
202
+ SELECT ${LOG_LIST_SELECT},
203
+ COALESCE(child.cnt, 0) AS child_count
204
+ FROM page_ids pg
205
+ JOIN request_logs rl ON rl.id = pg.id
170
206
  ${LOG_LIST_JOIN}
171
- WHERE ${where} ORDER BY rl.created_at DESC LIMIT ? OFFSET ?`)
207
+ LEFT JOIN (
208
+ SELECT original_request_id, COUNT(*) AS cnt
209
+ FROM request_logs
210
+ WHERE original_request_id IN (SELECT id FROM page_ids)
211
+ GROUP BY original_request_id
212
+ ) child ON child.original_request_id = rl.id
213
+ ORDER BY rl.created_at DESC`)
172
214
  .all(...params, options.limit, offset);
173
215
  return { data, total };
174
216
  }
175
217
  /** 后续 pipeline 阶段完成后,回写 snapshot 到已有日志 */
176
218
  export function updateLogPipelineSnapshot(db, logId, snapshot) {
177
- db.prepare("UPDATE request_logs SET pipeline_snapshot = ? WHERE id = ?").run(snapshot, logId);
219
+ getCachedStmt(db, "UPDATE request_logs SET pipeline_snapshot = ? WHERE id = ?").run(snapshot, logId);
178
220
  }
@@ -1,4 +1,5 @@
1
1
  import Database from "better-sqlite3";
2
+ import type { LogWriteBuffer } from "./log-write-buffer.js";
2
3
  export type MetricsPeriod = "1h" | "5h" | "6h" | "24h" | "7d" | "30d";
3
4
  export type MetricsMetric = "ttft" | "tps" | "text_tps" | "thinking_tps" | "tool_use_tps" | "non_thinking_tps" | "total_tps" | "tokens" | "cache_rate" | "request_count" | "input_tokens" | "output_tokens" | "cache_hit_tokens";
4
5
  export interface MetricsRow {
@@ -48,6 +49,15 @@ export type MetricsInsert = {
48
49
  non_thinking_tps?: number | null;
49
50
  total_tps?: number | null;
50
51
  };
52
+ /** 设置缓冲实例(由 index.ts buildApp 调用,传入与 logs.ts 共享的 buffer) */
53
+ export declare function setLogBuffer(buffer: LogWriteBuffer): void;
54
+ /** 清除缓冲引用(由 stopLogBuffer 调用) */
55
+ export declare function clearLogBuffer(): void;
56
+ /** 原始 DB INSERT 逻辑(无缓冲) */
57
+ declare function rawInsertMetrics(db: Database.Database, m: MetricsInsert & {
58
+ id: string;
59
+ }): void;
60
+ export { rawInsertMetrics };
51
61
  export declare function insertMetrics(db: Database.Database, m: MetricsInsert): string;
52
62
  export interface MetricsSummaryRow {
53
63
  provider_id: string;
@@ -1,13 +1,35 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import { MS_PER_SECOND } from "../core/constants.js";
3
- export function insertMetrics(db, m) {
4
- const id = randomUUID();
5
- db.prepare(`INSERT INTO request_metrics (id, request_log_id, provider_id, backend_model, api_type, router_key_id, status_code,
3
+ import { getCachedStmt } from "./helpers.js";
4
+ /** 模块级缓冲实例,与 logs.ts 共享同一个 LogWriteBuffer */
5
+ let logBuffer = null;
6
+ /** 设置缓冲实例(由 index.ts buildApp 调用,传入与 logs.ts 共享的 buffer) */
7
+ export function setLogBuffer(buffer) {
8
+ logBuffer = buffer;
9
+ }
10
+ /** 清除缓冲引用(由 stopLogBuffer 调用) */
11
+ export function clearLogBuffer() {
12
+ logBuffer = null;
13
+ }
14
+ /** 原始 DB INSERT 逻辑(无缓冲) */
15
+ function rawInsertMetrics(db, m) {
16
+ getCachedStmt(db, `INSERT INTO request_metrics (id, request_log_id, provider_id, backend_model, api_type, router_key_id, status_code,
6
17
  input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, ttft_ms, total_duration_ms, tokens_per_second, stop_reason, is_complete, input_tokens_estimated,
7
18
  client_type, cache_read_tokens_estimated,
8
19
  thinking_tokens, text_tokens, tool_use_tokens, thinking_duration_ms,
9
20
  thinking_tps, total_tps, non_thinking_duration_ms, non_thinking_tps)
10
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, m.request_log_id, m.provider_id, m.backend_model, m.api_type, m.router_key_id ?? null, m.status_code ?? null, 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, m.input_tokens_estimated ?? 0, m.client_type ?? 'unknown', m.cache_read_tokens_estimated ?? 0, m.thinking_tokens ?? null, m.text_tokens ?? null, m.tool_use_tokens ?? null, m.thinking_duration_ms ?? null, m.thinking_tps ?? null, m.total_tps ?? null, m.non_thinking_duration_ms ?? null, m.non_thinking_tps ?? null);
21
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(m.id, m.request_log_id, m.provider_id, m.backend_model, m.api_type, m.router_key_id ?? null, m.status_code ?? null, 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, m.input_tokens_estimated ?? 0, m.client_type ?? 'unknown', m.cache_read_tokens_estimated ?? 0, m.thinking_tokens ?? null, m.text_tokens ?? null, m.tool_use_tokens ?? null, m.thinking_duration_ms ?? null, m.thinking_tps ?? null, m.total_tps ?? null, m.non_thinking_duration_ms ?? null, m.non_thinking_tps ?? null);
22
+ }
23
+ // 导出给 LogWriteBuffer 的原始插入函数引用
24
+ export { rawInsertMetrics };
25
+ export function insertMetrics(db, m) {
26
+ const id = randomUUID();
27
+ if (logBuffer) {
28
+ logBuffer.pushMetrics({ ...m, id });
29
+ }
30
+ else {
31
+ rawInsertMetrics(db, { ...m, id });
32
+ }
11
33
  return id;
12
34
  }
13
35
  const PERIOD_OFFSET = {
@@ -0,0 +1,8 @@
1
+ -- 覆盖 provider_id 过滤 + 时间范围分页
2
+ CREATE INDEX IF NOT EXISTS idx_request_logs_provider_id ON request_logs(provider_id);
3
+ CREATE INDEX IF NOT EXISTS idx_request_logs_created_at_provider ON request_logs(created_at DESC, provider_id);
4
+ CREATE INDEX IF NOT EXISTS idx_request_logs_created_at_router_key ON request_logs(created_at DESC, router_key_id);
5
+
6
+ -- 覆盖按密钥过滤的聚合查询
7
+ CREATE INDEX IF NOT EXISTS idx_metrics_router_key ON request_metrics(router_key_id);
8
+ CREATE INDEX IF NOT EXISTS idx_metrics_created_at_router_key ON request_metrics(created_at, router_key_id);
@@ -0,0 +1,8 @@
1
+ -- request_metrics 核心聚合索引(Dashboard getMetricsSummary + getMetricsTimeseries)
2
+ CREATE INDEX IF NOT EXISTS idx_metrics_agg
3
+ ON request_metrics(is_complete, created_at DESC, provider_id, backend_model);
4
+
5
+ -- request_logs 子请求+时间排序复合索引
6
+ -- 注:018 已有 idx_request_logs_original_request_id(original_request_id),本索引额外覆盖时间排序
7
+ CREATE INDEX IF NOT EXISTS idx_logs_original_time
8
+ ON request_logs(original_request_id, created_at DESC);
@@ -1,9 +1,26 @@
1
+ import { getCachedStmt } from "./helpers.js";
2
+ // TTL 缓存:WeakMap 按 db 实例隔离,确保测试中 :memory: db 互不干扰
3
+ const settingsCache = new WeakMap();
4
+ const CACHE_TTL_MS = 30_000;
1
5
  export function getSetting(db, key) {
2
- const row = db.prepare("SELECT value FROM settings WHERE key = ?").get(key);
3
- return row?.value ?? null;
6
+ let cache = settingsCache.get(db);
7
+ if (!cache) {
8
+ cache = new Map();
9
+ settingsCache.set(db, cache);
10
+ }
11
+ const cached = cache.get(key);
12
+ if (cached && Date.now() < cached.expiresAt)
13
+ return cached.value;
14
+ const row = getCachedStmt(db, "SELECT value FROM settings WHERE key = ?").get(key);
15
+ const value = row?.value ?? null;
16
+ cache.set(key, { value, expiresAt: Date.now() + CACHE_TTL_MS });
17
+ return value;
4
18
  }
5
19
  export function setSetting(db, key, value) {
6
- db.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)").run(key, value);
20
+ getCachedStmt(db, "INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)").run(key, value);
21
+ const cache = settingsCache.get(db);
22
+ if (cache)
23
+ cache.delete(key);
7
24
  }
8
25
  export function isInitialized(db) {
9
26
  return getSetting(db, "initialized") === "true";
@@ -40,7 +57,7 @@ export function setConfigSyncSource(db, source) {
40
57
  setSetting(db, "config_sync_source", source);
41
58
  }
42
59
  export function getDetailLogEnabled(db) {
43
- const row = db.prepare("SELECT value FROM settings WHERE key = ?").get("detail_log_enabled");
60
+ const row = getCachedStmt(db, "SELECT value FROM settings WHERE key = ?").get("detail_log_enabled");
44
61
  return row ? row.value !== "0" : true;
45
62
  }
46
63
  export function getTokenEstimationEnabled(db) {
@@ -52,7 +69,7 @@ export function setTokenEstimationEnabled(db, enabled) {
52
69
  }
53
70
  const DEFAULT_LOG_FILE_RETENTION_DAYS = 3;
54
71
  export function getLogFileRetentionDays(db) {
55
- const row = db.prepare("SELECT value FROM settings WHERE key = ?").get("log_file_retention_days");
72
+ const row = getCachedStmt(db, "SELECT value FROM settings WHERE key = ?").get("log_file_retention_days");
56
73
  return row ? parseInt(row.value, 10) : DEFAULT_LOG_FILE_RETENTION_DAYS;
57
74
  }
58
75
  const DEFAULT_CLIENT_SESSION_HEADERS = [