llm-simple-router 0.6.6 → 0.7.0

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 (176) hide show
  1. package/dist/admin/constants.d.ts +1 -1
  2. package/dist/admin/constants.js +2 -2
  3. package/dist/admin/logs.d.ts +2 -0
  4. package/dist/admin/logs.js +17 -1
  5. package/dist/admin/providers.d.ts +2 -2
  6. package/dist/admin/providers.js +29 -16
  7. package/dist/admin/proxy-enhancement.d.ts +2 -0
  8. package/dist/admin/proxy-enhancement.js +4 -10
  9. package/dist/admin/retry-rules.d.ts +2 -2
  10. package/dist/admin/retry-rules.js +4 -8
  11. package/dist/admin/routes.d.ts +4 -4
  12. package/dist/admin/routes.js +6 -6
  13. package/dist/admin/settings-import-export.d.ts +2 -4
  14. package/dist/admin/settings-import-export.js +9 -19
  15. package/dist/admin/settings.d.ts +1 -0
  16. package/dist/admin/settings.js +29 -1
  17. package/dist/admin/upgrade.js +1 -1
  18. package/dist/{constants.d.ts → core/constants.d.ts} +4 -1
  19. package/dist/{constants.js → core/constants.js} +21 -1
  20. package/dist/core/container.d.ts +31 -0
  21. package/dist/core/container.js +41 -0
  22. package/dist/core/errors.d.ts +26 -0
  23. package/dist/core/errors.js +42 -0
  24. package/dist/core/registry.d.ts +43 -0
  25. package/dist/core/registry.js +3 -0
  26. package/dist/core/types.d.ts +105 -0
  27. package/dist/core/types.js +3 -0
  28. package/dist/db/index.d.ts +1 -1
  29. package/dist/db/index.js +1 -1
  30. package/dist/db/logs.d.ts +11 -24
  31. package/dist/db/logs.js +37 -38
  32. package/dist/db/metrics.js +1 -1
  33. package/dist/db/migrations/033_add_pipeline_snapshot.sql +1 -0
  34. package/dist/db/migrations/034_drop_redundant_log_columns.sql +13 -0
  35. package/dist/db/settings.d.ts +2 -0
  36. package/dist/db/settings.js +9 -0
  37. package/dist/index.d.ts +10 -2
  38. package/dist/index.js +191 -108
  39. package/dist/metrics/metrics-extractor.d.ts +1 -24
  40. package/dist/metrics/metrics-extractor.js +1 -1
  41. package/dist/metrics/sse-metrics-transform.d.ts +1 -1
  42. package/dist/middleware/admin-auth.js +4 -0
  43. package/dist/middleware/auth.js +1 -2
  44. package/dist/monitor/request-tracker.d.ts +3 -4
  45. package/dist/monitor/request-tracker.js +6 -16
  46. package/dist/monitor/runtime-collector.js +1 -1
  47. package/dist/monitor/types.d.ts +8 -0
  48. package/dist/proxy/adaptive-controller.d.ts +4 -1
  49. package/dist/proxy/adaptive-controller.js +16 -2
  50. package/dist/proxy/enhancement/enhancement-handler.d.ts +19 -3
  51. package/dist/proxy/enhancement/enhancement-handler.js +80 -28
  52. package/dist/proxy/enhancement/index.d.ts +1 -0
  53. package/dist/proxy/handler/anthropic.d.ts +7 -0
  54. package/dist/proxy/{anthropic.js → handler/anthropic.js} +8 -7
  55. package/dist/proxy/handler/openai.d.ts +7 -0
  56. package/dist/proxy/{openai.js → handler/openai.js} +10 -9
  57. package/dist/proxy/handler/proxy-handler-utils.d.ts +9 -0
  58. package/dist/proxy/handler/proxy-handler-utils.js +63 -0
  59. package/dist/proxy/handler/proxy-handler.d.ts +13 -0
  60. package/dist/proxy/{proxy-handler.js → handler/proxy-handler.js} +104 -120
  61. package/dist/proxy/log-detail-policy.d.ts +12 -0
  62. package/dist/proxy/log-detail-policy.js +21 -0
  63. package/dist/proxy/log-helpers.d.ts +8 -0
  64. package/dist/proxy/log-helpers.js +16 -4
  65. package/dist/proxy/loop-prevention/tool-loop-guard.d.ts +1 -1
  66. package/dist/proxy/loop-prevention/tool-loop-guard.js +9 -12
  67. package/dist/proxy/{orchestrator.d.ts → orchestration/orchestrator.d.ts} +6 -4
  68. package/dist/proxy/{orchestrator.js → orchestration/orchestrator.js} +2 -1
  69. package/dist/proxy/{resilience.d.ts → orchestration/resilience.d.ts} +2 -14
  70. package/dist/proxy/{resilience.js → orchestration/resilience.js} +2 -2
  71. package/dist/proxy/{retry-rules.d.ts → orchestration/retry-rules.d.ts} +1 -1
  72. package/dist/proxy/{retry-rules.js → orchestration/retry-rules.js} +1 -1
  73. package/dist/proxy/{scope.d.ts → orchestration/scope.d.ts} +3 -3
  74. package/dist/proxy/{semaphore.d.ts → orchestration/semaphore.d.ts} +7 -15
  75. package/dist/proxy/{semaphore.js → orchestration/semaphore.js} +12 -26
  76. package/dist/proxy/patch/index.d.ts +8 -2
  77. package/dist/proxy/patch/index.js +5 -2
  78. package/dist/proxy/pipeline-snapshot.d.ts +37 -0
  79. package/dist/proxy/pipeline-snapshot.js +15 -0
  80. package/dist/proxy/proxy-core.d.ts +1 -1
  81. package/dist/proxy/proxy-core.js +1 -1
  82. package/dist/proxy/proxy-logging.d.ts +10 -2
  83. package/dist/proxy/proxy-logging.js +23 -9
  84. package/dist/proxy/response-transform.d.ts +7 -0
  85. package/dist/proxy/response-transform.js +15 -0
  86. package/dist/proxy/{enhancement-config.js → routing/enhancement-config.js} +1 -1
  87. package/dist/proxy/{mapping-resolver.d.ts → routing/mapping-resolver.d.ts} +1 -1
  88. package/dist/proxy/{mapping-resolver.js → routing/mapping-resolver.js} +1 -1
  89. package/dist/proxy/{model-state.js → routing/model-state.js} +1 -1
  90. package/dist/proxy/{overflow.d.ts → routing/overflow.d.ts} +1 -1
  91. package/dist/proxy/{overflow.js → routing/overflow.js} +3 -3
  92. package/dist/proxy/{usage-window-tracker.js → routing/usage-window-tracker.js} +3 -3
  93. package/dist/proxy/{transport.d.ts → transport/http.d.ts} +2 -2
  94. package/dist/proxy/{transport.js → transport/http.js} +3 -3
  95. package/dist/proxy/{stream-proxy.d.ts → transport/stream.d.ts} +4 -4
  96. package/dist/proxy/{stream-proxy.js → transport/stream.js} +25 -7
  97. package/dist/proxy/{transport-fn.d.ts → transport/transport-fn.d.ts} +5 -5
  98. package/dist/proxy/{transport-fn.js → transport/transport-fn.js} +11 -9
  99. package/dist/proxy/types.d.ts +3 -64
  100. package/dist/proxy/types.js +5 -34
  101. package/dist/storage/log-file-compressor.d.ts +15 -0
  102. package/dist/storage/log-file-compressor.js +83 -0
  103. package/dist/storage/log-file-writer.d.ts +21 -0
  104. package/dist/storage/log-file-writer.js +103 -0
  105. package/dist/storage/types.d.ts +16 -0
  106. package/dist/storage/types.js +5 -0
  107. package/frontend-dist/assets/{CardContent-jQcfCC7J.js → CardContent-B_EIvwon.js} +1 -1
  108. package/frontend-dist/assets/{CardTitle-BrCTvULL.js → CardTitle-DHU-obrV.js} +1 -1
  109. package/frontend-dist/assets/{CascadingModelSelect-BFh67j5d.js → CascadingModelSelect-BFbt3-xP.js} +1 -1
  110. package/frontend-dist/assets/{Checkbox-Bbt7JpdE.js → Checkbox-CNGgDj55.js} +1 -1
  111. package/frontend-dist/assets/{CollapsibleTrigger-DMnEA0qC.js → CollapsibleTrigger-BIBFqVyy.js} +1 -1
  112. package/frontend-dist/assets/{Collection-CVk3TPHc.js → Collection-CYT_tSZn.js} +1 -1
  113. package/frontend-dist/assets/{Dashboard-Coftbg4B.js → Dashboard-DHzoPggK.js} +1 -1
  114. package/frontend-dist/assets/{DialogTitle-BbOAZzPQ.js → DialogTitle-C1gk4GN_.js} +1 -1
  115. package/frontend-dist/assets/{Input-DdHY9q0w.js → Input-CXpbIxXJ.js} +1 -1
  116. package/frontend-dist/assets/{Label-DRQv_Dr_.js → Label-BbOaRyGK.js} +1 -1
  117. package/frontend-dist/assets/{Login-SV3ctFnJ.js → Login-Cc2ocWjt.js} +1 -1
  118. package/frontend-dist/assets/{Logs-BG45kX6E.js → Logs-DMMr-_-8.js} +1 -1
  119. package/frontend-dist/assets/{ModelMappings-DEaBnRU3.js → ModelMappings-Cg8svYGw.js} +1 -1
  120. package/frontend-dist/assets/{Monitor-ZHOt11n-.js → Monitor-ps-VVzq3.js} +1 -1
  121. package/frontend-dist/assets/{PopoverTrigger-z-Z3EjBk.js → PopoverTrigger-BA1XOIO0.js} +1 -1
  122. package/frontend-dist/assets/{PopperContent-DPC-6a3n.js → PopperContent--3HCLUEp.js} +1 -1
  123. package/frontend-dist/assets/{Providers-DpY6pAcg.js → Providers-Bjhhw2O4.js} +1 -1
  124. package/frontend-dist/assets/{ProxyEnhancement-D6KBDXMp.js → ProxyEnhancement-BhlZ3pYo.js} +1 -1
  125. package/frontend-dist/assets/{RetryRules-DWI7_WLZ.js → RetryRules-BxCAMxNM.js} +1 -1
  126. package/frontend-dist/assets/{RouterKeys-CZ1657eX.js → RouterKeys-N-5AHCZf.js} +1 -1
  127. package/frontend-dist/assets/{RovingFocusItem-BREE2YEV.js → RovingFocusItem-BXHXOE-y.js} +1 -1
  128. package/frontend-dist/assets/{Schedules-BVPsBRPi.js → Schedules-oxUdLakf.js} +1 -1
  129. package/frontend-dist/assets/{SelectValue-H8hwQwbk.js → SelectValue-RQZKO7OV.js} +1 -1
  130. package/frontend-dist/assets/Settings-BlIRQ9y-.js +6 -0
  131. package/frontend-dist/assets/{Setup-yOYNKkOG.js → Setup-CmpQtMiu.js} +1 -1
  132. package/frontend-dist/assets/{Switch-CojD3rTH.js → Switch-B3sVDDC7.js} +1 -1
  133. package/frontend-dist/assets/{TableHeader-awoHTsWN.js → TableHeader-Bpen86Ix.js} +1 -1
  134. package/frontend-dist/assets/{TabsTrigger-DTKSFj85.js → TabsTrigger-VNURbZR2.js} +1 -1
  135. package/frontend-dist/assets/{Teleport-DehYAXud.js → Teleport-CsHgwC1b.js} +1 -1
  136. package/frontend-dist/assets/{TooltipTrigger-C2dl_dml.js → TooltipTrigger-BHwxX9h-.js} +1 -1
  137. package/frontend-dist/assets/{UnifiedRequestDialog-C8A-uSTR.js → UnifiedRequestDialog-3rdh4fhf.js} +2 -2
  138. package/frontend-dist/assets/{VisuallyHidden-C8oaGi2S.js → VisuallyHidden-DCZ9DkoD.js} +1 -1
  139. package/frontend-dist/assets/{VisuallyHiddenInput-BMc813t2.js → VisuallyHiddenInput-BxgnyBWj.js} +1 -1
  140. package/frontend-dist/assets/{alert-dialog-C8TZQmU6.js → alert-dialog-LaLkz7c6.js} +1 -1
  141. package/frontend-dist/assets/arrow-down-BpGORRs9.js +1 -0
  142. package/frontend-dist/assets/{badge-BVh2WpA5.js → badge-BYEw6ni_.js} +1 -1
  143. package/frontend-dist/assets/{button-N59D1BGa.js → button-C5VcfpgV.js} +2 -2
  144. package/frontend-dist/assets/check-Bqx_fTQX.js +1 -0
  145. package/frontend-dist/assets/{copy-DTOecxa9.js → copy-CmWRpuG-.js} +1 -1
  146. package/frontend-dist/assets/{dialog-kA7AUNoc.js → dialog-BEKceMtO.js} +1 -1
  147. package/frontend-dist/assets/{file-text-DzZCFO7y.js → file-text-C_jIlYPS.js} +1 -1
  148. package/frontend-dist/assets/{index-B5upNblU.js → index-C2aljBfM.js} +1 -1
  149. package/frontend-dist/assets/{lib-ClDokUbt.js → lib-Cj3cGvin.js} +1 -1
  150. package/frontend-dist/assets/loader-circle-BLhRyZzy.js +1 -0
  151. package/frontend-dist/assets/{useClipboard-DU1ne-Jw.js → useClipboard-ZCpnVKiU.js} +1 -1
  152. package/frontend-dist/assets/{useFocusGuards-Btmdbg_F.js → useFocusGuards-BjqTo_uk.js} +1 -1
  153. package/frontend-dist/assets/useFormControl-D7vjbPSC.js +1 -0
  154. package/frontend-dist/assets/{useLogRetention--EGNWXig.js → useLogRetention-BZZvG6jh.js} +1 -1
  155. package/frontend-dist/assets/useNonce-BuusARRu.js +1 -0
  156. package/frontend-dist/assets/x-CUQFnWz8.js +1 -0
  157. package/frontend-dist/index.html +19 -19
  158. package/package.json +1 -1
  159. package/dist/proxy/anthropic.d.ts +0 -19
  160. package/dist/proxy/openai.d.ts +0 -19
  161. package/dist/proxy/proxy-handler.d.ts +0 -19
  162. package/dist/proxy/strategy/types.d.ts +0 -21
  163. package/dist/proxy/strategy/types.js +0 -1
  164. package/frontend-dist/assets/Settings-DHYaYRgU.js +0 -6
  165. package/frontend-dist/assets/arrow-down-D-cQXxau.js +0 -1
  166. package/frontend-dist/assets/check-dDgrw3T3.js +0 -1
  167. package/frontend-dist/assets/loader-circle-DVHRL-38.js +0 -1
  168. package/frontend-dist/assets/useFormControl-C5Kjziuj.js +0 -1
  169. package/frontend-dist/assets/useNonce-Cp31yRzV.js +0 -1
  170. package/frontend-dist/assets/x-DMktsI_w.js +0 -1
  171. /package/dist/{config.d.ts → config/index.d.ts} +0 -0
  172. /package/dist/{config.js → config/index.js} +0 -0
  173. /package/dist/proxy/{scope.js → orchestration/scope.js} +0 -0
  174. /package/dist/proxy/{enhancement-config.d.ts → routing/enhancement-config.d.ts} +0 -0
  175. /package/dist/proxy/{model-state.d.ts → routing/model-state.d.ts} +0 -0
  176. /package/dist/proxy/{usage-window-tracker.d.ts → routing/usage-window-tracker.d.ts} +0 -0
@@ -1 +1 @@
1
- export { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_INTERNAL_ERROR, HTTP_BAD_GATEWAY, HTTP_SERVICE_UNAVAILABLE, } from "../constants.js";
1
+ export { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_INTERNAL_ERROR, HTTP_BAD_GATEWAY, HTTP_SERVICE_UNAVAILABLE, } from "../core/constants.js";
@@ -1,2 +1,2 @@
1
- // HTTP 状态码统一从 src/constants.ts 导入,避免重复定义
2
- export { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_INTERNAL_ERROR, HTTP_BAD_GATEWAY, HTTP_SERVICE_UNAVAILABLE, } from "../constants.js";
1
+ // HTTP 状态码统一从 core/constants.ts 导入,避免重复定义
2
+ export { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_INTERNAL_ERROR, HTTP_BAD_GATEWAY, HTTP_SERVICE_UNAVAILABLE, } from "../core/constants.js";
@@ -1,7 +1,9 @@
1
1
  import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
+ import type { LogFileWriter } from "../storage/log-file-writer.js";
3
4
  interface LogRoutesOptions {
4
5
  db: Database.Database;
6
+ logFileWriter?: LogFileWriter | null;
5
7
  }
6
8
  export declare const adminLogRoutes: FastifyPluginCallback<LogRoutesOptions>;
7
9
  export {};
@@ -19,7 +19,7 @@ const DeleteLogsBeforeSchema = Type.Object({
19
19
  });
20
20
  const DEFAULT_LOG_VIEW = "flat";
21
21
  export const adminLogRoutes = (app, options, done) => {
22
- const { db } = options;
22
+ const { db, logFileWriter } = options;
23
23
  app.get("/admin/api/logs", { schema: { querystring: LogQuerySchema } }, async (request, reply) => {
24
24
  const query = request.query;
25
25
  const page = parseInt(query.page || "1", 10);
@@ -47,6 +47,22 @@ export const adminLogRoutes = (app, options, done) => {
47
47
  if (!log) {
48
48
  return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Log not found"));
49
49
  }
50
+ // DB 字段为 null 时,从 JSONL 文件回填详情
51
+ const needsBackfill = log.client_request === null || log.upstream_request === null || log.upstream_response === null;
52
+ if (needsBackfill && logFileWriter && logFileWriter.isEnabled && log.created_at) {
53
+ const fileEntry = logFileWriter.read(log.id, log.created_at);
54
+ if (fileEntry) {
55
+ if (log.client_request === null && fileEntry.client_request !== null) {
56
+ log.client_request = fileEntry.client_request;
57
+ }
58
+ if (log.upstream_request === null && fileEntry.upstream_request !== null) {
59
+ log.upstream_request = fileEntry.upstream_request;
60
+ }
61
+ if (log.upstream_response === null && fileEntry.upstream_response !== null) {
62
+ log.upstream_response = fileEntry.upstream_response;
63
+ }
64
+ }
65
+ }
50
66
  return reply.send(log);
51
67
  });
52
68
  app.get("/admin/api/logs/:id/children", async (request, reply) => {
@@ -1,11 +1,11 @@
1
1
  import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
- import { ProviderSemaphoreManager } from "../proxy/semaphore.js";
3
+ import type { StateRegistry } from "../core/registry.js";
4
4
  import type { AdaptiveConcurrencyController } from "../proxy/adaptive-controller.js";
5
5
  import type { RequestTracker } from "../monitor/request-tracker.js";
6
6
  interface ProviderRoutesOptions {
7
7
  db: Database.Database;
8
- semaphoreManager?: ProviderSemaphoreManager;
8
+ stateRegistry?: StateRegistry;
9
9
  tracker?: RequestTracker;
10
10
  adaptiveController?: AdaptiveConcurrencyController;
11
11
  }
@@ -95,7 +95,7 @@ const UpdateProviderSchema = Type.Object({
95
95
  adaptive_enabled: Type.Optional(Type.Integer({ minimum: 0, maximum: 1 })),
96
96
  });
97
97
  export const adminProviderRoutes = (app, options, done) => {
98
- const { db, semaphoreManager, tracker, adaptiveController } = options;
98
+ const { db, stateRegistry, tracker, adaptiveController } = options;
99
99
  app.get("/admin/api/providers", async (_request, reply) => {
100
100
  const encryptionKey = getSetting(db, "encryption_key");
101
101
  const providers = getAllProviders(db);
@@ -114,7 +114,7 @@ export const adminProviderRoutes = (app, options, done) => {
114
114
  queue_timeout_ms: s.queue_timeout_ms,
115
115
  max_queue_size: s.max_queue_size,
116
116
  adaptive_enabled: s.adaptive_enabled,
117
- concurrency_status: semaphoreManager?.getStatus(s.id) ?? { active: 0, queued: 0 },
117
+ concurrency_status: stateRegistry?.getProviderStatus(s.id) ?? { active: 0, queued: 0 },
118
118
  created_at: s.created_at,
119
119
  updated_at: s.updated_at,
120
120
  };
@@ -148,11 +148,14 @@ export const adminProviderRoutes = (app, options, done) => {
148
148
  if (contextOverrides.length > 0) {
149
149
  setModelInfoForProvider(db, id, contextOverrides.map(o => ({ model_name: o.name, context_window: o.context_window })));
150
150
  }
151
- semaphoreManager?.updateConfig(id, {
152
- maxConcurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
153
- queueTimeoutMs: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
154
- maxQueueSize: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
155
- });
151
+ // 当 adaptive 启用时,由 syncProvider 全权管理信号量(避免重复调用 updateConfig
152
+ if (!isAdaptiveEnabled) {
153
+ stateRegistry?.updateProviderConcurrency(id, {
154
+ maxConcurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
155
+ queueTimeoutMs: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
156
+ maxQueueSize: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
157
+ });
158
+ }
156
159
  adaptiveController?.syncProvider(id, {
157
160
  adaptive_enabled: isAdaptiveEnabled,
158
161
  max_concurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
@@ -213,21 +216,31 @@ export const adminProviderRoutes = (app, options, done) => {
213
216
  let cascade;
214
217
  if (existing.is_active === 1 && body.is_active === 0) {
215
218
  cascade = cascadeProviderDisable(db, id);
219
+ // 禁用时清理信号量和自适应并发,避免排队请求悬挂
220
+ stateRegistry?.removeProvider(id);
221
+ adaptiveController?.remove(id);
216
222
  }
217
- if (body.max_concurrency !== undefined || body.queue_timeout_ms !== undefined || body.max_queue_size !== undefined) {
218
- semaphoreManager?.updateConfig(id, {
219
- maxConcurrency: updated.max_concurrency,
220
- queueTimeoutMs: updated.queue_timeout_ms,
221
- maxQueueSize: updated.max_queue_size,
222
- });
223
- }
224
- if (body.adaptive_enabled !== undefined || body.max_concurrency !== undefined || body.queue_timeout_ms !== undefined || body.max_queue_size !== undefined) {
223
+ // 重新启用时重建信号量和自适应并发
224
+ const concurrencyChanged = body.max_concurrency !== undefined || body.queue_timeout_ms !== undefined || body.max_queue_size !== undefined;
225
+ const adaptiveChanged = body.adaptive_enabled !== undefined;
226
+ const reenabled = existing.is_active === 0 && body.is_active === 1;
227
+ const needsSync = concurrencyChanged || adaptiveChanged || reenabled;
228
+ if (needsSync) {
229
+ // adaptive 同步:syncProvider 内部根据 adaptive_enabled 决定是 init+syncToSemaphore 还是 remove+updateConfig
225
230
  adaptiveController?.syncProvider(id, {
226
231
  adaptive_enabled: updated.adaptive_enabled,
227
232
  max_concurrency: updated.max_concurrency,
228
233
  queue_timeout_ms: updated.queue_timeout_ms,
229
234
  max_queue_size: updated.max_queue_size,
230
235
  });
236
+ // 非 adaptive 模式下手动同步信号量(adaptive 启用时由 syncProvider 内部管理)
237
+ if (!updated.adaptive_enabled) {
238
+ stateRegistry?.updateProviderConcurrency(id, {
239
+ maxConcurrency: updated.max_concurrency,
240
+ queueTimeoutMs: updated.queue_timeout_ms,
241
+ maxQueueSize: updated.max_queue_size,
242
+ });
243
+ }
231
244
  }
232
245
  tracker?.updateProviderConfig(id, {
233
246
  name: body.name ?? existing.name,
@@ -292,7 +305,7 @@ export const adminProviderRoutes = (app, options, done) => {
292
305
  }
293
306
  }
294
307
  deleteProvider(db, id);
295
- semaphoreManager?.remove(id);
308
+ stateRegistry?.removeProvider(id);
296
309
  adaptiveController?.remove(id);
297
310
  tracker?.removeProviderConfig(id);
298
311
  return reply.send({ success: true });
@@ -1,7 +1,9 @@
1
1
  import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
+ import type { StateRegistry } from "../core/registry.js";
3
4
  interface ProxyEnhancementOptions {
4
5
  db: Database.Database;
6
+ stateRegistry?: StateRegistry;
5
7
  }
6
8
  export declare const adminProxyEnhancementRoutes: FastifyPluginCallback<ProxyEnhancementOptions>;
7
9
  export {};
@@ -1,6 +1,5 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { setSetting } from "../db/settings.js";
3
- import { loadEnhancementConfig } from "../proxy/enhancement-config.js";
4
3
  const UpdateProxyEnhancementSchema = Type.Object({
5
4
  claude_code_enabled: Type.Boolean(),
6
5
  tool_call_loop_enabled: Type.Boolean(),
@@ -11,16 +10,11 @@ const SessionParamsSchema = Type.Object({
11
10
  sessionId: Type.String(),
12
11
  });
13
12
  import { getSessionStates, getSessionHistory, } from "../db/session-states.js";
14
- import { modelState } from "../proxy/model-state.js";
15
13
  export const adminProxyEnhancementRoutes = (app, options, done) => {
16
- const { db } = options;
14
+ const { db, stateRegistry } = options;
17
15
  app.get("/admin/api/proxy-enhancement", async (_request, reply) => {
18
- const config = loadEnhancementConfig(db);
19
- return reply.send({
20
- claude_code_enabled: config.claude_code_enabled,
21
- tool_call_loop_enabled: config.tool_call_loop_enabled,
22
- stream_loop_enabled: config.stream_loop_enabled,
23
- });
16
+ const config = stateRegistry?.getEnhancementConfig() ?? { claude_code_enabled: false, tool_call_loop_enabled: false, stream_loop_enabled: false };
17
+ return reply.send(config);
24
18
  });
25
19
  app.put("/admin/api/proxy-enhancement", { schema: { body: UpdateProxyEnhancementSchema } }, async (request, reply) => {
26
20
  const body = request.body;
@@ -43,7 +37,7 @@ export const adminProxyEnhancementRoutes = (app, options, done) => {
43
37
  });
44
38
  app.delete("/admin/api/session-states/:keyId/:sessionId", { schema: { params: SessionParamsSchema } }, async (req, reply) => {
45
39
  const { keyId, sessionId } = req.params;
46
- modelState.delete(keyId, sessionId);
40
+ stateRegistry?.deleteModelState(keyId, sessionId);
47
41
  return reply.send({ success: true });
48
42
  });
49
43
  done();
@@ -1,9 +1,9 @@
1
1
  import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
- import { RetryRuleMatcher } from "../proxy/retry-rules.js";
3
+ import type { StateRegistry } from "../core/registry.js";
4
4
  interface RetryRuleRoutesOptions {
5
5
  db: Database.Database;
6
- matcher: RetryRuleMatcher | null;
6
+ stateRegistry?: StateRegistry;
7
7
  }
8
8
  export declare const adminRetryRuleRoutes: FastifyPluginCallback<RetryRuleRoutesOptions>;
9
9
  export {};
@@ -34,12 +34,8 @@ function validateBodyPattern(pattern) {
34
34
  return "Invalid body_pattern regex";
35
35
  }
36
36
  }
37
- function refreshMatcher(matcher, db) {
38
- if (matcher)
39
- matcher.load(db);
40
- }
41
37
  export const adminRetryRuleRoutes = (app, options, done) => {
42
- const { db, matcher } = options;
38
+ const { db, stateRegistry } = options;
43
39
  app.get("/admin/api/retry-rules", async (_request, reply) => {
44
40
  const rules = getAllRetryRules(db);
45
41
  return reply.send(rules);
@@ -60,7 +56,7 @@ export const adminRetryRuleRoutes = (app, options, done) => {
60
56
  max_retries: body.max_retries ?? DEFAULT_MAX_RETRIES,
61
57
  max_delay_ms: body.max_delay_ms ?? DEFAULT_MAX_DELAY_MS,
62
58
  });
63
- refreshMatcher(matcher, db);
59
+ stateRegistry?.refreshRetryRules();
64
60
  return reply.code(HTTP_CREATED).send({ id });
65
61
  });
66
62
  app.put("/admin/api/retry-rules/:id", { schema: { body: UpdateRetryRuleSchema } }, async (request, reply) => {
@@ -89,7 +85,7 @@ export const adminRetryRuleRoutes = (app, options, done) => {
89
85
  if (body.max_delay_ms !== undefined)
90
86
  fields.max_delay_ms = body.max_delay_ms;
91
87
  updateRetryRule(db, id, fields);
92
- refreshMatcher(matcher, db);
88
+ stateRegistry?.refreshRetryRules();
93
89
  return reply.send({ success: true });
94
90
  });
95
91
  app.delete("/admin/api/retry-rules/:id", async (request, reply) => {
@@ -98,7 +94,7 @@ export const adminRetryRuleRoutes = (app, options, done) => {
98
94
  if (!existing)
99
95
  return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Retry rule not found"));
100
96
  deleteRetryRule(db, id);
101
- refreshMatcher(matcher, db);
97
+ stateRegistry?.refreshRetryRules();
102
98
  return reply.send({ success: true });
103
99
  });
104
100
  done();
@@ -1,15 +1,15 @@
1
1
  import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
- import { RetryRuleMatcher } from "../proxy/retry-rules.js";
3
+ import type { StateRegistry } from "../core/registry.js";
4
4
  import type { RequestTracker } from "../monitor/request-tracker.js";
5
- import { ProviderSemaphoreManager } from "../proxy/semaphore.js";
6
5
  import type { AdaptiveConcurrencyController } from "../proxy/adaptive-controller.js";
7
6
  interface AdminRoutesOptions {
8
7
  db: Database.Database;
9
- matcher: RetryRuleMatcher | null;
8
+ stateRegistry: StateRegistry;
10
9
  tracker?: RequestTracker;
11
- semaphoreManager?: ProviderSemaphoreManager;
12
10
  adaptiveController?: AdaptiveConcurrencyController;
11
+ logFileWriter?: import("../storage/log-file-writer.js").LogFileWriter | null;
12
+ logsDir?: string;
13
13
  }
14
14
  export declare const adminRoutes: FastifyPluginCallback<AdminRoutesOptions>;
15
15
  export {};
@@ -21,19 +21,19 @@ export const adminRoutes = (app, options, done) => {
21
21
  app.register(adminSetupRoutes, { db: options.db });
22
22
  app.register(adminAuthPlugin, { db: options.db });
23
23
  app.register(adminLoginRoutes, { db: options.db });
24
- app.register(adminProviderRoutes, { db: options.db, semaphoreManager: options.semaphoreManager, tracker: options.tracker, adaptiveController: options.adaptiveController });
24
+ app.register(adminProviderRoutes, { db: options.db, stateRegistry: options.stateRegistry, tracker: options.tracker, adaptiveController: options.adaptiveController });
25
25
  app.register(adminMappingRoutes, { db: options.db });
26
26
  app.register(adminGroupRoutes, { db: options.db });
27
27
  app.register(adminScheduleRoutes, { db: options.db });
28
- app.register(adminRetryRuleRoutes, { db: options.db, matcher: options.matcher });
29
- app.register(adminLogRoutes, { db: options.db });
28
+ app.register(adminRetryRuleRoutes, { db: options.db, stateRegistry: options.stateRegistry });
29
+ app.register(adminLogRoutes, { db: options.db, logFileWriter: options.logFileWriter });
30
30
  app.register(adminRouterKeyRoutes, { db: options.db });
31
31
  app.register(adminStatsRoutes, { db: options.db });
32
32
  app.register(adminMetricsRoutes, { db: options.db });
33
- app.register(adminProxyEnhancementRoutes, { db: options.db });
33
+ app.register(adminProxyEnhancementRoutes, { db: options.db, stateRegistry: options.stateRegistry });
34
34
  app.register(adminMonitorRoutes, { tracker: options.tracker });
35
- app.register(adminSettingsRoutes, { db: options.db });
36
- app.register(adminImportExportRoutes, { db: options.db, matcher: options.matcher, semaphoreManager: options.semaphoreManager });
35
+ app.register(adminSettingsRoutes, { db: options.db, logsDir: options.logsDir });
36
+ app.register(adminImportExportRoutes, { db: options.db, stateRegistry: options.stateRegistry });
37
37
  app.register(adminRecommendedRoutes, { db: options.db });
38
38
  app.register(adminUsageRoutes, { db: options.db });
39
39
  app.register(adminUpgradeRoutes, { db: options.db });
@@ -1,11 +1,9 @@
1
1
  import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
- import { RetryRuleMatcher } from "../proxy/retry-rules.js";
4
- import { ProviderSemaphoreManager } from "../proxy/semaphore.js";
3
+ import type { StateRegistry } from "../core/registry.js";
5
4
  interface ImportExportOptions {
6
5
  db: Database.Database;
7
- matcher: RetryRuleMatcher | null;
8
- semaphoreManager?: ProviderSemaphoreManager;
6
+ stateRegistry: StateRegistry;
9
7
  }
10
8
  export declare const adminImportExportRoutes: FastifyPluginCallback<ImportExportOptions>;
11
9
  export {};
@@ -1,8 +1,6 @@
1
1
  import { createHash } from "crypto";
2
- import { getAllProviders, PROVIDER_CONCURRENCY_DEFAULTS } from "../db/index.js";
3
2
  import { encrypt, decrypt } from "../utils/crypto.js";
4
3
  import { getSetting } from "../db/settings.js";
5
- import { modelState } from "../proxy/model-state.js";
6
4
  import { API_CODE, apiError } from "./api-response.js";
7
5
  const CONFIG_TABLES = [
8
6
  "providers",
@@ -11,6 +9,9 @@ const CONFIG_TABLES = [
11
9
  "router_keys",
12
10
  "settings",
13
11
  "session_model_states",
12
+ "schedules",
13
+ "provider_model_info",
14
+ "session_model_history",
14
15
  ];
15
16
  // settings 表按 key 列的值过滤,不覆盖本地安全敏感配置
16
17
  const PROTECTED_SETTING_KEYS = new Set(["admin_password_hash", "jwt_secret", "encryption_key"]);
@@ -19,7 +20,7 @@ const ISO_DATE_LENGTH = 10;
19
20
  const BAD_REQUEST = 400;
20
21
  const KEY_PREFIX_LENGTH = 8;
21
22
  export const adminImportExportRoutes = (app, options, done) => {
22
- const { db, matcher, semaphoreManager } = options;
23
+ const { db, stateRegistry } = options;
23
24
  app.get("/admin/api/settings/export", async (_request, reply) => {
24
25
  const encryptionKey = getSetting(db, "encryption_key");
25
26
  const data = {};
@@ -123,22 +124,11 @@ export const adminImportExportRoutes = (app, options, done) => {
123
124
  db.pragma(`foreign_keys = ${prevFk}`);
124
125
  })();
125
126
  // 导入成功后刷新内存缓存
126
- if (matcher)
127
- matcher.load(db);
128
- if (semaphoreManager) {
129
- // 清除旧的 semaphore 配置,按导入后的 providers 表重建
130
- semaphoreManager.removeAll();
131
- const providers = getAllProviders(db);
132
- for (const p of providers) {
133
- semaphoreManager.updateConfig(p.id, {
134
- maxConcurrency: p.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
135
- queueTimeoutMs: p.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
136
- maxQueueSize: p.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
137
- });
138
- }
139
- }
140
- // session_model_states 已通过 DB 导入,内存缓存会在读取时自然回填
141
- modelState.clearAll();
127
+ stateRegistry.refreshRetryRules();
128
+ // 清除旧的 semaphore/adaptive/tracker 配置,按导入后的 DB 数据全量重建
129
+ stateRegistry.removeAllProviders();
130
+ stateRegistry.clearModelState();
131
+ stateRegistry.reinitializeProviders();
142
132
  return reply.send(counts);
143
133
  });
144
134
  done();
@@ -2,6 +2,7 @@ import { FastifyPluginCallback } from "fastify";
2
2
  import Database from "better-sqlite3";
3
3
  interface SettingsOptions {
4
4
  db: Database.Database;
5
+ logsDir?: string;
5
6
  }
6
7
  export declare const adminSettingsRoutes: FastifyPluginCallback<SettingsOptions>;
7
8
  export {};
@@ -1,8 +1,10 @@
1
+ import { statSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
1
3
  import { getLogRetentionDays, setLogRetentionDays, getDbMaxSizeMb, setDbMaxSizeMb, getLogTableMaxSizeMb, setLogTableMaxSizeMb, getSetting, } from "../db/settings.js";
2
4
  import { HTTP_BAD_REQUEST } from "./constants.js";
3
5
  import { API_CODE, apiError } from "./api-response.js";
4
6
  export const adminSettingsRoutes = (app, options, done) => {
5
- const { db } = options;
7
+ const { db, logsDir } = options;
6
8
  app.get("/admin/api/settings/log-retention", async () => {
7
9
  return { days: getLogRetentionDays(db) };
8
10
  });
@@ -25,8 +27,17 @@ export const adminSettingsRoutes = (app, options, done) => {
25
27
  }
26
28
  catch { /* eslint-disable-line taste/no-silent-catch -- 损坏的缓存值,回退默认 */ }
27
29
  }
30
+ // 计算日志文件目录大小
31
+ let logFileBytes = 0;
32
+ if (logsDir) {
33
+ try {
34
+ logFileBytes = calcDirSize(logsDir);
35
+ }
36
+ catch { /* eslint-disable-line taste/no-silent-catch -- 目录可能不存在 */ }
37
+ }
28
38
  return {
29
39
  ...sizeInfo,
40
+ logFileBytes,
30
41
  thresholds: {
31
42
  dbMaxSizeMb: getDbMaxSizeMb(db),
32
43
  logTableMaxSizeMb: getLogTableMaxSizeMb(db),
@@ -54,3 +65,20 @@ export const adminSettingsRoutes = (app, options, done) => {
54
65
  });
55
66
  done();
56
67
  };
68
+ /** 递归计算目录下所有文件的总大小(字节) */
69
+ function calcDirSize(dirPath) {
70
+ let total = 0;
71
+ for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
72
+ const fullPath = join(dirPath, entry.name);
73
+ if (entry.isDirectory()) {
74
+ total += calcDirSize(fullPath);
75
+ }
76
+ else if (entry.isFile()) {
77
+ try {
78
+ total += statSync(fullPath).size;
79
+ }
80
+ catch { /* 文件可能刚被删除 */ }
81
+ }
82
+ }
83
+ return total;
84
+ }
@@ -5,7 +5,7 @@ import { reloadConfig } from '../config/recommended.js';
5
5
  import { execSync } from 'node:child_process';
6
6
  import fs from 'node:fs';
7
7
  import path from 'node:path';
8
- import { HTTP_BAD_REQUEST, HTTP_INTERNAL_ERROR } from '../constants.js';
8
+ import { HTTP_BAD_REQUEST, HTTP_INTERNAL_ERROR } from '../core/constants.js';
9
9
  import { API_CODE, apiError } from './api-response.js';
10
10
  const GITHUB_CONFIG_BASE = 'https://raw.githubusercontent.com/zhushanwen321/llm-simple-router/main/config';
11
11
  const GITEE_CONFIG_BASE = 'https://gitee.com/zzzzswszzzz/llm-simple-router/raw/main/config';
@@ -3,10 +3,13 @@ export declare const HTTP_CREATED = 201;
3
3
  export declare const HTTP_FORBIDDEN = 403;
4
4
  export declare const HTTP_NOT_FOUND = 404;
5
5
  export declare const HTTP_CONFLICT = 409;
6
+ export declare const HTTP_UNPROCESSABLE_ENTITY = 422;
6
7
  export declare const HTTP_INTERNAL_ERROR = 500;
7
8
  export declare const HTTP_BAD_GATEWAY = 502;
8
- export declare const HTTP_UNPROCESSABLE_ENTITY = 422;
9
9
  export declare const HTTP_SERVICE_UNAVAILABLE = 503;
10
10
  export declare const PROXY_API_TYPES: Record<string, string>;
11
11
  export declare function getProxyApiType(url: string): string | null;
12
12
  export declare const MS_PER_SECOND = 1000;
13
+ export declare const UPSTREAM_SUCCESS = 200;
14
+ /** 过滤掉不应转发给下游的 hop-by-hop headers */
15
+ export declare function filterHeaders(raw: import("./types.js").RawHeaders): Record<string, string>;
@@ -1,12 +1,13 @@
1
+ // src/core/constants.ts
1
2
  // HTTP 状态码常量 — 全局唯一来源
2
3
  export const HTTP_BAD_REQUEST = 400;
3
4
  export const HTTP_CREATED = 201;
4
5
  export const HTTP_FORBIDDEN = 403;
5
6
  export const HTTP_NOT_FOUND = 404;
6
7
  export const HTTP_CONFLICT = 409;
8
+ export const HTTP_UNPROCESSABLE_ENTITY = 422;
7
9
  export const HTTP_INTERNAL_ERROR = 500;
8
10
  export const HTTP_BAD_GATEWAY = 502;
9
- export const HTTP_UNPROCESSABLE_ENTITY = 422;
10
11
  export const HTTP_SERVICE_UNAVAILABLE = 503;
11
12
  // api_type 路由映射:proxy path → api type,用于全局 hook/errorHandler 中识别代理请求
12
13
  export const PROXY_API_TYPES = {
@@ -19,3 +20,22 @@ export function getProxyApiType(url) {
19
20
  return PROXY_API_TYPES[path] ?? null;
20
21
  }
21
22
  export const MS_PER_SECOND = 1000;
23
+ // 上游成功状态码
24
+ export const UPSTREAM_SUCCESS = 200;
25
+ /** 过滤掉不应转发给下游的 hop-by-hop headers */
26
+ const SKIP_DOWNSTREAM = new Set([
27
+ "content-length",
28
+ "transfer-encoding",
29
+ "connection",
30
+ "keep-alive",
31
+ ]);
32
+ /** 过滤掉不应转发给下游的 hop-by-hop headers */
33
+ export function filterHeaders(raw) {
34
+ const out = {};
35
+ for (const [key, value] of Object.entries(raw)) {
36
+ if (value == null || SKIP_DOWNSTREAM.has(key.toLowerCase()))
37
+ continue;
38
+ out[key] = Array.isArray(value) ? value.join(", ") : value;
39
+ }
40
+ return out;
41
+ }
@@ -0,0 +1,31 @@
1
+ /** 服务键常量 — 避免魔法字符串,编译期类型保护 */
2
+ export declare const SERVICE_KEYS: {
3
+ readonly db: "db";
4
+ readonly matcher: "matcher";
5
+ readonly semaphoreManager: "semaphoreManager";
6
+ readonly tracker: "tracker";
7
+ readonly usageWindowTracker: "usageWindowTracker";
8
+ readonly sessionTracker: "sessionTracker";
9
+ readonly adaptiveController: "adaptiveController";
10
+ readonly logFileWriter: "logFileWriter";
11
+ };
12
+ export type ServiceKey = (typeof SERVICE_KEYS)[keyof typeof SERVICE_KEYS];
13
+ /**
14
+ * 轻量服务容器 — 懒加载单例工厂注册表。
15
+ *
16
+ * 用法:
17
+ * const c = new ServiceContainer();
18
+ * c.register(SERVICE_KEYS.db, () => db);
19
+ * c.register(SERVICE_KEYS.tracker, (c) => new RequestTracker(c.resolve(SERVICE_KEYS.db)));
20
+ * const tracker = c.resolve<RequestTracker>(SERVICE_KEYS.tracker);
21
+ *
22
+ * 注册的工厂最多执行一次(惰性求值 + 缓存)。
23
+ */
24
+ export declare class ServiceContainer {
25
+ private readonly factories;
26
+ private readonly cache;
27
+ /** 注册服务工厂。重复注册同一 key 会覆盖(但已缓存的实例不会被清除)。 */
28
+ register<T>(key: string, factory: (c: ServiceContainer) => T): void;
29
+ /** 获取服务实例。首次调用时执行工厂并缓存。 */
30
+ resolve<T>(key: string): T;
31
+ }
@@ -0,0 +1,41 @@
1
+ /** 服务键常量 — 避免魔法字符串,编译期类型保护 */
2
+ export const SERVICE_KEYS = {
3
+ db: "db",
4
+ matcher: "matcher",
5
+ semaphoreManager: "semaphoreManager",
6
+ tracker: "tracker",
7
+ usageWindowTracker: "usageWindowTracker",
8
+ sessionTracker: "sessionTracker",
9
+ adaptiveController: "adaptiveController",
10
+ logFileWriter: "logFileWriter",
11
+ };
12
+ /**
13
+ * 轻量服务容器 — 懒加载单例工厂注册表。
14
+ *
15
+ * 用法:
16
+ * const c = new ServiceContainer();
17
+ * c.register(SERVICE_KEYS.db, () => db);
18
+ * c.register(SERVICE_KEYS.tracker, (c) => new RequestTracker(c.resolve(SERVICE_KEYS.db)));
19
+ * const tracker = c.resolve<RequestTracker>(SERVICE_KEYS.tracker);
20
+ *
21
+ * 注册的工厂最多执行一次(惰性求值 + 缓存)。
22
+ */
23
+ export class ServiceContainer {
24
+ factories = new Map();
25
+ cache = new Map();
26
+ /** 注册服务工厂。重复注册同一 key 会覆盖(但已缓存的实例不会被清除)。 */
27
+ register(key, factory) {
28
+ this.factories.set(key, factory);
29
+ }
30
+ /** 获取服务实例。首次调用时执行工厂并缓存。 */
31
+ resolve(key) {
32
+ if (this.cache.has(key))
33
+ return this.cache.get(key);
34
+ const factory = this.factories.get(key);
35
+ if (!factory)
36
+ throw new Error(`Service not registered: "${key}"`);
37
+ const instance = factory(this);
38
+ this.cache.set(key, instance);
39
+ return instance;
40
+ }
41
+ }
@@ -0,0 +1,26 @@
1
+ import type { TransportResult, ResilienceAttempt } from "./types.js";
2
+ /**
3
+ * Provider 并发队列已满时抛出。
4
+ */
5
+ export declare class SemaphoreQueueFullError extends Error {
6
+ readonly providerId: string;
7
+ constructor(providerId: string);
8
+ }
9
+ /**
10
+ * Provider 并发等待超时时抛出。
11
+ */
12
+ export declare class SemaphoreTimeoutError extends Error {
13
+ readonly providerId: string;
14
+ readonly timeoutMs: number;
15
+ constructor(providerId: string, timeoutMs: number);
16
+ }
17
+ /**
18
+ * 跨 provider failover 时由 ResilienceLayer 抛出,
19
+ * orchestrator 捕获后释放当前信号量并获取新 provider 的信号量。
20
+ */
21
+ export declare class ProviderSwitchNeeded extends Error {
22
+ readonly targetProviderId: string;
23
+ readonly attempts?: ResilienceAttempt[] | undefined;
24
+ readonly lastResult?: TransportResult | undefined;
25
+ constructor(targetProviderId: string, attempts?: ResilienceAttempt[] | undefined, lastResult?: TransportResult | undefined);
26
+ }
@@ -0,0 +1,42 @@
1
+ // src/core/errors.ts
2
+ // 被多目录共享的错误类型(从 proxy/semaphore.ts 和 proxy/types.ts 移出)
3
+ /**
4
+ * Provider 并发队列已满时抛出。
5
+ */
6
+ export class SemaphoreQueueFullError extends Error {
7
+ providerId;
8
+ constructor(providerId) {
9
+ super(`Provider '${providerId}' concurrency queue is full`);
10
+ this.providerId = providerId;
11
+ this.name = "SemaphoreQueueFullError";
12
+ }
13
+ }
14
+ /**
15
+ * Provider 并发等待超时时抛出。
16
+ */
17
+ export class SemaphoreTimeoutError extends Error {
18
+ providerId;
19
+ timeoutMs;
20
+ constructor(providerId, timeoutMs) {
21
+ super(`Provider '${providerId}' concurrency wait timeout (${timeoutMs}ms)`);
22
+ this.providerId = providerId;
23
+ this.timeoutMs = timeoutMs;
24
+ this.name = "SemaphoreTimeoutError";
25
+ }
26
+ }
27
+ /**
28
+ * 跨 provider failover 时由 ResilienceLayer 抛出,
29
+ * orchestrator 捕获后释放当前信号量并获取新 provider 的信号量。
30
+ */
31
+ export class ProviderSwitchNeeded extends Error {
32
+ targetProviderId;
33
+ attempts;
34
+ lastResult;
35
+ constructor(targetProviderId, attempts, lastResult) {
36
+ super(`Provider switch needed: ${targetProviderId}`);
37
+ this.targetProviderId = targetProviderId;
38
+ this.attempts = attempts;
39
+ this.lastResult = lastResult;
40
+ this.name = "ProviderSwitchNeeded";
41
+ }
42
+ }