llm-simple-router 0.6.7 → 0.7.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 (182) hide show
  1. package/README.md +69 -0
  2. package/dist/admin/constants.d.ts +1 -1
  3. package/dist/admin/constants.js +2 -2
  4. package/dist/admin/logs.d.ts +2 -0
  5. package/dist/admin/logs.js +17 -1
  6. package/dist/admin/providers.d.ts +2 -2
  7. package/dist/admin/providers.js +29 -16
  8. package/dist/admin/proxy-enhancement.d.ts +2 -0
  9. package/dist/admin/proxy-enhancement.js +4 -10
  10. package/dist/admin/retry-rules.d.ts +2 -2
  11. package/dist/admin/retry-rules.js +4 -8
  12. package/dist/admin/routes.d.ts +5 -4
  13. package/dist/admin/routes.js +7 -7
  14. package/dist/admin/settings-import-export.d.ts +2 -4
  15. package/dist/admin/settings-import-export.js +9 -19
  16. package/dist/admin/settings.d.ts +1 -0
  17. package/dist/admin/settings.js +29 -1
  18. package/dist/admin/upgrade.d.ts +1 -0
  19. package/dist/admin/upgrade.js +37 -4
  20. package/dist/{constants.d.ts → core/constants.d.ts} +4 -1
  21. package/dist/{constants.js → core/constants.js} +21 -1
  22. package/dist/core/container.d.ts +31 -0
  23. package/dist/core/container.js +41 -0
  24. package/dist/core/errors.d.ts +26 -0
  25. package/dist/core/errors.js +42 -0
  26. package/dist/core/registry.d.ts +43 -0
  27. package/dist/core/registry.js +3 -0
  28. package/dist/core/types.d.ts +105 -0
  29. package/dist/core/types.js +3 -0
  30. package/dist/db/index.d.ts +1 -1
  31. package/dist/db/index.js +1 -1
  32. package/dist/db/logs.d.ts +11 -24
  33. package/dist/db/logs.js +37 -38
  34. package/dist/db/metrics.js +1 -1
  35. package/dist/db/migrations/033_add_pipeline_snapshot.sql +1 -0
  36. package/dist/db/migrations/034_drop_redundant_log_columns.sql +13 -0
  37. package/dist/db/settings.d.ts +2 -0
  38. package/dist/db/settings.js +9 -0
  39. package/dist/index.d.ts +10 -2
  40. package/dist/index.js +196 -108
  41. package/dist/metrics/metrics-extractor.d.ts +1 -24
  42. package/dist/metrics/metrics-extractor.js +1 -1
  43. package/dist/metrics/sse-metrics-transform.d.ts +1 -1
  44. package/dist/middleware/admin-auth.js +4 -0
  45. package/dist/middleware/auth.js +1 -2
  46. package/dist/monitor/request-tracker.d.ts +3 -4
  47. package/dist/monitor/request-tracker.js +6 -16
  48. package/dist/monitor/runtime-collector.js +1 -1
  49. package/dist/monitor/types.d.ts +8 -0
  50. package/dist/proxy/adaptive-controller.d.ts +4 -1
  51. package/dist/proxy/adaptive-controller.js +5 -0
  52. package/dist/proxy/enhancement/enhancement-handler.d.ts +19 -3
  53. package/dist/proxy/enhancement/enhancement-handler.js +80 -28
  54. package/dist/proxy/enhancement/index.d.ts +1 -0
  55. package/dist/proxy/handler/anthropic.d.ts +7 -0
  56. package/dist/proxy/{anthropic.js → handler/anthropic.js} +8 -7
  57. package/dist/proxy/handler/openai.d.ts +7 -0
  58. package/dist/proxy/{openai.js → handler/openai.js} +10 -9
  59. package/dist/proxy/handler/proxy-handler-utils.d.ts +9 -0
  60. package/dist/proxy/handler/proxy-handler-utils.js +63 -0
  61. package/dist/proxy/handler/proxy-handler.d.ts +13 -0
  62. package/dist/proxy/{proxy-handler.js → handler/proxy-handler.js} +104 -120
  63. package/dist/proxy/log-detail-policy.d.ts +12 -0
  64. package/dist/proxy/log-detail-policy.js +21 -0
  65. package/dist/proxy/log-helpers.d.ts +8 -0
  66. package/dist/proxy/log-helpers.js +16 -4
  67. package/dist/proxy/loop-prevention/tool-loop-guard.d.ts +1 -1
  68. package/dist/proxy/loop-prevention/tool-loop-guard.js +9 -12
  69. package/dist/proxy/{orchestrator.d.ts → orchestration/orchestrator.d.ts} +6 -4
  70. package/dist/proxy/{orchestrator.js → orchestration/orchestrator.js} +2 -1
  71. package/dist/proxy/{resilience.d.ts → orchestration/resilience.d.ts} +2 -14
  72. package/dist/proxy/{resilience.js → orchestration/resilience.js} +2 -2
  73. package/dist/proxy/{retry-rules.d.ts → orchestration/retry-rules.d.ts} +1 -1
  74. package/dist/proxy/{retry-rules.js → orchestration/retry-rules.js} +1 -1
  75. package/dist/proxy/{scope.d.ts → orchestration/scope.d.ts} +3 -3
  76. package/dist/proxy/{semaphore.d.ts → orchestration/semaphore.d.ts} +7 -15
  77. package/dist/proxy/{semaphore.js → orchestration/semaphore.js} +12 -26
  78. package/dist/proxy/patch/index.d.ts +8 -2
  79. package/dist/proxy/patch/index.js +5 -2
  80. package/dist/proxy/pipeline-snapshot.d.ts +37 -0
  81. package/dist/proxy/pipeline-snapshot.js +15 -0
  82. package/dist/proxy/proxy-core.d.ts +1 -1
  83. package/dist/proxy/proxy-core.js +1 -1
  84. package/dist/proxy/proxy-logging.d.ts +10 -2
  85. package/dist/proxy/proxy-logging.js +23 -9
  86. package/dist/proxy/response-transform.d.ts +7 -0
  87. package/dist/proxy/response-transform.js +15 -0
  88. package/dist/proxy/{enhancement-config.js → routing/enhancement-config.js} +1 -1
  89. package/dist/proxy/{mapping-resolver.d.ts → routing/mapping-resolver.d.ts} +1 -1
  90. package/dist/proxy/{mapping-resolver.js → routing/mapping-resolver.js} +1 -1
  91. package/dist/proxy/{model-state.js → routing/model-state.js} +1 -1
  92. package/dist/proxy/{overflow.d.ts → routing/overflow.d.ts} +1 -1
  93. package/dist/proxy/{overflow.js → routing/overflow.js} +3 -3
  94. package/dist/proxy/{usage-window-tracker.js → routing/usage-window-tracker.js} +3 -3
  95. package/dist/proxy/{transport.d.ts → transport/http.d.ts} +2 -2
  96. package/dist/proxy/{transport.js → transport/http.js} +3 -3
  97. package/dist/proxy/{stream-proxy.d.ts → transport/stream.d.ts} +4 -4
  98. package/dist/proxy/{stream-proxy.js → transport/stream.js} +25 -7
  99. package/dist/proxy/{transport-fn.d.ts → transport/transport-fn.d.ts} +5 -5
  100. package/dist/proxy/{transport-fn.js → transport/transport-fn.js} +11 -9
  101. package/dist/proxy/types.d.ts +3 -64
  102. package/dist/proxy/types.js +5 -34
  103. package/dist/storage/log-file-compressor.d.ts +15 -0
  104. package/dist/storage/log-file-compressor.js +83 -0
  105. package/dist/storage/log-file-writer.d.ts +21 -0
  106. package/dist/storage/log-file-writer.js +103 -0
  107. package/dist/storage/types.d.ts +16 -0
  108. package/dist/storage/types.js +5 -0
  109. package/dist/upgrade/deployment.d.ts +13 -0
  110. package/dist/upgrade/deployment.js +40 -0
  111. package/frontend-dist/assets/{CardContent-jQcfCC7J.js → CardContent-CxOF1feY.js} +1 -1
  112. package/frontend-dist/assets/{CardTitle-BrCTvULL.js → CardTitle-BSEFcEOM.js} +1 -1
  113. package/frontend-dist/assets/{CascadingModelSelect-BFh67j5d.js → CascadingModelSelect-DTwksDPZ.js} +1 -1
  114. package/frontend-dist/assets/{Checkbox-Bbt7JpdE.js → Checkbox-RfsERG07.js} +1 -1
  115. package/frontend-dist/assets/{CollapsibleTrigger-DMnEA0qC.js → CollapsibleTrigger-Dsjo7QlC.js} +1 -1
  116. package/frontend-dist/assets/{Collection-CVk3TPHc.js → Collection-rQ4eIYfa.js} +1 -1
  117. package/frontend-dist/assets/{Dashboard-Coftbg4B.js → Dashboard-YejfAPiB.js} +1 -1
  118. package/frontend-dist/assets/{DialogTitle-BbOAZzPQ.js → DialogTitle-DeFTnmgC.js} +1 -1
  119. package/frontend-dist/assets/{Input-DdHY9q0w.js → Input-CENz_g9t.js} +1 -1
  120. package/frontend-dist/assets/{Label-DRQv_Dr_.js → Label-BAciBrrd.js} +1 -1
  121. package/frontend-dist/assets/{Login-SV3ctFnJ.js → Login-DQkYFq7R.js} +1 -1
  122. package/frontend-dist/assets/{Logs-BG45kX6E.js → Logs-Dol8AX7z.js} +1 -1
  123. package/frontend-dist/assets/{ModelMappings-DEaBnRU3.js → ModelMappings-VEYW1TrW.js} +1 -1
  124. package/frontend-dist/assets/{Monitor-ZHOt11n-.js → Monitor-C0r9WefB.js} +1 -1
  125. package/frontend-dist/assets/{PopoverTrigger-z-Z3EjBk.js → PopoverTrigger-Cyqik5SE.js} +1 -1
  126. package/frontend-dist/assets/{PopperContent-DPC-6a3n.js → PopperContent-B7IuAHeq.js} +1 -1
  127. package/frontend-dist/assets/{Providers-DpY6pAcg.js → Providers-D8Z97edN.js} +1 -1
  128. package/frontend-dist/assets/{ProxyEnhancement-D6KBDXMp.js → ProxyEnhancement-Kn8r2SN6.js} +1 -1
  129. package/frontend-dist/assets/{RetryRules-DWI7_WLZ.js → RetryRules-F0295m4_.js} +1 -1
  130. package/frontend-dist/assets/{RouterKeys-CZ1657eX.js → RouterKeys-CFbPtUE_.js} +1 -1
  131. package/frontend-dist/assets/{RovingFocusItem-BREE2YEV.js → RovingFocusItem-D291Vjh8.js} +1 -1
  132. package/frontend-dist/assets/{Schedules-BVPsBRPi.js → Schedules-DWhF3uod.js} +1 -1
  133. package/frontend-dist/assets/{SelectValue-H8hwQwbk.js → SelectValue-BWlgUZa3.js} +1 -1
  134. package/frontend-dist/assets/Settings-BnIzEF_k.js +6 -0
  135. package/frontend-dist/assets/{Setup-yOYNKkOG.js → Setup-BglKyQKq.js} +1 -1
  136. package/frontend-dist/assets/{Switch-CojD3rTH.js → Switch-DyCR-CPu.js} +1 -1
  137. package/frontend-dist/assets/{TableHeader-awoHTsWN.js → TableHeader-DVUlBL35.js} +1 -1
  138. package/frontend-dist/assets/{TabsTrigger-DTKSFj85.js → TabsTrigger-BU1DY-C8.js} +1 -1
  139. package/frontend-dist/assets/{Teleport-DehYAXud.js → Teleport-BQgusr9g.js} +1 -1
  140. package/frontend-dist/assets/{TooltipTrigger-C2dl_dml.js → TooltipTrigger-Bv_QoBns.js} +1 -1
  141. package/frontend-dist/assets/{UnifiedRequestDialog-C8A-uSTR.js → UnifiedRequestDialog-f_evI835.js} +2 -2
  142. package/frontend-dist/assets/{VisuallyHidden-C8oaGi2S.js → VisuallyHidden-Con10z4F.js} +1 -1
  143. package/frontend-dist/assets/{VisuallyHiddenInput-BMc813t2.js → VisuallyHiddenInput-yrDtxucb.js} +1 -1
  144. package/frontend-dist/assets/{alert-dialog-C8TZQmU6.js → alert-dialog-2Db6Z7JQ.js} +1 -1
  145. package/frontend-dist/assets/arrow-down-WyouvE7T.js +1 -0
  146. package/frontend-dist/assets/{badge-BVh2WpA5.js → badge-DEhZfeI0.js} +1 -1
  147. package/frontend-dist/assets/button-Cnkbp_6J.js +12 -0
  148. package/frontend-dist/assets/check-BuqB5Nyb.js +1 -0
  149. package/frontend-dist/assets/{copy-DTOecxa9.js → copy-CwqZSuIG.js} +1 -1
  150. package/frontend-dist/assets/{dialog-kA7AUNoc.js → dialog-CVMKSdPr.js} +1 -1
  151. package/frontend-dist/assets/{file-text-DzZCFO7y.js → file-text-D0K8Hovo.js} +1 -1
  152. package/frontend-dist/assets/index-Ct718O93.js +1 -0
  153. package/frontend-dist/assets/{lib-ClDokUbt.js → lib-H3YI7EK4.js} +1 -1
  154. package/frontend-dist/assets/loader-circle-Be82FnVY.js +1 -0
  155. package/frontend-dist/assets/{useClipboard-DU1ne-Jw.js → useClipboard-Cd7k-5Yq.js} +1 -1
  156. package/frontend-dist/assets/{useFocusGuards-Btmdbg_F.js → useFocusGuards-luoLXnwV.js} +1 -1
  157. package/frontend-dist/assets/useFormControl-Da4ViGZF.js +1 -0
  158. package/frontend-dist/assets/{useLogRetention--EGNWXig.js → useLogRetention-DB4Iu6o_.js} +1 -1
  159. package/frontend-dist/assets/useNonce-DvAdQ48J.js +1 -0
  160. package/frontend-dist/assets/x-DB22csQl.js +1 -0
  161. package/frontend-dist/index.html +19 -19
  162. package/package.json +1 -1
  163. package/dist/proxy/anthropic.d.ts +0 -19
  164. package/dist/proxy/openai.d.ts +0 -19
  165. package/dist/proxy/proxy-handler.d.ts +0 -19
  166. package/dist/proxy/strategy/types.d.ts +0 -21
  167. package/dist/proxy/strategy/types.js +0 -1
  168. package/frontend-dist/assets/Settings-DHYaYRgU.js +0 -6
  169. package/frontend-dist/assets/arrow-down-D-cQXxau.js +0 -1
  170. package/frontend-dist/assets/button-N59D1BGa.js +0 -12
  171. package/frontend-dist/assets/check-dDgrw3T3.js +0 -1
  172. package/frontend-dist/assets/index-DVTeNVaa.js +0 -1
  173. package/frontend-dist/assets/loader-circle-DVHRL-38.js +0 -1
  174. package/frontend-dist/assets/useFormControl-C5Kjziuj.js +0 -1
  175. package/frontend-dist/assets/useNonce-Cp31yRzV.js +0 -1
  176. package/frontend-dist/assets/x-DMktsI_w.js +0 -1
  177. /package/dist/{config.d.ts → config/index.d.ts} +0 -0
  178. /package/dist/{config.js → config/index.js} +0 -0
  179. /package/dist/proxy/{scope.js → orchestration/scope.js} +0 -0
  180. /package/dist/proxy/{enhancement-config.d.ts → routing/enhancement-config.d.ts} +0 -0
  181. /package/dist/proxy/{model-state.d.ts → routing/model-state.d.ts} +0 -0
  182. /package/dist/proxy/{usage-window-tracker.d.ts → routing/usage-window-tracker.d.ts} +0 -0
package/dist/index.js CHANGED
@@ -5,42 +5,74 @@ 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
- import { HTTP_NOT_FOUND, HTTP_INTERNAL_ERROR, getProxyApiType } from "./constants.js";
8
+ import { HTTP_NOT_FOUND, HTTP_INTERNAL_ERROR, getProxyApiType } from "./core/constants.js";
9
9
  import { API_CODE, apiError, isAdminApiResponse, statusToApiCode } from "./admin/api-response.js";
10
10
  const PROVIDER_DEFAULT_QUEUE_TIMEOUT_MS = 5000;
11
11
  const PROVIDER_DEFAULT_MAX_QUEUE_SIZE = 100;
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = path.dirname(__filename);
14
- import { getConfig } from "./config.js";
15
- import { initDatabase, getAllProviders, backfillMetricsFromRequestMetrics } from "./db/index.js";
14
+ import { getConfig, getBaseConfig } from "./config/index.js";
15
+ import { initDatabase, getAllProviders } from "./db/index.js";
16
16
  import { loadRecommendedConfig } from "./config/recommended.js";
17
17
  import { authMiddleware } from "./middleware/auth.js";
18
- import { openaiProxy } from "./proxy/openai.js";
19
- import { anthropicProxy } from "./proxy/anthropic.js";
18
+ import { openaiProxy } from "./proxy/handler/openai.js";
19
+ import { anthropicProxy } from "./proxy/handler/anthropic.js";
20
20
  import { adminRoutes } from "./admin/routes.js";
21
- import { RetryRuleMatcher } from "./proxy/retry-rules.js";
22
- import { ProviderSemaphoreManager } from "./proxy/semaphore.js";
21
+ import { RetryRuleMatcher } from "./proxy/orchestration/retry-rules.js";
22
+ import { ProviderSemaphoreManager } from "./proxy/orchestration/semaphore.js";
23
23
  import { AdaptiveConcurrencyController } from "./proxy/adaptive-controller.js";
24
+ import { loadEnhancementConfig } from "./proxy/routing/enhancement-config.js";
24
25
  import { RequestTracker } from "./monitor/request-tracker.js";
25
- import { modelState } from "./proxy/model-state.js";
26
- import { UsageWindowTracker } from "./proxy/usage-window-tracker.js";
26
+ import { modelState } from "./proxy/routing/model-state.js";
27
+ import { UsageWindowTracker } from "./proxy/routing/usage-window-tracker.js";
27
28
  import { SessionTracker } from "./proxy/loop-prevention/session-tracker.js";
28
29
  import { DEFAULT_LOOP_PREVENTION_CONFIG } from "./proxy/loop-prevention/types.js";
29
30
  import { scheduleLogCleanup } from "./db/log-cleaner.js";
30
31
  import { scheduleDbSizeMonitor } from "./db/db-size-monitor.js";
31
32
  import { startUpgradeChecker, stopUpgradeChecker } from "./admin/upgrade.js";
32
33
  import fastifyStatic from "@fastify/static";
34
+ import { ServiceContainer, SERVICE_KEYS } from "./core/container.js";
35
+ import { LogFileWriter } from "./storage/log-file-writer.js";
36
+ import { scheduleLogFileMaintenance } from "./storage/log-file-compressor.js";
37
+ import { getDetailLogEnabled, getLogFileRetentionDays } from "./db/settings.js";
38
+ import { dirname, join } from "node:path";
39
+ /**
40
+ * 共享初始化逻辑 — 启动时和导入配置后都需要调用。
41
+ * 从 DB 读取所有 provider,初始化信号量/自适应并发/tracker 缓存。
42
+ */
43
+ export function initializeProviderState(db, semaphoreManager, adaptiveController, tracker) {
44
+ const allProviders = getAllProviders(db);
45
+ for (const p of allProviders) {
46
+ if (p.adaptive_enabled) {
47
+ adaptiveController.init(p.id, { max: p.max_concurrency }, {
48
+ queueTimeoutMs: p.queue_timeout_ms,
49
+ maxQueueSize: p.max_queue_size,
50
+ });
51
+ }
52
+ else if (p.max_concurrency > 0) {
53
+ semaphoreManager.updateConfig(p.id, {
54
+ maxConcurrency: p.max_concurrency,
55
+ queueTimeoutMs: p.queue_timeout_ms,
56
+ maxQueueSize: p.max_queue_size,
57
+ });
58
+ }
59
+ tracker.updateProviderConfig(p.id, {
60
+ name: p.name,
61
+ maxConcurrency: p.max_concurrency ?? 0,
62
+ queueTimeoutMs: p.queue_timeout_ms ?? PROVIDER_DEFAULT_QUEUE_TIMEOUT_MS,
63
+ maxQueueSize: p.max_queue_size ?? PROVIDER_DEFAULT_MAX_QUEUE_SIZE,
64
+ });
65
+ }
66
+ }
33
67
  export async function buildApp(options) {
34
68
  const config = options?.config ?? getBaseConfig();
35
69
  // 允许外部传入已初始化的 DB(测试用),否则自行创建
36
70
  let db;
37
- let shouldBackfill = false;
38
71
  if (options?.db) {
39
72
  db = options.db;
40
73
  }
41
74
  else {
42
75
  db = initDatabase(config.DB_PATH);
43
- shouldBackfill = true;
44
76
  }
45
77
  const isDev = process.env.NODE_ENV !== "production";
46
78
  const MAX_BODY_SIZE_MB = 50;
@@ -96,19 +128,24 @@ export async function buildApp(options) {
96
128
  const body = request.body;
97
129
  const receivedAt = request.receivedAt;
98
130
  const latencyMs = receivedAt ? Date.now() - receivedAt : 0;
99
- insertRequestLog(db, {
100
- id: randomUUID(),
101
- api_type: proxyApiType,
102
- model: body?.model || null,
103
- provider_id: null,
104
- status_code: status,
105
- latency_ms: latencyMs,
106
- is_stream: body?.stream === true ? 1 : 0,
107
- error_message: fastifyError.message,
108
- created_at: new Date().toISOString(),
109
- client_request: JSON.stringify({ headers: request.headers, ...(body ? { body } : {}) }),
110
- router_key_id: request.routerKey?.id ?? null,
111
- });
131
+ try {
132
+ insertRequestLog(db, {
133
+ id: randomUUID(),
134
+ api_type: proxyApiType,
135
+ model: body?.model || null,
136
+ provider_id: null,
137
+ status_code: status,
138
+ latency_ms: latencyMs,
139
+ is_stream: body?.stream === true ? 1 : 0,
140
+ error_message: fastifyError.message,
141
+ created_at: new Date().toISOString(),
142
+ client_request: JSON.stringify({ headers: request.headers, ...(body ? { body } : {}) }),
143
+ router_key_id: request.routerKey?.id ?? null,
144
+ });
145
+ }
146
+ catch (logErr) {
147
+ request.log.error({ err: logErr }, "Failed to log proxy error to request_logs");
148
+ }
112
149
  }
113
150
  return reply.code(status).send({ error: { message: fastifyError.message } });
114
151
  }
@@ -125,91 +162,89 @@ export async function buildApp(options) {
125
162
  if (typeof payload === 'string') {
126
163
  try {
127
164
  const parsed = JSON.parse(payload);
128
- if ('code' in parsed)
165
+ if (parsed !== null && typeof parsed === 'object' && 'code' in parsed)
129
166
  return payload; // errorHandler 或路由已手动包装
167
+ // 复用已解析结果,避免二次 JSON.parse
168
+ const wrapped = {
169
+ code: API_CODE.SUCCESS,
170
+ message: 'ok',
171
+ data: parsed,
172
+ };
173
+ return JSON.stringify(wrapped);
130
174
  }
131
175
  catch {
132
176
  return payload;
133
177
  }
134
178
  }
135
- // 包装成功响应
136
- const wrapped = {
137
- code: API_CODE.SUCCESS,
138
- message: 'ok',
139
- data: typeof payload === 'string' ? JSON.parse(payload) : payload,
140
- };
141
- return JSON.stringify(wrapped);
179
+ return payload;
142
180
  });
143
181
  loadRecommendedConfig();
144
182
  startUpgradeChecker(options?.upgradeCheckerOptions);
145
- // 启动时回填:补齐回退老版本期间缺失的 metrics 冗余列
146
- if (shouldBackfill) {
147
- const backfilled = backfillMetricsFromRequestMetrics(db);
148
- if (backfilled > 0) {
149
- app.log.info({ backfilled }, "Backfilled metrics from request_metrics");
150
- }
151
- }
183
+ const container = new ServiceContainer();
184
+ container.register(SERVICE_KEYS.db, () => db);
185
+ container.register(SERVICE_KEYS.matcher, (c) => { const m = new RetryRuleMatcher(); m.load(c.resolve(SERVICE_KEYS.db)); return m; });
186
+ container.register(SERVICE_KEYS.semaphoreManager, () => new ProviderSemaphoreManager());
187
+ container.register(SERVICE_KEYS.tracker, (c) => {
188
+ const t = new RequestTracker({ semaphoreManager: c.resolve(SERVICE_KEYS.semaphoreManager), logger: app.log });
189
+ t.startPushInterval();
190
+ return t;
191
+ });
192
+ container.register(SERVICE_KEYS.usageWindowTracker, (c) => {
193
+ const uwt = new UsageWindowTracker(c.resolve(SERVICE_KEYS.db));
194
+ uwt.reconcileOnStartup();
195
+ return uwt;
196
+ });
197
+ container.register(SERVICE_KEYS.sessionTracker, () => new SessionTracker(DEFAULT_LOOP_PREVENTION_CONFIG.sessionTracker));
198
+ // 文件日志写入器
199
+ const isMemoryDb = config.DB_PATH === ":memory:";
200
+ const logsDir = isMemoryDb ? "" : join(dirname(config.DB_PATH), "logs");
201
+ // :memory: 模式注册 null,避免 DB 日志记录被 isFileWriter 逻辑抑制
202
+ const logFileWriter = isMemoryDb
203
+ ? null
204
+ : new LogFileWriter(logsDir, { enabled: getDetailLogEnabled(db) });
205
+ container.register(SERVICE_KEYS.logFileWriter, () => logFileWriter);
152
206
  // 注入 DB 到 modelState 单例,启用会话级持久化
153
207
  modelState.init(db);
154
- const matcher = new RetryRuleMatcher();
155
- matcher.load(db);
156
- const semaphoreManager = new ProviderSemaphoreManager();
157
- const tracker = new RequestTracker({ semaphoreManager, logger: app.log });
158
- tracker.startPushInterval();
159
- const adaptiveController = new AdaptiveConcurrencyController(semaphoreManager, app.log);
208
+ // 注册 AdaptiveConcurrencyController(依赖已注册的 semaphoreManager)
209
+ container.register(SERVICE_KEYS.adaptiveController, (c) => {
210
+ const ac = new AdaptiveConcurrencyController(c.resolve(SERVICE_KEYS.semaphoreManager), app.log);
211
+ return ac;
212
+ });
213
+ // 从容器解析所有服务
214
+ const matcher = container.resolve(SERVICE_KEYS.matcher);
215
+ const semaphoreManager = container.resolve(SERVICE_KEYS.semaphoreManager);
216
+ const tracker = container.resolve(SERVICE_KEYS.tracker);
217
+ const usageWindowTracker = container.resolve(SERVICE_KEYS.usageWindowTracker);
218
+ const adaptiveController = container.resolve(SERVICE_KEYS.adaptiveController);
219
+ // Wire adaptive controller to tracker
160
220
  tracker.setAdaptiveController(adaptiveController);
161
- // 5h 用量窗口追踪器,启动时自动补齐缺失窗口
162
- const usageWindowTracker = new UsageWindowTracker(db);
163
- usageWindowTracker.reconcileOnStartup();
164
- // Session tracker(工具调用循环检测用),始终创建但检测受 proxy_enhancement 配置控制
165
- const sessionTracker = new SessionTracker(DEFAULT_LOOP_PREVENTION_CONFIG.sessionTracker);
166
- // 从 DB 读取已有 provider 的并发配置,初始化信号量管理器和 tracker
167
- const allProviders = getAllProviders(db);
168
- for (const p of allProviders) {
169
- if (p.adaptive_enabled) {
170
- adaptiveController.init(p.id, { max: p.max_concurrency }, {
171
- queueTimeoutMs: p.queue_timeout_ms,
172
- maxQueueSize: p.max_queue_size,
173
- });
174
- }
175
- else if (p.max_concurrency > 0) {
176
- semaphoreManager.updateConfig(p.id, {
177
- maxConcurrency: p.max_concurrency,
178
- queueTimeoutMs: p.queue_timeout_ms,
179
- maxQueueSize: p.max_queue_size,
180
- });
181
- }
182
- tracker.updateProviderConfig(p.id, {
183
- name: p.name,
184
- maxConcurrency: p.max_concurrency ?? 0,
185
- queueTimeoutMs: p.queue_timeout_ms ?? PROVIDER_DEFAULT_QUEUE_TIMEOUT_MS,
186
- maxQueueSize: p.max_queue_size ?? PROVIDER_DEFAULT_MAX_QUEUE_SIZE,
187
- });
188
- }
221
+ // DB 读取已有 provider 的并发配置,初始化信号量/adaptive/tracker(共享逻辑)
222
+ initializeProviderState(db, semaphoreManager, adaptiveController, tracker);
189
223
  app.register(authMiddleware, { db });
190
- app.register(openaiProxy, {
191
- db,
192
- streamTimeoutMs: config.STREAM_TIMEOUT_MS,
193
- retryBaseDelayMs: config.RETRY_BASE_DELAY_MS,
194
- matcher,
195
- semaphoreManager,
196
- tracker,
197
- usageWindowTracker,
198
- sessionTracker,
199
- adaptiveController,
200
- });
201
- app.register(anthropicProxy, {
202
- db,
203
- streamTimeoutMs: config.STREAM_TIMEOUT_MS,
204
- retryBaseDelayMs: config.RETRY_BASE_DELAY_MS,
205
- matcher,
206
- semaphoreManager,
207
- tracker,
208
- usageWindowTracker,
209
- sessionTracker,
210
- adaptiveController,
211
- });
212
- app.register(adminRoutes, { db, matcher, tracker, semaphoreManager, adaptiveController });
224
+ app.register(openaiProxy, { db, container });
225
+ app.register(anthropicProxy, { db, container });
226
+ // StateRegistry — Admin 层通过此接口触发 proxy 层状态刷新,消除 admin→proxy 依赖
227
+ const stateRegistry = {
228
+ refreshRetryRules: () => matcher.load(db),
229
+ updateProviderConcurrency: (providerId, cfg) => semaphoreManager.updateConfig(providerId, cfg),
230
+ removeProvider: (providerId) => semaphoreManager.remove(providerId),
231
+ removeAllProviders: () => semaphoreManager.removeAll(),
232
+ getProviderStatus: (providerId) => semaphoreManager.getStatus(providerId),
233
+ clearModelState: () => modelState.clearAll(),
234
+ deleteModelState: (keyId, sessionId) => modelState.delete(keyId, sessionId),
235
+ getEnhancementConfig: () => loadEnhancementConfig(db),
236
+ syncAdaptiveProvider: (providerId, cfg) => adaptiveController.syncProvider(providerId, cfg),
237
+ removeAdaptiveProvider: (providerId) => adaptiveController.remove(providerId),
238
+ getAdaptiveStatus: (providerId) => adaptiveController.getStatus(providerId),
239
+ reinitializeProviders: () => {
240
+ adaptiveController.removeAll();
241
+ initializeProviderState(db, semaphoreManager, adaptiveController, tracker);
242
+ },
243
+ };
244
+ // Late-bound close ref — close 函数在 adminRoutes 注册之后才定义,
245
+ // 但 restart API 需要在运行时调用它
246
+ const closeRef = { fn: async () => { } };
247
+ app.register(adminRoutes, { db, stateRegistry, tracker, adaptiveController, logFileWriter, logsDir, closeFn: () => closeRef.fn() });
213
248
  // 前端静态文件服务(生产环境)
214
249
  const frontendDist = path.resolve(process.env.FRONTEND_DIST || path.join(__dirname, "../frontend-dist"));
215
250
  if (existsSync(frontendDist)) {
@@ -237,26 +272,79 @@ export async function buildApp(options) {
237
272
  const dbSizeMonitor = scheduleDbSizeMonitor(db, config.DB_PATH, {
238
273
  log: app.log,
239
274
  });
275
+ let close = async () => {
276
+ stopUpgradeChecker();
277
+ logCleanup.stop();
278
+ dbSizeMonitor.stop();
279
+ tracker.stopPushInterval();
280
+ modelState.clearAll();
281
+ semaphoreManager.removeAll();
282
+ const sessionTracker = container.resolve(SERVICE_KEYS.sessionTracker);
283
+ sessionTracker.stop();
284
+ await app.close();
285
+ db.close();
286
+ };
287
+ // 文件压缩和清理任务(仅非 :memory: 模式)
288
+ if (!isMemoryDb) {
289
+ const logFileMaintenance = scheduleLogFileMaintenance(logsDir, {
290
+ retentionDays: getLogFileRetentionDays(db),
291
+ log: app.log,
292
+ });
293
+ // 注册到 close
294
+ const prevClose = close;
295
+ close = async () => {
296
+ logFileMaintenance.stop();
297
+ await prevClose();
298
+ };
299
+ }
300
+ // 将最终版 close 函数绑定到 late-bound ref(供 restart API 运行时调用)
301
+ closeRef.fn = close;
240
302
  return {
241
303
  app,
242
304
  db,
243
305
  usageWindowTracker,
244
- close: async () => {
245
- stopUpgradeChecker();
246
- logCleanup.stop();
247
- dbSizeMonitor.stop();
248
- tracker.stopPushInterval();
249
- sessionTracker.stop();
250
- await app.close();
251
- db.close();
252
- },
306
+ close,
253
307
  };
254
308
  }
255
- // index.ts 自身也需要 getBaseConfig,避免循环依赖
256
- import { getBaseConfig } from "./config.js";
257
309
  export async function main() {
258
- const { app } = await buildApp();
310
+ const { app, close } = await buildApp();
259
311
  const config = getConfig();
312
+ // 全局兜底:防止未捕获异常导致进程崩溃
313
+ process.on("uncaughtException", (err) => {
314
+ try {
315
+ app.log.fatal({ err }, "Uncaught exception");
316
+ /* eslint-disable taste/no-silent-catch -- app.log 可能已崩溃,console 是最后手段 */
317
+ }
318
+ catch {
319
+ console.error("FATAL: Uncaught exception:", err);
320
+ }
321
+ /* eslint-enable taste/no-silent-catch */
322
+ close().finally(() => process.exit(1));
323
+ });
324
+ process.on("unhandledRejection", (reason) => {
325
+ try {
326
+ app.log.error({ err: reason instanceof Error ? reason : new Error(String(reason)) }, "Unhandled rejection");
327
+ /* eslint-disable taste/no-silent-catch -- app.log 可能已崩溃,console 是最后手段 */
328
+ }
329
+ catch {
330
+ console.error("Unhandled rejection:", reason);
331
+ }
332
+ /* eslint-enable taste/no-silent-catch */
333
+ });
334
+ // 优雅关闭:SIGTERM(systemd/docker stop)和 SIGINT(Ctrl+C)
335
+ const shutdown = async (signal) => {
336
+ try {
337
+ app.log.info(`Received ${signal}, shutting down gracefully...`);
338
+ await close();
339
+ app.log.info("Shutdown complete");
340
+ }
341
+ catch (err) {
342
+ app.log.error({ err }, "Error during shutdown");
343
+ }
344
+ process.exit(0);
345
+ };
346
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
347
+ process.on("SIGINT", () => shutdown("SIGINT"));
260
348
  try {
261
349
  await app.listen({ port: config.PORT, host: "0.0.0.0" });
262
350
  app.log.info(`Server listening on port ${config.PORT}`);
@@ -1,28 +1,5 @@
1
+ import type { MetricsResult } from "../core/types.js";
1
2
  import type { SSEEvent } from "./sse-parser.js";
2
- export interface MetricsResult {
3
- input_tokens: number | null;
4
- output_tokens: number | null;
5
- cache_creation_tokens: number | null;
6
- cache_read_tokens: number | null;
7
- ttft_ms: number | null;
8
- /** T6 - T0: proxy end-to-end streaming duration */
9
- total_duration_ms: number | null;
10
- /** @deprecated Use total_tps instead */
11
- tokens_per_second: number | null;
12
- stop_reason: string | null;
13
- is_complete: number;
14
- input_tokens_estimated?: number;
15
- thinking_tokens: number | null;
16
- /** T3 - T0: request start to last thinking delta */
17
- thinking_duration_ms: number | null;
18
- thinking_tps: number | null;
19
- /** T6 - T3 (thinking) or T6 - T0 (non-thinking) */
20
- non_thinking_duration_ms: number | null;
21
- non_thinking_tps: number | null;
22
- total_tps: number | null;
23
- text_tokens: number | null;
24
- tool_use_tokens: number | null;
25
- }
26
3
  export declare class MetricsExtractor {
27
4
  private apiType;
28
5
  private requestStartTime;
@@ -1,5 +1,5 @@
1
1
  // TODO: 当文件超过 400 行时拆分为 metrics-streaming.ts(流式事件处理 + TPS 计算)和 metrics-extractor.ts(非流式 + 类型)
2
- import { MS_PER_SECOND } from "../constants.js";
2
+ import { MS_PER_SECOND } from "../core/constants.js";
3
3
  import { encode } from "gpt-tokenizer";
4
4
  export class MetricsExtractor {
5
5
  apiType;
@@ -1,6 +1,6 @@
1
1
  import { Transform, TransformCallback } from "stream";
2
2
  import { MetricsExtractor } from "./metrics-extractor.js";
3
- import type { MetricsResult } from "./metrics-extractor.js";
3
+ import type { MetricsResult } from "../core/types.js";
4
4
  export interface MetricsTransformOptions {
5
5
  /** 每次处理 SSE 事件后触发的回调,附带当前指标快照 */
6
6
  onMetrics?: (metrics: MetricsResult) => void;
@@ -53,6 +53,10 @@ export const adminLoginRoutes = (app, options, done) => {
53
53
  return reply.code(HTTP_UNAUTHORIZED).send(apiError(API_CODE.WRONG_PASSWORD, "Invalid password"));
54
54
  }
55
55
  const secret = getSetting(options.db, "jwt_secret");
56
+ if (!secret) {
57
+ request.log.error("JWT secret not configured, cannot issue token");
58
+ return reply.code(HTTP_UNAUTHORIZED).send(apiError(API_CODE.TOKEN_INVALID, "JWT secret not configured"));
59
+ }
56
60
  const token = jwt.sign({ role: "admin" }, secret, { expiresIn: TOKEN_EXPIRY_SECONDS });
57
61
  reply.setCookie("admin_token", token, {
58
62
  path: "/admin",
@@ -2,10 +2,9 @@ import { createHash, randomUUID } from "crypto";
2
2
  import fp from "fastify-plugin";
3
3
  import { isInitialized } from "../db/settings.js";
4
4
  import { insertRequestLog } from "../db/logs.js";
5
- import { getProxyApiType } from "../constants.js";
5
+ import { getProxyApiType, HTTP_SERVICE_UNAVAILABLE } from "../core/constants.js";
6
6
  const SKIP_PATHS = ["/health", "/admin"];
7
7
  const HTTP_UNAUTHORIZED = 401;
8
- const HTTP_SERVICE_UNAVAILABLE = 503;
9
8
  const BEARER_PREFIX_LENGTH = "Bearer ".length;
10
9
  function shouldSkipAuth(url) {
11
10
  const path = url.split("?")[0];
@@ -1,9 +1,8 @@
1
1
  import type { ServerResponse } from "node:http";
2
2
  import { StatsAggregator } from "./stats-aggregator.js";
3
3
  import { RuntimeCollector } from "./runtime-collector.js";
4
- import type { ProviderSemaphoreManager } from "../proxy/semaphore.js";
5
4
  import type { AdaptiveConcurrencyController } from "../proxy/adaptive-controller.js";
6
- import type { ActiveRequest, AttemptSnapshot, ProviderConcurrencySnapshot, RuntimeMetrics, StatsSnapshot } from "./types.js";
5
+ import type { ActiveRequest, AttemptSnapshot, ISemaphoreStatus, ProviderConcurrencySnapshot, RuntimeMetrics, StatsSnapshot } from "./types.js";
7
6
  export interface TrackerLogger {
8
7
  debug(obj: Record<string, unknown>, msg: string): void;
9
8
  warn(obj: Record<string, unknown>, msg: string): void;
@@ -25,7 +24,7 @@ export declare class RequestTracker {
25
24
  private readonly semaphoreManager?;
26
25
  private adaptiveController?;
27
26
  constructor(deps?: {
28
- semaphoreManager?: ProviderSemaphoreManager;
27
+ semaphoreManager?: ISemaphoreStatus;
29
28
  runtimeCollector?: RuntimeCollector;
30
29
  logger?: TrackerLogger;
31
30
  });
@@ -50,7 +49,7 @@ export declare class RequestTracker {
50
49
  getConcurrency(): ProviderConcurrencySnapshot[];
51
50
  getRuntime(): RuntimeMetrics;
52
51
  addClient(res: ServerResponse): void;
53
- /** 向单个客户端发送当前活跃请求快照 */
52
+ /** 向单个客户端发送当前活跃请求快照(保留 clientRequest 以便前端即时展示) */
54
53
  private sendInitialSnapshot;
55
54
  removeClient(res: ServerResponse): void;
56
55
  startPushInterval(): void;
@@ -173,13 +173,9 @@ export class RequestTracker {
173
173
  this.clients.delete(res);
174
174
  });
175
175
  }
176
- /** 向单个客户端发送当前活跃请求快照 */
176
+ /** 向单个客户端发送当前活跃请求快照(保留 clientRequest 以便前端即时展示) */
177
177
  sendInitialSnapshot(res) {
178
- const active = this.getActive().map((req) => {
179
- const copy = { ...req };
180
- delete copy.clientRequest;
181
- return copy;
182
- });
178
+ const active = this.getActive();
183
179
  const msg = `event: request_update\ndata: ${JSON.stringify(active)}\n\n`;
184
180
  try {
185
181
  if (!res.writableEnded)
@@ -224,17 +220,11 @@ export class RequestTracker {
224
220
  this.runtimeCollector.stop();
225
221
  }
226
222
  broadcast(event, data) {
227
- // Strip clientRequest from broadcasts to reduce bandwidth;
228
- // full data available on-demand via API endpoint
223
+ // request_update: 保留 clientRequest,前端 pending 请求需要即时展示内容
224
+ // request_start: 无需处理,已是原始数据
225
+ // request_complete: strip clientRequest(完成后从 DB 加载详情)
229
226
  let payload = data;
230
- if (event === "request_update" && Array.isArray(data)) {
231
- payload = data.map((req) => {
232
- const copy = { ...req };
233
- delete copy.clientRequest;
234
- return copy;
235
- });
236
- }
237
- else if (event === "request_complete" && data && typeof data === "object") {
227
+ if (event === "request_complete" && data && typeof data === "object") {
238
228
  const copy = { ...data };
239
229
  delete copy.clientRequest;
240
230
  payload = copy;
@@ -1,5 +1,5 @@
1
1
  import { performance } from "node:perf_hooks";
2
- import { MS_PER_SECOND } from "../constants.js";
2
+ import { MS_PER_SECOND } from "../core/constants.js";
3
3
  const NS_PER_MS = 1e6;
4
4
  const perf = performance;
5
5
  const proc = process;
@@ -29,6 +29,7 @@ export interface ActiveRequest {
29
29
  clientIp?: string;
30
30
  sessionId?: string;
31
31
  clientRequest?: string;
32
+ upstreamRequest?: string;
32
33
  completedAt?: number;
33
34
  }
34
35
  export interface AttemptSnapshot {
@@ -89,6 +90,13 @@ export interface ProviderStats {
89
90
  count: number;
90
91
  }>;
91
92
  }
93
+ /** request-tracker 需要的信号量状态查询接口 */
94
+ export interface ISemaphoreStatus {
95
+ getStatus(providerId: string): {
96
+ active: number;
97
+ queued: number;
98
+ };
99
+ }
92
100
  export interface RuntimeMetrics {
93
101
  uptimeMs: number;
94
102
  memoryUsage: NodeJS.MemoryUsage;
@@ -1,4 +1,4 @@
1
- import type { ProviderSemaphoreManager } from "./semaphore.js";
1
+ import type { ProviderSemaphoreManager } from "./orchestration/semaphore.js";
2
2
  export interface AdaptiveState {
3
3
  currentLimit: number;
4
4
  probeActive: boolean;
@@ -31,7 +31,10 @@ export declare class AdaptiveConcurrencyController {
31
31
  queueTimeoutMs: number;
32
32
  maxQueueSize: number;
33
33
  }): void;
34
+ /** 移除 provider 的自适应并发状态。调用方还需调用 semaphoreManager.remove() 或 updateConfig() 清理信号量配置。 */
34
35
  remove(providerId: string): void;
36
+ /** 清除所有 provider 的自适应并发状态(导入配置后重建前调用) */
37
+ removeAll(): void;
35
38
  onRequestComplete(providerId: string, result: AdaptiveResult): void;
36
39
  getStatus(providerId: string): AdaptiveState | undefined;
37
40
  syncProvider(providerId: string, p: ProviderAdaptiveConfig): void;
@@ -29,9 +29,14 @@ export class AdaptiveConcurrencyController {
29
29
  });
30
30
  this.syncToSemaphore(providerId);
31
31
  }
32
+ /** 移除 provider 的自适应并发状态。调用方还需调用 semaphoreManager.remove() 或 updateConfig() 清理信号量配置。 */
32
33
  remove(providerId) {
33
34
  this.entries.delete(providerId);
34
35
  }
36
+ /** 清除所有 provider 的自适应并发状态(导入配置后重建前调用) */
37
+ removeAll() {
38
+ this.entries.clear();
39
+ }
35
40
  onRequestComplete(providerId, result) {
36
41
  const entry = this.entries.get(providerId);
37
42
  if (!entry)
@@ -1,6 +1,5 @@
1
- import type { FastifyRequest } from "fastify";
2
1
  import Database from "better-sqlite3";
3
- import { type EnhancementConfig } from "../enhancement-config.js";
2
+ import { type EnhancementConfig } from "../routing/enhancement-config.js";
4
3
  export interface InterceptResponse {
5
4
  statusCode: number;
6
5
  body: unknown;
@@ -10,15 +9,32 @@ export interface InterceptResponse {
10
9
  detail?: string;
11
10
  };
12
11
  }
12
+ export interface EnhancementMeta {
13
+ router_tags_stripped: number;
14
+ directive: {
15
+ type: "select_model" | "router_model" | "router_command";
16
+ value: string;
17
+ } | null;
18
+ }
13
19
  export interface EnhancementResult {
20
+ body: Record<string, unknown>;
14
21
  effectiveModel: string;
15
22
  originalModel: string | null;
16
23
  interceptResponse: InterceptResponse | null;
24
+ meta: EnhancementMeta;
25
+ }
26
+ /** 调用方传入的 routerKey 信息,从 FastifyRequest.routerKey 解耦 */
27
+ export interface RouterKeyInfo {
28
+ id: string;
29
+ name: string;
30
+ allowed_models: string | null;
17
31
  }
18
32
  /**
19
33
  * 在代理转发前应用代理增强逻辑(指令解析 + 会话记忆 + 模型替换 + 命令拦截)。
20
34
  * 仅当 proxy_enhancement.claude_code_enabled 开启时生效。
35
+ *
36
+ * 纯函数:不修改输入 body,返回变换后的新 body + 元数据。
21
37
  */
22
- export declare function applyEnhancement(db: Database.Database, request: FastifyRequest, clientModel: string, sessionId?: string, enhancementConfig?: EnhancementConfig): EnhancementResult;
38
+ export declare function applyEnhancement(db: Database.Database, body: Record<string, unknown>, clientModel: string, sessionId: string | undefined, routerKey: RouterKeyInfo | undefined, enhancementConfig?: EnhancementConfig): EnhancementResult;
23
39
  /** 生成注入到非流式响应中的模型信息标签 */
24
40
  export declare function buildModelInfoTag(effectiveModel: string): string;