llm-simple-router 0.3.6 → 0.4.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 (126) hide show
  1. package/README.md +81 -49
  2. package/dist/admin/constants.d.ts +1 -8
  3. package/dist/admin/constants.js +2 -8
  4. package/dist/admin/logs.js +18 -3
  5. package/dist/admin/router-keys.js +1 -2
  6. package/dist/cli.js +0 -0
  7. package/dist/constants.d.ts +8 -0
  8. package/dist/constants.js +9 -0
  9. package/dist/db/index.d.ts +4 -4
  10. package/dist/db/index.js +2 -2
  11. package/dist/db/logs.d.ts +18 -33
  12. package/dist/db/logs.js +40 -17
  13. package/dist/db/metrics.d.ts +33 -0
  14. package/dist/db/metrics.js +7 -0
  15. package/dist/db/migrations/018_add_failover_field.sql +2 -0
  16. package/dist/db/retry-rules.d.ts +2 -2
  17. package/dist/db/retry-rules.js +26 -13
  18. package/dist/index.js +3 -5
  19. package/dist/monitor/request-tracker.d.ts +6 -0
  20. package/dist/monitor/request-tracker.js +23 -54
  21. package/dist/monitor/stream-extractor.d.ts +11 -0
  22. package/dist/monitor/stream-extractor.js +51 -0
  23. package/dist/proxy/anthropic.js +19 -32
  24. package/dist/proxy/log-helpers.d.ts +11 -4
  25. package/dist/proxy/log-helpers.js +5 -3
  26. package/dist/proxy/openai.js +18 -34
  27. package/dist/proxy/orchestrator.d.ts +52 -0
  28. package/dist/proxy/orchestrator.js +100 -0
  29. package/dist/proxy/proxy-core.d.ts +14 -26
  30. package/dist/proxy/proxy-core.js +40 -337
  31. package/dist/proxy/proxy-handler.d.ts +18 -0
  32. package/dist/proxy/proxy-handler.js +223 -0
  33. package/dist/proxy/proxy-logging.d.ts +28 -0
  34. package/dist/proxy/proxy-logging.js +122 -0
  35. package/dist/proxy/resilience.d.ts +63 -0
  36. package/dist/proxy/resilience.js +188 -0
  37. package/dist/proxy/scope.d.ts +18 -0
  38. package/dist/proxy/scope.js +37 -0
  39. package/dist/proxy/semaphore.d.ts +9 -2
  40. package/dist/proxy/semaphore.js +34 -7
  41. package/dist/proxy/stream-proxy.d.ts +7 -0
  42. package/dist/proxy/stream-proxy.js +263 -0
  43. package/dist/proxy/{upstream-call.d.ts → transport.d.ts} +25 -18
  44. package/dist/proxy/transport.js +128 -0
  45. package/dist/proxy/types.d.ts +58 -0
  46. package/dist/proxy/types.js +30 -0
  47. package/frontend-dist/assets/CardContent-CTnwqTdL.js +1 -0
  48. package/frontend-dist/assets/{CardHeader-BBKKDXEh.js → CardHeader-CfUeY7tk.js} +1 -1
  49. package/frontend-dist/assets/CardTitle-CWiDwWqd.js +1 -0
  50. package/frontend-dist/assets/Checkbox-BxNz70R_.js +1 -0
  51. package/frontend-dist/assets/CollapsibleTrigger-Uz1aGdtH.js +1 -0
  52. package/frontend-dist/assets/Collection-1EHC87X5.js +3 -0
  53. package/frontend-dist/assets/Dashboard-C3FL30UN.js +3 -0
  54. package/frontend-dist/assets/DialogTitle-CAOFxr83.js +1 -0
  55. package/frontend-dist/assets/Input-DRIid2C6.js +1 -0
  56. package/frontend-dist/assets/Label-UyNN2jyE.js +1 -0
  57. package/frontend-dist/assets/LogDetailDialog-8BT4vIlV.js +3 -0
  58. package/frontend-dist/assets/Login-CnzH6TdS.js +1 -0
  59. package/frontend-dist/assets/Logs-CbK8NB_X.js +1 -0
  60. package/frontend-dist/assets/ModelMappings-DeRFgsYG.js +1 -0
  61. package/frontend-dist/assets/Monitor-Dd80bdUn.js +1 -0
  62. package/frontend-dist/assets/PopperContent-B3fZao7v.js +1 -0
  63. package/frontend-dist/assets/Providers-B_DbV-_y.js +1 -0
  64. package/frontend-dist/assets/ProxyEnhancement-up1fnPzq.js +5 -0
  65. package/frontend-dist/assets/RetryRules-Dkuhjh0u.js +1 -0
  66. package/frontend-dist/assets/RouterKeys-CvMMAa4t.js +1 -0
  67. package/frontend-dist/assets/RovingFocusItem-X0bfqWWS.js +1 -0
  68. package/frontend-dist/assets/SelectValue-zO8t-tx1.js +1 -0
  69. package/frontend-dist/assets/Setup-ByT2ThOQ.js +1 -0
  70. package/frontend-dist/assets/Switch-BEMjVugO.js +1 -0
  71. package/frontend-dist/assets/TableHeader-DpHWSnxK.js +1 -0
  72. package/frontend-dist/assets/TabsTrigger-Db6RqsZc.js +1 -0
  73. package/frontend-dist/assets/VisuallyHidden-hs8pj8OP.js +1 -0
  74. package/frontend-dist/assets/VisuallyHiddenInput-1m0nNADN.js +1 -0
  75. package/frontend-dist/assets/alert-dialog-PP91kaO8.js +1 -0
  76. package/frontend-dist/assets/button-Dcc0gF5i.js +1 -0
  77. package/frontend-dist/assets/{client-BBW9-06a.js → client-DIIo9zPK.js} +2 -2
  78. package/frontend-dist/assets/{createLucideIcon-CCI4wMy0.js → createLucideIcon-DGZkBjcJ.js} +1 -1
  79. package/frontend-dist/assets/dialog-CxSyR-fN.js +1 -0
  80. package/frontend-dist/assets/format-CPdJtjZ5.js +1 -0
  81. package/frontend-dist/assets/index-BL-LAtac.css +1 -0
  82. package/frontend-dist/assets/index-CvT41fGL.js +1 -0
  83. package/frontend-dist/assets/lib-Bl0OuBjh.js +1 -0
  84. package/frontend-dist/assets/{ohash.D__AXeF1-Bj3Sy1wQ.js → ohash.D__AXeF1-B64hB831.js} +1 -1
  85. package/frontend-dist/assets/{useClipboard-XyA4kDfF.js → useClipboard-CWc1cTDo.js} +1 -1
  86. package/frontend-dist/assets/useForwardExpose-AkE0lq8y.js +1 -0
  87. package/frontend-dist/assets/useNonce-DGyPxdjq.js +1 -0
  88. package/frontend-dist/assets/x-BuUpx9Fr.js +1 -0
  89. package/frontend-dist/index.html +7 -7
  90. package/package.json +1 -1
  91. package/dist/admin/services.d.ts +0 -7
  92. package/dist/admin/services.js +0 -63
  93. package/dist/proxy/retry.d.ts +0 -43
  94. package/dist/proxy/retry.js +0 -121
  95. package/dist/proxy/upstream-call.js +0 -208
  96. package/frontend-dist/assets/CardContent-DKzAH8lX.js +0 -1
  97. package/frontend-dist/assets/CardTitle-BQtpsfYd.js +0 -1
  98. package/frontend-dist/assets/Checkbox-DhBbPKjw.js +0 -1
  99. package/frontend-dist/assets/CollapsibleTrigger-BC4bE5yr.js +0 -1
  100. package/frontend-dist/assets/Dashboard-3z77m9VQ.js +0 -3
  101. package/frontend-dist/assets/DialogTitle-Bh2A7j2j.js +0 -1
  102. package/frontend-dist/assets/Input-C5_w9X6Y.js +0 -1
  103. package/frontend-dist/assets/Label-9sUKofNb.js +0 -1
  104. package/frontend-dist/assets/Login-Dxo1j9ZV.js +0 -1
  105. package/frontend-dist/assets/Logs-KthJmRch.js +0 -3
  106. package/frontend-dist/assets/ModelMappings-Dci1SkBO.js +0 -1
  107. package/frontend-dist/assets/PopperContent-DGr-wo47.js +0 -1
  108. package/frontend-dist/assets/Providers-wZxNIXXh.js +0 -1
  109. package/frontend-dist/assets/ProxyEnhancement-CcvyXQNb.js +0 -1
  110. package/frontend-dist/assets/RetryRules-B9Cw-Ycd.js +0 -1
  111. package/frontend-dist/assets/RouterKeys-yZrj5YNm.js +0 -1
  112. package/frontend-dist/assets/RovingFocusItem-CvMnUs02.js +0 -1
  113. package/frontend-dist/assets/SelectValue-DhqRtJKk.js +0 -1
  114. package/frontend-dist/assets/Setup-DPk7lIZy.js +0 -1
  115. package/frontend-dist/assets/TableHeader-D9I1uQTp.js +0 -1
  116. package/frontend-dist/assets/TabsTrigger-D4xjbMaQ.js +0 -1
  117. package/frontend-dist/assets/VisuallyHiddenInput-Dr8wp-H0.js +0 -1
  118. package/frontend-dist/assets/alert-dialog-ChPy9vB2.js +0 -1
  119. package/frontend-dist/assets/badge-HGT44FNA.js +0 -3
  120. package/frontend-dist/assets/button-B3kgf-D2.js +0 -1
  121. package/frontend-dist/assets/dialog-BFhbK4vw.js +0 -1
  122. package/frontend-dist/assets/index-DSrFEJ7Y.css +0 -1
  123. package/frontend-dist/assets/index-SUCErp6B.js +0 -1
  124. package/frontend-dist/assets/lib-D-4ywYag.js +0 -1
  125. package/frontend-dist/assets/useForwardExpose-CIZH3-CG.js +0 -1
  126. package/frontend-dist/assets/x-CTNEl6Fz.js +0 -1
package/README.md CHANGED
@@ -9,23 +9,18 @@
9
9
 
10
10
  个人使用 Claude Code 配合国产模型时的实际痛点:
11
11
 
12
- - **自动重试** — 国产模型限流、网络错误频繁,对可恢复错误(429/500/网络超时)自动指数退避重试
13
- - **多供应商模型映射**高峰期主模型不可用时,将 claude-opus 映射到 GLM,claude-sonnet 映射到 Kimi 等,低谷期再切回来
14
- - **多密钥隔离**为不同使用方分配独立密钥,按密钥筛选日志和性能指标
12
+ - **自动重试** — 国产模型限流、网络错误频繁,对可恢复错误(429/400/网络超时)自动指数退避重试。默认已经针对智谱模型进行了配置,开箱即用。
13
+ - **多供应商支持**支持智谱、Moonshot、Minimax、火山引擎、阿里云、腾讯云等,Coding Plan 选择后会自动填写,只需要配置 API Key。也可以完全自定义。
14
+ - **模型分时段映射**可以每天分时段自动切换模型。以我自己使用体验来说,平时将 sonnet 映射到 glm-5.1 ,14-18点将 sonnet 映射到 kimi ,减少智谱高峰期限流错误和三倍消耗。
15
+ - **并发队列等待** — 不同 Provider 可以配置并发数限制,超过限制的请求会进入队列等待。解决 Claude Code 多个subagent并行执行时经常触发限流失败的问题。不过需要配置 claude code 的 API_TIMEOUT_MS 为一个比较大的值。这个功能可以让使用 Claude Code 体验更好,但不能根本性解决限流问题。
16
+ - **实时请求监控** — 实时监控活跃请求、队列情况,可以实时查看流式请求的输出并结构化展示(目前仅支持 Claude Code )。
15
17
 
16
- ## 功能
18
+ ## 其他功能
17
19
 
18
20
  | 功能 | 说明 |
19
21
  |------|------|
20
- | 模型映射 | 客户端模型名 -> 后端模型名 + 供应商,支持分组和优先级 |
21
- | 自动重试 | 429/500/网络错误自动重试,指数退避,可配置次数和间隔 |
22
- | 多供应商 | 配置多个后端供应商,按模型映射路由 |
23
- | 多密钥 (Router Keys) | 为不同使用方创建独立密钥,支持模型白名单 |
24
- | 流式代理 | 完整支持 SSE 流式和非流式请求 |
25
- | 供应商并发控制 | 按 Provider 维度限制并发数、队列长度和超时,防止单一供应商过载 |
26
- | 实时监控 | SSE 推送活跃请求、延迟热力图、Token 吞吐、运行时资源指标 |
27
- | 代理增强 (实验性) | 注入系统指令、会话记忆、模型锁定等增强功能 |
28
- | 管理后台 | Vue 3 + shadcn-vue Web UI,管理供应商、映射、密钥 |
22
+ | 多密钥 | 为不同使用方创建独立密钥,支持模型白名单 |
23
+ | 代理增强 (实验性) | 支持通过 Claude Code 发送select-model指令直接变更 router 的模型,不经过 LLM 请求(实验中,可能有bug) |
29
24
  | 请求日志 | 结构化展示完整四阶段链路(客户端请求/上游请求/上游响应/客户端响应),适配 Claude Code 请求格式 |
30
25
  | 性能指标 | TTFT、吞吐量、Token 用量、缓存命中率,支持按模型/密钥筛选 |
31
26
 
@@ -33,39 +28,89 @@
33
28
 
34
29
  ## 管理后台预览
35
30
 
36
- | Dashboard | Provider 管理 |
37
- |-----------|-------------|
38
- | ![Dashboard](docs/screenshot/dashboard.png) | ![Provider](docs/screenshot/provider.png) |
31
+ | Provider 管理和并发控制 |
32
+ |-----------|
33
+ | ![Provider Concurrency](docs/screenshot/provider_concurrency.png) |
39
34
 
40
- | 供应商并发控制 | 实时监控 |
35
+ | 实时监控 |
36
+ |-----------------|
37
+ | ![Monitor](docs/screenshot/monitor.png) |
38
+
39
+ | 模型映射 |
40
+ |---------|
41
+ | ![Model Mapping](docs/screenshot/model_mapping.png) |
42
+
43
+ | 重试规则 |
44
+ |---------|
45
+ | ![Retry](docs/screenshot/retry.png) |
46
+
47
+ | Dashboard | 请求日志 |
41
48
  |--------------|---------|
42
- | ![Provider Concurrency](docs/screenshot/provider_concurrency.png) | ![Monitor](docs/screenshot/monitor.png) |
49
+ | ![Dashboard](docs/screenshot/dashboard.png) | ![Logs](docs/screenshot/log.png) |
43
50
 
44
51
  | 代理增强 (实验性) |
45
52
  |-----------------|
46
53
  | ![Proxy Enhancement](docs/screenshot/proxy_enhance.png) |
47
54
 
48
- | 模型映射 | 重试规则 |
49
- |---------|--------|
50
- | ![Model Mapping](docs/screenshot/model_mapping.png) | ![Retry](docs/screenshot/retry.png) |
51
-
52
- | 请求日志 |
53
- |---------|
54
- | ![Logs](docs/screenshot/log.png) |
55
-
56
55
  ## 工作原理
57
56
 
58
57
  ```
59
- Claude Code -> Router (模型映射 + 自动重试) -> 智谱 GLM / Kimi / 其他供应商
58
+ Claude Code -> Router (模型映射 + 自动重试 + 并发控制) -> 智谱 GLM / Kimi / 其他供应商
60
59
  ```
61
60
 
62
61
  Router 根据模型映射找到后端供应商 -> 转发请求 -> 自动重试失败请求 -> 记录日志和性能指标 -> 返回响应。
63
62
 
64
- ## 典型使用场景
63
+ ### 架构图
64
+
65
+ **系统上下文**([详细说明](docs/system-context.md)):
66
+
67
+ ```mermaid
68
+ graph LR
69
+ Clients["Claude Code / Cursor / 其他客户端"]
70
+ Admin["管理员"]
71
+ Router>"LLM Simple Router"]
72
+ Providers>"智谱 / Moonshot / OpenAI / Anthropic / ..."]
73
+
74
+ Clients -->|"API 请求<br/>Bearer Token"| Router
75
+ Admin -->|"管理后台<br/>/admin/"| Router
76
+ Router -->|"转发请求<br/>SSE 流式"| Providers
77
+ ```
65
78
 
66
- ### Claude Code 配置
79
+ **请求处理流水线**([详细说明](docs/request-pipeline.md)):
67
80
 
68
- 通过环境变量将 Claude Code 指向 Router:
81
+ ```mermaid
82
+ flowchart LR
83
+ A[客户端请求] --> B[认证]
84
+ B --> C[模型映射<br/>+ 路由策略]
85
+ C --> D[并发排队]
86
+ D --> E[调用上游<br/>失败自动重试]
87
+ E --> F[记录日志<br/>+ 指标]
88
+ F --> G[返回响应]
89
+
90
+ E -.->|失败| C
91
+ ```
92
+ Router 收到请求后:认证 → 按映射规则找到后端 Provider → 排队控制并发 → 转发到上游(失败自动重试,Failover 策略下会切换 Provider)→ 记录日志和指标 → 返回响应。
93
+
94
+ ## 快速开始
95
+
96
+ ### npx 启动
97
+ ```bash
98
+ # 一行命令启动
99
+ npx llm-simple-router
100
+ # 访问 http://localhost:9981/admin
101
+ # 首次访问会进入 Setup 页面设置管理员密码
102
+ ```
103
+
104
+ 无需任何环境变量。数据默认存储在 `~/.llm-simple-router/`。
105
+
106
+ ### Docker 部署
107
+
108
+ ```bash
109
+ docker compose up -d
110
+ ```
111
+ ### 创建 Router API 密钥
112
+
113
+ 在管理后台创建一个 API 密钥,原先 Claude Code 密钥替换为 Router 密钥。
69
114
 
70
115
  **方式一:shell alias(推荐)**
71
116
 
@@ -85,7 +130,9 @@ alias clodedev='ANTHROPIC_AUTH_TOKEN="<your-router-key>" ANTHROPIC_BASE_URL="htt
85
130
  }
86
131
  ```
87
132
 
88
- `<your-router-key>` 替换为管理后台中创建的 Router Key。
133
+ ### 管理后台配置 Provider
134
+
135
+ Provider 支持快速配置,目前支持智谱、Moonshot、Minimax、火山引擎、阿里云、腾讯云等,Coding Plan 选择后会自动填写,只需要配置 API Key。
89
136
 
90
137
  ### 管理后台配置模型映射
91
138
 
@@ -96,24 +143,9 @@ alias clodedev='ANTHROPIC_AUTH_TOKEN="<your-router-key>" ANTHROPIC_BASE_URL="htt
96
143
  | sonnet | kimi-for-coding | Moonshot | 14:00-18:00 |
97
144
  | sonnet | glm-5-turbo | 智谱 | / |
98
145
 
99
- 高峰期 GLM 3倍用量,且频繁超限时,将 sonnet 切到 Kimi;低谷期切回 GLM。
100
-
101
- ## 快速开始
102
-
103
- ```bash
104
- # 一行命令启动
105
- npx llm-simple-router
106
- # 访问 http://localhost:9981/admin
107
- # 首次访问会进入 Setup 页面设置管理员密码
108
- ```
109
-
110
- 无需任何环境变量。数据默认存储在 `~/.llm-simple-router/`。
111
-
112
- ## Docker 部署
113
-
114
- ```bash
115
- docker compose up -d
116
- ```
146
+ 客户端模型是指 Claude Code 实际请求发出的模型名。
147
+ 如果你配置了 ANTHROPIC_MODEL 等变量,那应该以该变量为准。
148
+ 个人的配置:高峰期 GLM 3倍用量,且频繁超限时,将 sonnet 切到 Kimi;低谷期切回 GLM。
117
149
 
118
150
  ## 环境变量
119
151
 
@@ -1,10 +1,3 @@
1
1
  import type { FastifyReply } from "fastify";
2
- export declare const HTTP_BAD_REQUEST = 400;
3
- export declare const HTTP_CREATED = 201;
4
- export declare const HTTP_FORBIDDEN = 403;
5
- export declare const HTTP_NOT_FOUND = 404;
6
- export declare const HTTP_CONFLICT = 409;
7
- export declare const HTTP_INTERNAL_ERROR = 500;
8
- export declare const HTTP_BAD_GATEWAY = 502;
9
- export declare const HTTP_SERVICE_UNAVAILABLE = 503;
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";
10
3
  export declare function sendErrorResponse(reply: FastifyReply, statusCode: number, message: string): FastifyReply<import("fastify").RouteGenericInterface, import("fastify").RawServerDefault, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>;
@@ -1,11 +1,5 @@
1
- export const HTTP_BAD_REQUEST = 400;
2
- export const HTTP_CREATED = 201;
3
- export const HTTP_FORBIDDEN = 403;
4
- export const HTTP_NOT_FOUND = 404;
5
- export const HTTP_CONFLICT = 409;
6
- export const HTTP_INTERNAL_ERROR = 500;
7
- export const HTTP_BAD_GATEWAY = 502;
8
- export const HTTP_SERVICE_UNAVAILABLE = 503;
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";
9
3
  export function sendErrorResponse(reply, statusCode, message) {
10
4
  return reply.code(statusCode).send({ error: { message } });
11
5
  }
@@ -1,5 +1,5 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import { getRequestLogs, getRequestLogById, deleteLogsBefore } from "../db/index.js";
2
+ import { getRequestLogs, getRequestLogsGrouped, getRequestLogById, getRequestLogChildren, deleteLogsBefore } from "../db/index.js";
3
3
  import { HTTP_NOT_FOUND } from "./constants.js";
4
4
  const LogQuerySchema = Type.Object({
5
5
  page: Type.Optional(Type.String()),
@@ -7,23 +7,29 @@ const LogQuerySchema = Type.Object({
7
7
  api_type: Type.Optional(Type.String()),
8
8
  model: Type.Optional(Type.String()),
9
9
  router_key_id: Type.Optional(Type.String()),
10
+ view: Type.Optional(Type.Literal("grouped")),
10
11
  });
11
12
  const DeleteLogsBeforeSchema = Type.Object({
12
13
  before: Type.String({ minLength: 1 }),
13
14
  });
15
+ const DEFAULT_LOG_VIEW = "flat";
14
16
  export const adminLogRoutes = (app, options, done) => {
15
17
  const { db } = options;
16
18
  app.get("/admin/api/logs", { schema: { querystring: LogQuerySchema } }, async (request, reply) => {
17
19
  const query = request.query;
18
20
  const page = parseInt(query.page || "1", 10);
19
21
  const limit = parseInt(query.limit || "20", 10);
20
- const result = getRequestLogs(db, {
22
+ const view = query.view || DEFAULT_LOG_VIEW;
23
+ const listOptions = {
21
24
  page,
22
25
  limit,
23
26
  api_type: query.api_type || undefined,
24
27
  model: query.model || undefined,
25
28
  router_key_id: query.router_key_id || undefined,
26
- });
29
+ };
30
+ const result = view === "grouped"
31
+ ? getRequestLogsGrouped(db, listOptions)
32
+ : getRequestLogs(db, listOptions);
27
33
  return reply.send({ ...result, page, limit });
28
34
  });
29
35
  app.get("/admin/api/logs/:id", async (request, reply) => {
@@ -34,6 +40,15 @@ export const adminLogRoutes = (app, options, done) => {
34
40
  }
35
41
  return reply.send(log);
36
42
  });
43
+ app.get("/admin/api/logs/:id/children", async (request, reply) => {
44
+ const params = request.params;
45
+ const parent = getRequestLogById(db, params.id);
46
+ if (!parent) {
47
+ return reply.code(HTTP_NOT_FOUND).send({ error: { message: "Log not found" } });
48
+ }
49
+ const rows = getRequestLogChildren(db, params.id);
50
+ return reply.send({ data: rows });
51
+ });
37
52
  app.delete("/admin/api/logs/before", { schema: { body: DeleteLogsBeforeSchema } }, async (request, reply) => {
38
53
  const body = request.body;
39
54
  const deleted = deleteLogsBefore(db, body.before);
@@ -3,8 +3,7 @@ import { Type } from "@sinclair/typebox";
3
3
  import { encrypt, decrypt } from "../utils/crypto.js";
4
4
  import { getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "../db/index.js";
5
5
  import { getSetting } from "../db/settings.js";
6
- const HTTP_CREATED = 201;
7
- const HTTP_NOT_FOUND = 404;
6
+ import { HTTP_CREATED, HTTP_NOT_FOUND } from "./constants.js";
8
7
  const KEY_RANDOM_BYTES = 32;
9
8
  const KEY_PREFIX_LENGTH = 8;
10
9
  /** 归一化 allowed_models:null/空数组/仅含空字符串 → null(允许所有模型) */
package/dist/cli.js CHANGED
File without changes
@@ -0,0 +1,8 @@
1
+ export declare const HTTP_BAD_REQUEST = 400;
2
+ export declare const HTTP_CREATED = 201;
3
+ export declare const HTTP_FORBIDDEN = 403;
4
+ export declare const HTTP_NOT_FOUND = 404;
5
+ export declare const HTTP_CONFLICT = 409;
6
+ export declare const HTTP_INTERNAL_ERROR = 500;
7
+ export declare const HTTP_BAD_GATEWAY = 502;
8
+ export declare const HTTP_SERVICE_UNAVAILABLE = 503;
@@ -0,0 +1,9 @@
1
+ // HTTP 状态码常量 — 全局唯一来源
2
+ export const HTTP_BAD_REQUEST = 400;
3
+ export const HTTP_CREATED = 201;
4
+ export const HTTP_FORBIDDEN = 403;
5
+ export const HTTP_NOT_FOUND = 404;
6
+ export const HTTP_CONFLICT = 409;
7
+ export const HTTP_INTERNAL_ERROR = 500;
8
+ export const HTTP_BAD_GATEWAY = 502;
9
+ export const HTTP_SERVICE_UNAVAILABLE = 503;
@@ -6,12 +6,12 @@ export { getModelMapping, getAllModelMappings, createModelMapping, updateModelMa
6
6
  export type { ModelMapping, MappingGroup, ProviderModelEntry } from "./mappings.js";
7
7
  export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, seedDefaultRules, } from "./retry-rules.js";
8
8
  export type { RetryRule } from "./retry-rules.js";
9
- export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, insertMetrics, } from "./logs.js";
10
- export type { RequestLog, MetricsRow, MetricsInsert } from "./logs.js";
9
+ export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, } from "./logs.js";
10
+ export type { RequestLog, RequestLogGroupedRow, RequestLogListRow } from "./logs.js";
11
11
  export { getRouterKeyByHash, getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "./router-keys.js";
12
12
  export type { RouterKey } from "./router-keys.js";
13
- export { getMetricsSummary, getMetricsTimeseries } from "./metrics.js";
14
- export type { MetricsSummaryRow, MetricsTimeseriesRow, MetricsPeriod, MetricsMetric } from "./metrics.js";
13
+ export { getMetricsSummary, getMetricsTimeseries, insertMetrics } from "./metrics.js";
14
+ export type { MetricsSummaryRow, MetricsTimeseriesRow, MetricsPeriod, MetricsMetric, MetricsRow, MetricsInsert } from "./metrics.js";
15
15
  export { getStats } from "./stats.js";
16
16
  export type { Stats, StatsPeriod } from "./stats.js";
17
17
  export { getSetting, setSetting, isInitialized } from "./settings.js";
package/dist/db/index.js CHANGED
@@ -39,9 +39,9 @@ export function initDatabase(dbPath) {
39
39
  export { getActiveProviders, getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
40
40
  export { getModelMapping, getAllModelMappings, createModelMapping, updateModelMapping, deleteModelMapping, getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getActiveProviderModels, resolveByProviderModel, } from "./mappings.js";
41
41
  export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, seedDefaultRules, } from "./retry-rules.js";
42
- export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, insertMetrics, } from "./logs.js";
42
+ export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, } from "./logs.js";
43
43
  export { getRouterKeyByHash, getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "./router-keys.js";
44
- export { getMetricsSummary, getMetricsTimeseries } from "./metrics.js";
44
+ export { getMetricsSummary, getMetricsTimeseries, insertMetrics } from "./metrics.js";
45
45
  export { getStats } from "./stats.js";
46
46
  export { getSetting, setSetting, isInitialized } from "./settings.js";
47
47
  export { getSessionStates, getSessionState, getSessionHistory, upsertSessionState, insertSessionHistory, deleteSessionState, } from "./session-states.js";
package/dist/db/logs.d.ts CHANGED
@@ -16,6 +16,7 @@ export interface RequestLog {
16
16
  upstream_response: string | null;
17
17
  client_response: string | null;
18
18
  is_retry: number;
19
+ is_failover: number;
19
20
  original_request_id: string | null;
20
21
  original_model: string | null;
21
22
  }
@@ -24,38 +25,6 @@ export interface RequestLogListRow extends RequestLog {
24
25
  backend_model: string | null;
25
26
  provider_name: string | null;
26
27
  }
27
- export interface MetricsRow {
28
- id: string;
29
- request_log_id: string;
30
- provider_id: string;
31
- backend_model: string;
32
- api_type: string;
33
- input_tokens: number | null;
34
- output_tokens: number | null;
35
- cache_creation_tokens: number | null;
36
- cache_read_tokens: number | null;
37
- ttft_ms: number | null;
38
- total_duration_ms: number | null;
39
- tokens_per_second: number | null;
40
- stop_reason: string | null;
41
- is_complete: number;
42
- created_at: string;
43
- }
44
- export type MetricsInsert = {
45
- request_log_id: string;
46
- provider_id: string;
47
- backend_model: string;
48
- api_type: string;
49
- input_tokens?: number | null;
50
- output_tokens?: number | null;
51
- cache_creation_tokens?: number | null;
52
- cache_read_tokens?: number | null;
53
- ttft_ms?: number | null;
54
- total_duration_ms?: number | null;
55
- tokens_per_second?: number | null;
56
- stop_reason?: string | null;
57
- is_complete?: number;
58
- };
59
28
  export interface RequestLogInsert {
60
29
  id: string;
61
30
  api_type: string;
@@ -73,6 +42,7 @@ export interface RequestLogInsert {
73
42
  upstream_response?: string | null;
74
43
  client_response?: string | null;
75
44
  is_retry?: number;
45
+ is_failover?: number;
76
46
  original_request_id?: string | null;
77
47
  router_key_id?: string | null;
78
48
  original_model?: string | null;
@@ -90,4 +60,19 @@ export declare function getRequestLogs(db: Database.Database, options: {
90
60
  };
91
61
  export declare function getRequestLogById(db: Database.Database, id: string): RequestLog | undefined;
92
62
  export declare function deleteLogsBefore(db: Database.Database, beforeDate: string): number;
93
- export declare function insertMetrics(db: Database.Database, m: MetricsInsert): string;
63
+ /** 查询某条日志的子请求(retry/failover 关联),上限 100 */
64
+ export declare function getRequestLogChildren(db: Database.Database, parentId: string): RequestLogListRow[];
65
+ export interface RequestLogGroupedRow extends RequestLogListRow {
66
+ child_count: number;
67
+ }
68
+ /** 只返回根请求(original_request_id IS NULL),附带子请求数量 */
69
+ export declare function getRequestLogsGrouped(db: Database.Database, options: {
70
+ page: number;
71
+ limit: number;
72
+ api_type?: string;
73
+ model?: string;
74
+ router_key_id?: string;
75
+ }): {
76
+ data: RequestLogGroupedRow[];
77
+ total: number;
78
+ };
package/dist/db/logs.js CHANGED
@@ -1,10 +1,17 @@
1
- import { randomUUID } from "crypto";
1
+ // --- request_logs ---
2
+ /** 三处日志列表查询共享的 SELECT 列 + JOIN 子句 */
3
+ const LOG_LIST_SELECT = `rl.id, rl.api_type, rl.model, rl.provider_id, rl.status_code, rl.latency_ms,
4
+ rl.is_stream, rl.error_message, rl.created_at, rl.is_retry, rl.is_failover, rl.original_request_id, rl.original_model,
5
+ CASE WHEN rl.provider_id = 'router' THEN rl.upstream_request ELSE NULL END AS upstream_request,
6
+ rm.backend_model, COALESCE(p.name, rl.provider_id) AS provider_name`;
7
+ const LOG_LIST_JOIN = `LEFT JOIN request_metrics rm ON rm.request_log_id = rl.id
8
+ LEFT JOIN providers p ON p.id = rl.provider_id`;
2
9
  export function insertRequestLog(db, log) {
3
- db.prepare(`INSERT INTO request_logs (id, api_type, model, provider_id, status_code, latency_ms, is_stream, error_message, created_at, request_body, response_body, client_request, upstream_request, upstream_response, client_response, is_retry, original_request_id, router_key_id, original_model)
4
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(log.id, log.api_type, log.model, log.provider_id, log.status_code, log.latency_ms, log.is_stream, log.error_message, log.created_at, log.request_body ?? null, log.response_body ?? null, log.client_request ?? null, log.upstream_request ?? null, log.upstream_response ?? null, log.client_response ?? null, log.is_retry ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null);
10
+ db.prepare(`INSERT INTO request_logs (id, api_type, model, provider_id, status_code, latency_ms, is_stream, error_message, created_at, request_body, response_body, client_request, upstream_request, upstream_response, client_response, is_retry, is_failover, original_request_id, router_key_id, original_model)
11
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(log.id, log.api_type, log.model, log.provider_id, log.status_code, log.latency_ms, log.is_stream, log.error_message, log.created_at, log.request_body ?? null, log.response_body ?? null, log.client_request ?? null, log.upstream_request ?? null, log.upstream_response ?? null, log.client_response ?? null, log.is_retry ?? 0, log.is_failover ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null);
5
12
  }
6
- export function getRequestLogs(db, options) {
7
- let where = "1=1";
13
+ function buildLogWhereClause(options, baseCondition) {
14
+ let where = baseCondition;
8
15
  const params = [];
9
16
  if (options.api_type) {
10
17
  where += " AND rl.api_type = ?";
@@ -18,16 +25,16 @@ export function getRequestLogs(db, options) {
18
25
  where += " AND rl.router_key_id = ?";
19
26
  params.push(options.router_key_id);
20
27
  }
28
+ return { where, params };
29
+ }
30
+ export function getRequestLogs(db, options) {
31
+ const { where, params } = buildLogWhereClause(options, "1=1");
21
32
  const total = db.prepare(`SELECT COUNT(*) as count FROM request_logs rl WHERE ${where}`).get(...params).count;
22
33
  const offset = (options.page - 1) * options.limit;
23
34
  const data = db
24
- .prepare(`SELECT rl.id, rl.api_type, rl.model, rl.provider_id, rl.status_code, rl.latency_ms,
25
- rl.is_stream, rl.error_message, rl.created_at, rl.is_retry, rl.original_request_id, rl.original_model,
26
- CASE WHEN rl.provider_id = 'router' THEN rl.upstream_request ELSE NULL END AS upstream_request,
27
- rm.backend_model, COALESCE(p.name, rl.provider_id) AS provider_name
35
+ .prepare(`SELECT ${LOG_LIST_SELECT}
28
36
  FROM request_logs rl
29
- LEFT JOIN request_metrics rm ON rm.request_log_id = rl.id
30
- LEFT JOIN providers p ON p.id = rl.provider_id
37
+ ${LOG_LIST_JOIN}
31
38
  WHERE ${where} ORDER BY rl.created_at DESC LIMIT ? OFFSET ?`)
32
39
  .all(...params, options.limit, offset);
33
40
  return { data, total };
@@ -38,10 +45,26 @@ export function getRequestLogById(db, id) {
38
45
  export function deleteLogsBefore(db, beforeDate) {
39
46
  return db.prepare("DELETE FROM request_logs WHERE created_at < ?").run(beforeDate).changes;
40
47
  }
41
- // --- request_metrics ---
42
- export function insertMetrics(db, m) {
43
- const id = randomUUID();
44
- db.prepare(`INSERT INTO request_metrics (id, request_log_id, provider_id, backend_model, api_type, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, ttft_ms, total_duration_ms, tokens_per_second, stop_reason, is_complete)
45
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, m.request_log_id, m.provider_id, m.backend_model, m.api_type, m.input_tokens ?? null, m.output_tokens ?? null, m.cache_creation_tokens ?? null, m.cache_read_tokens ?? null, m.ttft_ms ?? null, m.total_duration_ms ?? null, m.tokens_per_second ?? null, m.stop_reason ?? null, m.is_complete ?? 1);
46
- return id;
48
+ /** 查询某条日志的子请求(retry/failover 关联),上限 100 条 */
49
+ export function getRequestLogChildren(db, parentId) {
50
+ return db.prepare(`SELECT ${LOG_LIST_SELECT}
51
+ FROM request_logs rl
52
+ ${LOG_LIST_JOIN}
53
+ WHERE rl.original_request_id = ?
54
+ ORDER BY rl.created_at ASC
55
+ LIMIT 100`).all(parentId);
56
+ }
57
+ /** 只返回根请求(original_request_id IS NULL),附带子请求数量 */
58
+ export function getRequestLogsGrouped(db, options) {
59
+ const { where, params } = buildLogWhereClause(options, "rl.original_request_id IS NULL");
60
+ const total = db.prepare(`SELECT COUNT(*) as count FROM request_logs rl WHERE ${where}`).get(...params).count;
61
+ const offset = (options.page - 1) * options.limit;
62
+ const data = db
63
+ .prepare(`SELECT ${LOG_LIST_SELECT},
64
+ (SELECT COUNT(*) FROM request_logs c WHERE c.original_request_id = rl.id) AS child_count
65
+ FROM request_logs rl
66
+ ${LOG_LIST_JOIN}
67
+ WHERE ${where} ORDER BY rl.created_at DESC LIMIT ? OFFSET ?`)
68
+ .all(...params, options.limit, offset);
69
+ return { data, total };
47
70
  }
@@ -1,6 +1,39 @@
1
1
  import Database from "better-sqlite3";
2
2
  export type MetricsPeriod = "1h" | "6h" | "24h" | "7d" | "30d";
3
3
  export type MetricsMetric = "ttft" | "tps" | "tokens" | "cache_rate" | "request_count" | "input_tokens" | "output_tokens" | "cache_hit_tokens";
4
+ export interface MetricsRow {
5
+ id: string;
6
+ request_log_id: string;
7
+ provider_id: string;
8
+ backend_model: string;
9
+ api_type: string;
10
+ input_tokens: number | null;
11
+ output_tokens: number | null;
12
+ cache_creation_tokens: number | null;
13
+ cache_read_tokens: number | null;
14
+ ttft_ms: number | null;
15
+ total_duration_ms: number | null;
16
+ tokens_per_second: number | null;
17
+ stop_reason: string | null;
18
+ is_complete: number;
19
+ created_at: string;
20
+ }
21
+ export type MetricsInsert = {
22
+ request_log_id: string;
23
+ provider_id: string;
24
+ backend_model: string;
25
+ api_type: string;
26
+ input_tokens?: number | null;
27
+ output_tokens?: number | null;
28
+ cache_creation_tokens?: number | null;
29
+ cache_read_tokens?: number | null;
30
+ ttft_ms?: number | null;
31
+ total_duration_ms?: number | null;
32
+ tokens_per_second?: number | null;
33
+ stop_reason?: string | null;
34
+ is_complete?: number;
35
+ };
36
+ export declare function insertMetrics(db: Database.Database, m: MetricsInsert): string;
4
37
  export interface MetricsSummaryRow {
5
38
  provider_id: string;
6
39
  provider_name: string;
@@ -1,3 +1,10 @@
1
+ import { randomUUID } from "crypto";
2
+ export function insertMetrics(db, m) {
3
+ const id = randomUUID();
4
+ db.prepare(`INSERT INTO request_metrics (id, request_log_id, provider_id, backend_model, api_type, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, ttft_ms, total_duration_ms, tokens_per_second, stop_reason, is_complete)
5
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, m.request_log_id, m.provider_id, m.backend_model, m.api_type, m.input_tokens ?? null, m.output_tokens ?? null, m.cache_creation_tokens ?? null, m.cache_read_tokens ?? null, m.ttft_ms ?? null, m.total_duration_ms ?? null, m.tokens_per_second ?? null, m.stop_reason ?? null, m.is_complete ?? 1);
6
+ return id;
7
+ }
1
8
  const PERIOD_OFFSET = {
2
9
  "1h": "-1 hours",
3
10
  "6h": "-6 hours",
@@ -0,0 +1,2 @@
1
+ ALTER TABLE request_logs ADD COLUMN is_failover INTEGER NOT NULL DEFAULT 0;
2
+ CREATE INDEX IF NOT EXISTS idx_request_logs_original_request_id ON request_logs(original_request_id);
@@ -26,7 +26,7 @@ export declare function createRetryRule(db: Database.Database, rule: {
26
26
  export declare function updateRetryRule(db: Database.Database, id: string, fields: Partial<Pick<RetryRule, "name" | "status_code" | "body_pattern" | "is_active" | "retry_strategy" | "retry_delay_ms" | "max_retries" | "max_delay_ms">>): void;
27
27
  export declare function deleteRetryRule(db: Database.Database, id: string): void;
28
28
  /**
29
- * 首次启动时(表为空)插入默认重试规则。
30
- * 429/503 为通用 HTTP 重试;其余为 ZAI middleware 特定 400 响应。
29
+ * 启动时按名称查重插入默认重试规则。
30
+ * 已存在的规则不会被重复插入或覆盖。
31
31
  */
32
32
  export declare function seedDefaultRules(db: Database.Database): void;
@@ -28,25 +28,38 @@ export function deleteRetryRule(db, id) {
28
28
  deleteById(db, "retry_rules", id);
29
29
  }
30
30
  // ---------- Default seed rules ----------
31
+ const DEFAULT_RETRY_FIELDS = {
32
+ is_active: 1,
33
+ retry_strategy: "exponential",
34
+ retry_delay_ms: DEFAULT_RETRY_DELAY_MS,
35
+ max_retries: DEFAULT_MAX_RETRIES,
36
+ max_delay_ms: DEFAULT_MAX_DELAY_MS,
37
+ };
31
38
  const DEFAULT_RULES = [
32
- { name: "429 Too Many Requests", status_code: 429, body_pattern: ".*", is_active: 1, retry_strategy: "exponential", retry_delay_ms: 5000, max_retries: 10, max_delay_ms: 60000 },
33
- { name: "503 Service Unavailable", status_code: 503, body_pattern: ".*", is_active: 1, retry_strategy: "exponential", retry_delay_ms: 5000, max_retries: 10, max_delay_ms: 60000 },
34
- { name: 'ZAI 网络错误 (code 1234)', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*"code"\\s*:\\s*"1234"', is_active: 1, retry_strategy: "exponential", retry_delay_ms: 5000, max_retries: 10, max_delay_ms: 60000 },
35
- { name: 'ZAI 临时不可用', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*请稍后重试', is_active: 1, retry_strategy: "exponential", retry_delay_ms: 5000, max_retries: 10, max_delay_ms: 60000 },
36
- { name: 'ZAI 操作失败 (code 500)', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*"code"\\s*:\\s*"500"', is_active: 1, retry_strategy: "exponential", retry_delay_ms: 5000, max_retries: 10, max_delay_ms: 60000 },
39
+ { name: "429 Too Many Requests", status_code: 429, body_pattern: ".*", ...DEFAULT_RETRY_FIELDS },
40
+ { name: "503 Service Unavailable", status_code: 503, body_pattern: ".*", ...DEFAULT_RETRY_FIELDS },
41
+ { name: 'ZAI 网络错误 (code 1234)', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*"code"\\s*:\\s*"1234"', ...DEFAULT_RETRY_FIELDS },
42
+ { name: 'ZAI 临时不可用', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*请稍后重试', ...DEFAULT_RETRY_FIELDS },
43
+ { name: 'ZAI 操作失败 (code 500)', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*"code"\\s*:\\s*"500"', ...DEFAULT_RETRY_FIELDS },
44
+ { name: 'ZAI 速率限制 (HTTP 200, code 1302)', status_code: 200, body_pattern: '"error".*"code"\\s*:\\s*"1302"', ...DEFAULT_RETRY_FIELDS },
45
+ { name: 'ZAI SSE 错误 (HTTP 200, code 500)', status_code: 200, body_pattern: '"error".*"code"\\s*:\\s*"500"', ...DEFAULT_RETRY_FIELDS },
46
+ { name: 'ZAI SSE 错误 (HTTP 200, code 1234)', status_code: 200, body_pattern: '"error".*"code"\\s*:\\s*"1234"', ...DEFAULT_RETRY_FIELDS },
37
47
  ];
38
48
  /**
39
- * 首次启动时(表为空)插入默认重试规则。
40
- * 429/503 为通用 HTTP 重试;其余为 ZAI middleware 特定 400 响应。
49
+ * 启动时按名称查重插入默认重试规则。
50
+ * 已存在的规则不会被重复插入或覆盖。
41
51
  */
42
52
  export function seedDefaultRules(db) {
43
- const count = db.prepare("SELECT COUNT(*) as c FROM retry_rules").get().c;
44
- if (count > 0)
45
- return;
46
- const now = new Date().toISOString();
47
- const insert = db.prepare(`INSERT INTO retry_rules (id, name, status_code, body_pattern, is_active, created_at, retry_strategy, retry_delay_ms, max_retries, max_delay_ms)
53
+ const names = DEFAULT_RULES.map(r => r.name);
54
+ const placeholders = names.map(() => '?').join(',');
55
+ const existing = db.prepare(`SELECT name FROM retry_rules WHERE name IN (${placeholders})`).all(...names);
56
+ const existingSet = new Set(existing.map(r => r.name));
57
+ const stmt = db.prepare(`INSERT INTO retry_rules (id, name, status_code, body_pattern, is_active, created_at, retry_strategy, retry_delay_ms, max_retries, max_delay_ms)
48
58
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
59
+ const now = new Date().toISOString();
49
60
  for (const rule of DEFAULT_RULES) {
50
- insert.run(randomUUID(), rule.name, rule.status_code, rule.body_pattern, rule.is_active, now, rule.retry_strategy, rule.retry_delay_ms, rule.max_retries, rule.max_delay_ms);
61
+ if (!existingSet.has(rule.name)) {
62
+ stmt.run(randomUUID(), rule.name, rule.status_code, rule.body_pattern, rule.is_active, now, rule.retry_strategy, rule.retry_delay_ms, rule.max_retries, rule.max_delay_ms);
63
+ }
51
64
  }
52
65
  }
package/dist/index.js CHANGED
@@ -5,9 +5,7 @@ import { existsSync } from "node:fs";
5
5
  import { randomUUID } from "crypto";
6
6
  import Fastify from "fastify";
7
7
  import { insertRequestLog } from "./db/logs.js";
8
- const HTTP_NOT_FOUND = 404;
9
- const HTTP_INTERNAL_ERROR = 500;
10
- const HTTP_BAD_REQUEST = 400;
8
+ import { HTTP_NOT_FOUND, HTTP_INTERNAL_ERROR, HTTP_BAD_REQUEST } from "./constants.js";
11
9
  const PROVIDER_DEFAULT_QUEUE_TIMEOUT_MS = 5000;
12
10
  const PROVIDER_DEFAULT_MAX_QUEUE_SIZE = 100;
13
11
  // 代理路由路径 → api_type,用于在全局 hook/errorHandler 中识别代理请求
@@ -109,7 +107,7 @@ export async function buildApp(options) {
109
107
  const matcher = new RetryRuleMatcher();
110
108
  matcher.load(db);
111
109
  const semaphoreManager = new ProviderSemaphoreManager();
112
- const tracker = new RequestTracker({ semaphoreManager });
110
+ const tracker = new RequestTracker({ semaphoreManager, logger: app.log });
113
111
  tracker.startPushInterval();
114
112
  // 从 DB 读取已有 provider 的并发配置,初始化信号量管理器和 tracker
115
113
  const allProviders = getAllProviders(db);
@@ -162,7 +160,7 @@ export async function buildApp(options) {
162
160
  !request.url.startsWith("/admin/api")) {
163
161
  return reply.sendFile("index.html");
164
162
  }
165
- reply.code(HTTP_NOT_FOUND).send({ error: "Not Found" });
163
+ reply.code(HTTP_NOT_FOUND).send({ error: { message: "Not Found" } });
166
164
  });
167
165
  }
168
166
  else {