llm-simple-router 0.5.0 → 0.5.2

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 (179) hide show
  1. package/config/recommended-providers.json +76 -0
  2. package/config/recommended-retry-rules.json +10 -0
  3. package/dist/admin/api-response.d.ts +27 -0
  4. package/dist/admin/api-response.js +40 -0
  5. package/dist/admin/constants.d.ts +0 -2
  6. package/dist/admin/constants.js +0 -3
  7. package/dist/admin/groups.js +9 -5
  8. package/dist/admin/logs.js +3 -2
  9. package/dist/admin/mappings.js +7 -6
  10. package/dist/admin/metrics.js +23 -5
  11. package/dist/admin/monitor.js +2 -1
  12. package/dist/admin/providers.js +13 -4
  13. package/dist/admin/proxy-enhancement.js +11 -6
  14. package/dist/admin/recommended.js +1 -9
  15. package/dist/admin/retry-rules.js +8 -4
  16. package/dist/admin/router-keys.js +5 -1
  17. package/dist/admin/routes.js +2 -0
  18. package/dist/admin/settings-import-export.js +3 -2
  19. package/dist/admin/settings.js +7 -5
  20. package/dist/admin/setup.js +3 -2
  21. package/dist/admin/stats.js +20 -3
  22. package/dist/admin/upgrade.d.ts +13 -0
  23. package/dist/admin/upgrade.js +114 -0
  24. package/dist/admin/usage.js +12 -24
  25. package/dist/config.d.ts +1 -1
  26. package/dist/config.js +1 -1
  27. package/dist/constants.d.ts +3 -0
  28. package/dist/constants.js +11 -0
  29. package/dist/db/index.d.ts +3 -3
  30. package/dist/db/index.js +2 -2
  31. package/dist/db/mappings.js +5 -8
  32. package/dist/db/metrics.js +3 -4
  33. package/dist/db/providers.d.ts +8 -0
  34. package/dist/db/providers.js +6 -0
  35. package/dist/db/retry-rules.d.ts +1 -0
  36. package/dist/db/retry-rules.js +3 -0
  37. package/dist/db/settings.d.ts +2 -0
  38. package/dist/db/settings.js +7 -0
  39. package/dist/db/stats.d.ts +1 -2
  40. package/dist/db/stats.js +7 -11
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +55 -34
  43. package/dist/metrics/metrics-extractor.js +1 -1
  44. package/dist/metrics/sse-parser.js +2 -0
  45. package/dist/middleware/admin-auth.js +6 -5
  46. package/dist/middleware/auth.js +1 -10
  47. package/dist/monitor/request-tracker.d.ts +1 -0
  48. package/dist/monitor/request-tracker.js +9 -45
  49. package/dist/monitor/runtime-collector.js +1 -1
  50. package/dist/monitor/stream-content-accumulator.d.ts +14 -0
  51. package/dist/monitor/stream-content-accumulator.js +58 -0
  52. package/dist/proxy/anthropic.d.ts +2 -1
  53. package/dist/proxy/anthropic.js +3 -3
  54. package/dist/proxy/enhancement/directive-parser.d.ts +18 -0
  55. package/dist/proxy/{directive-parser.js → enhancement/directive-parser.js} +44 -0
  56. package/dist/proxy/{enhancement-handler.js → enhancement/enhancement-handler.js} +152 -32
  57. package/dist/proxy/enhancement/index.d.ts +3 -0
  58. package/dist/proxy/enhancement/index.js +3 -0
  59. package/dist/proxy/{response-cleaner.js → enhancement/response-cleaner.js} +14 -0
  60. package/dist/proxy/log-helpers.d.ts +1 -1
  61. package/dist/proxy/mapping-resolver.js +4 -4
  62. package/dist/proxy/openai.d.ts +2 -1
  63. package/dist/proxy/openai.js +4 -4
  64. package/dist/proxy/orchestrator.d.ts +0 -1
  65. package/dist/proxy/orchestrator.js +1 -3
  66. package/dist/proxy/proxy-core.d.ts +0 -4
  67. package/dist/proxy/proxy-core.js +0 -2
  68. package/dist/proxy/proxy-handler.d.ts +1 -1
  69. package/dist/proxy/proxy-handler.js +52 -132
  70. package/dist/proxy/proxy-logging.d.ts +0 -2
  71. package/dist/proxy/proxy-logging.js +1 -3
  72. package/dist/proxy/resilience.d.ts +5 -2
  73. package/dist/proxy/resilience.js +16 -7
  74. package/dist/proxy/strategy/failover.js +2 -7
  75. package/dist/proxy/strategy/random.js +2 -2
  76. package/dist/proxy/strategy/round-robin.js +2 -2
  77. package/dist/proxy/strategy/scheduled.js +1 -8
  78. package/dist/proxy/strategy/targets-rule.d.ts +1 -0
  79. package/dist/proxy/strategy/targets-rule.js +5 -0
  80. package/dist/proxy/transport-fn.d.ts +25 -0
  81. package/dist/proxy/transport-fn.js +55 -0
  82. package/dist/proxy/transport.d.ts +0 -25
  83. package/dist/proxy/transport.js +0 -38
  84. package/dist/upgrade/checker.d.ts +25 -0
  85. package/dist/upgrade/checker.js +120 -0
  86. package/dist/upgrade/deployment.d.ts +2 -0
  87. package/dist/upgrade/deployment.js +20 -0
  88. package/dist/upgrade/version.d.ts +1 -0
  89. package/dist/upgrade/version.js +13 -0
  90. package/dist/utils/password.js +4 -2
  91. package/dist/utils/time-range.d.ts +9 -0
  92. package/dist/utils/time-range.js +40 -0
  93. package/frontend-dist/assets/CardContent-WrBnGhTg.js +1 -0
  94. package/frontend-dist/assets/CardTitle-BcDYk7cq.js +1 -0
  95. package/frontend-dist/assets/Checkbox-MZf0YsDG.js +1 -0
  96. package/frontend-dist/assets/CollapsibleTrigger-CrOH9HlW.js +1 -0
  97. package/frontend-dist/assets/Collection-DcTx_Y54.js +1 -0
  98. package/frontend-dist/assets/Dashboard-D0oDrSLr.js +3 -0
  99. package/frontend-dist/assets/DialogTitle-Cl5Cd7QH.js +1 -0
  100. package/frontend-dist/assets/{Input-l5ZurXX5.js → Input-O0ebU-Va.js} +1 -1
  101. package/frontend-dist/assets/Label-C_S0y7Um.js +1 -0
  102. package/frontend-dist/assets/Login-DGY7uF8P.js +1 -0
  103. package/frontend-dist/assets/Logs-ls8pv89b.js +1 -0
  104. package/frontend-dist/assets/ModelMappings-DGlf0S4s.js +1 -0
  105. package/frontend-dist/assets/Monitor-BSI87grz.js +1 -0
  106. package/frontend-dist/assets/PopperContent-C6Q7hDmf.js +1 -0
  107. package/frontend-dist/assets/Providers-ZkRpj8_m.js +1 -0
  108. package/frontend-dist/assets/ProxyEnhancement-DFPI1W6Z.js +5 -0
  109. package/frontend-dist/assets/RetryRules-DtM31qsl.js +1 -0
  110. package/frontend-dist/assets/RouterKeys-D63tRFKm.js +1 -0
  111. package/frontend-dist/assets/RovingFocusItem-BJoylAKU.js +1 -0
  112. package/frontend-dist/assets/SelectValue-CLp5z6_I.js +1 -0
  113. package/frontend-dist/assets/Settings-DSgRKbTQ.js +6 -0
  114. package/frontend-dist/assets/Setup-BDmj6CRk.js +1 -0
  115. package/frontend-dist/assets/Switch-Wz-t_zkv.js +1 -0
  116. package/frontend-dist/assets/TableHeader-DGtcqGkw.js +1 -0
  117. package/frontend-dist/assets/TabsTrigger-CPCi2HIa.js +1 -0
  118. package/frontend-dist/assets/Teleport-DdjYHlNK.js +3 -0
  119. package/frontend-dist/assets/TooltipTrigger-H_QoPY1n.js +1 -0
  120. package/frontend-dist/assets/UnifiedRequestDialog-BAAfMJJl.js +3 -0
  121. package/frontend-dist/assets/{VisuallyHidden-BwwTtzb9.js → VisuallyHidden-Cyk-jWwh.js} +1 -1
  122. package/frontend-dist/assets/VisuallyHiddenInput-CYjNe_H8.js +1 -0
  123. package/frontend-dist/assets/alert-dialog-Bi3dliLl.js +1 -0
  124. package/frontend-dist/assets/badge-Kkta3e9W.js +1 -0
  125. package/frontend-dist/assets/button-BQ3s7yNh.js +12 -0
  126. package/frontend-dist/assets/{createLucideIcon-Biq59l_W.js → createLucideIcon-D1tkPDOQ.js} +1 -1
  127. package/frontend-dist/assets/dialog-DoIATUYw.js +1 -0
  128. package/frontend-dist/assets/{file-text-DoRW0hQW.js → file-text-Dt6QP1bZ.js} +1 -1
  129. package/frontend-dist/assets/index-BY0E7CHR.js +1 -0
  130. package/frontend-dist/assets/index-Bnrh1mFY.css +1 -0
  131. package/frontend-dist/assets/lib-CxwxnlwW.js +1 -0
  132. package/frontend-dist/assets/{ohash.D__AXeF1-BGxYMs6k.js → ohash.D__AXeF1-b0PiKZB_.js} +1 -1
  133. package/frontend-dist/assets/{useClipboard-vaHkvJHw.js → useClipboard-Cnnz6AAN.js} +1 -1
  134. package/frontend-dist/assets/useLogRetention-DYP5LOAc.js +1 -0
  135. package/frontend-dist/assets/useNonce-DKbOCfgM.js +1 -0
  136. package/frontend-dist/assets/x-CAoitXRt.js +1 -0
  137. package/frontend-dist/index.html +18 -9
  138. package/package.json +2 -1
  139. package/dist/proxy/directive-parser.d.ts +0 -7
  140. package/frontend-dist/assets/CardContent-CIO85eT6.js +0 -1
  141. package/frontend-dist/assets/CardTitle-DiqIReMT.js +0 -1
  142. package/frontend-dist/assets/Checkbox-C2u5pIp4.js +0 -1
  143. package/frontend-dist/assets/CollapsibleTrigger-RKFL41om.js +0 -1
  144. package/frontend-dist/assets/Collection-iiNnuTQj.js +0 -1
  145. package/frontend-dist/assets/Dashboard-DOEqP6gF.js +0 -3
  146. package/frontend-dist/assets/DialogTitle-CEqndrf6.js +0 -1
  147. package/frontend-dist/assets/Label-PgGtS8v2.js +0 -1
  148. package/frontend-dist/assets/Login-DaN6ZcCx.js +0 -1
  149. package/frontend-dist/assets/Logs-CleRQ7Xk.js +0 -1
  150. package/frontend-dist/assets/ModelMappings-CacA_ua_.js +0 -1
  151. package/frontend-dist/assets/Monitor-LSMFOBN2.js +0 -1
  152. package/frontend-dist/assets/PopperContent-zLFHqQP0.js +0 -1
  153. package/frontend-dist/assets/Providers-NT5MUDU0.js +0 -1
  154. package/frontend-dist/assets/ProxyEnhancement-DhOy8nNy.js +0 -5
  155. package/frontend-dist/assets/RetryRules-7arWa3jB.js +0 -1
  156. package/frontend-dist/assets/RouterKeys-CdaZunRg.js +0 -1
  157. package/frontend-dist/assets/SelectValue-CSg-MKW_.js +0 -1
  158. package/frontend-dist/assets/Settings-1ntV9XE3.js +0 -6
  159. package/frontend-dist/assets/Setup-CXLTDhYJ.js +0 -1
  160. package/frontend-dist/assets/Switch-DivrIFE3.js +0 -1
  161. package/frontend-dist/assets/TableHeader-Bn0bodWx.js +0 -1
  162. package/frontend-dist/assets/TabsContent-MWvOH_LJ.js +0 -1
  163. package/frontend-dist/assets/TabsTrigger-WKkUfO2M.js +0 -1
  164. package/frontend-dist/assets/Teleport-B0PNXZbP.js +0 -3
  165. package/frontend-dist/assets/UnifiedRequestDialog-Ba2e7YuJ.js +0 -3
  166. package/frontend-dist/assets/VisuallyHiddenInput-EGZSP7s8.js +0 -1
  167. package/frontend-dist/assets/alert-dialog-CS1yFhdV.js +0 -1
  168. package/frontend-dist/assets/badge-C-QcC5n2.js +0 -1
  169. package/frontend-dist/assets/button-Dbz2Be22.js +0 -12
  170. package/frontend-dist/assets/dialog-Cr0YQlLW.js +0 -1
  171. package/frontend-dist/assets/index-0H2uCGbx.js +0 -1
  172. package/frontend-dist/assets/index-D-cdVNCb.css +0 -1
  173. package/frontend-dist/assets/lib-B0lieqgg.js +0 -1
  174. package/frontend-dist/assets/useForwardExpose-C2_ks3sW.js +0 -1
  175. package/frontend-dist/assets/useLogRetention-Cs_fiKql.js +0 -1
  176. package/frontend-dist/assets/useNonce-C9do0jOI.js +0 -1
  177. package/frontend-dist/assets/x-BlTnH_0_.js +0 -1
  178. /package/dist/proxy/{enhancement-handler.d.ts → enhancement/enhancement-handler.d.ts} +0 -0
  179. /package/dist/proxy/{response-cleaner.d.ts → enhancement/response-cleaner.d.ts} +0 -0
@@ -0,0 +1,76 @@
1
+ [
2
+ {
3
+ "group": "DeepSeek",
4
+ "presets": [
5
+ { "plan": "Anthropic", "presetName": "deepseek", "apiType": "anthropic", "baseUrl": "https://api.deepseek.com/anthropic", "models": ["deepseek-chat", "deepseek-reasoner"] },
6
+ { "plan": "OpenAI", "presetName": "deepseek-openai", "apiType": "openai", "baseUrl": "https://api.deepseek.com", "models": ["deepseek-chat", "deepseek-reasoner"] }
7
+ ]
8
+ },
9
+ {
10
+ "group": "百度千帆",
11
+ "presets": [
12
+ { "plan": "API", "presetName": "qianfan", "apiType": "openai", "baseUrl": "https://qianfan.baidubce.com/v2", "models": ["ernie-4.0-8k", "ernie-4.0-turbo-8k", "ernie-3.5-8k", "ernie-speed-8k", "ernie-lite-8k", "ernie-x1-32k-preview", "deepseek-v3", "deepseek-r1"] }
13
+ ]
14
+ },
15
+ {
16
+ "group": "科大讯飞",
17
+ "presets": [
18
+ { "plan": "API", "presetName": "iflytek-spark", "apiType": "openai", "baseUrl": "https://spark-api-open.xf-yun.com/v1", "models": ["4.0Ultra", "generalv3.5", "max-32k", "generalv3", "pro-128k", "lite"] }
19
+ ]
20
+ },
21
+ {
22
+ "group": "硅基流动",
23
+ "presets": [
24
+ { "plan": "API", "presetName": "siliconflow", "apiType": "openai", "baseUrl": "https://api.siliconflow.cn/v1", "models": ["deepseek-ai/DeepSeek-V3.2-Exp", "deepseek-ai/DeepSeek-R1", "Qwen/Qwen3-8B", "Qwen/Qwen2.5-72B-Instruct", "Qwen/Qwen2.5-Coder-32B-Instruct", "moonshotai/Kimi-K2-Instruct", "moonshotai/Kimi-K2.5"] }
25
+ ]
26
+ },
27
+ {
28
+ "group": "智谱",
29
+ "presets": [
30
+ { "plan": "Coding Plan", "presetName": "zhipu-coding-plan", "apiType": "anthropic", "baseUrl": "https://open.bigmodel.cn/api/anthropic", "models": ["glm-5.1", "glm-5", "glm-4.7", "glm-4.5-air"] },
31
+ { "plan": "API", "presetName": "zhipu", "apiType": "openai", "baseUrl": "https://open.bigmodel.cn/api/paas/v4", "models": ["glm-5.1", "glm-5", "glm-5-turbo", "glm-4.7", "glm-4.7-flash", "glm-4.6"] }
32
+ ]
33
+ },
34
+ {
35
+ "group": "KIMI",
36
+ "presets": [
37
+ { "plan": "Coding Plan", "presetName": "kimi-coding-plan", "apiType": "anthropic", "baseUrl": "https://api.kimi.com/coding", "models": ["kimi-for-coding", "kimi-k2.5"] },
38
+ { "plan": "API", "presetName": "kimi", "apiType": "openai", "baseUrl": "https://api.moonshot.cn", "models": ["kimi-k2.6", "kimi-k2.5", "kimi-k2-turbo-preview", "kimi-k2-thinking", "moonshot-v1-128k"] }
39
+ ]
40
+ },
41
+ {
42
+ "group": "Minimax",
43
+ "presets": [
44
+ { "plan": "Token Plan", "presetName": "minimax-token-plan", "apiType": "anthropic", "baseUrl": "https://api.minimaxi.com/anthropic", "models": ["MiniMax-M2.7"] },
45
+ { "plan": "API", "presetName": "minimax", "apiType": "openai", "baseUrl": "https://api.minimax.chat", "models": ["MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M2.5", "MiniMax-M2.5-highspeed", "MiniMax-M2.1", "MiniMax-M2"] }
46
+ ]
47
+ },
48
+ {
49
+ "group": "火山引擎",
50
+ "presets": [
51
+ { "plan": "Coding Plan", "presetName": "volcengine-coding-plan", "apiType": "anthropic", "baseUrl": "https://ark.cn-beijing.volces.com/api/compatible", "models": ["ark-code-latest", "doubao-seed-2.0-code", "kimi-k2.5", "glm-4.7", "deepseek-v3.2"] },
52
+ { "plan": "API", "presetName": "volcengine", "apiType": "openai", "baseUrl": "https://ark.cn-beijing.volces.com/api/v3", "models": ["doubao-seed-2-0-pro-260215", "doubao-seed-1-8-251228", "doubao-seed-code-preview-251028"] }
53
+ ]
54
+ },
55
+ {
56
+ "group": "阿里云",
57
+ "presets": [
58
+ { "plan": "Coding Plan", "presetName": "aliyun-coding-plan", "apiType": "anthropic", "baseUrl": "https://coding.dashscope.aliyuncs.com/apps/anthropic", "models": ["qwen3.6-plus", "qwen3-coder-next", "qwen3-coder-plus", "kimi-k2.5", "glm-5", "MiniMax-M2.5"] },
59
+ { "plan": "API", "presetName": "aliyun", "apiType": "openai", "baseUrl": "https://dashscope.aliyuncs.com/compatible-mode", "models": ["qwen3.6-plus", "qwen3.5-plus", "qwen3-max", "qwen3.5-flash", "qwen3-coder-plus", "qwen3-coder-next"] }
60
+ ]
61
+ },
62
+ {
63
+ "group": "腾讯云",
64
+ "presets": [
65
+ { "plan": "Coding Plan", "presetName": "tencent-coding-plan", "apiType": "anthropic", "baseUrl": "https://api.lkeap.cloud.tencent.com/coding/anthropic", "models": ["tc-code-latest", "hunyuan-2.0-instruct", "hunyuan-2.0-thinking", "hunyuan-turbos", "hunyuan-t1", "glm-5", "kimi-k2.5"] },
66
+ { "plan": "API", "presetName": "tencent", "apiType": "openai", "baseUrl": "https://api.hunyuan.cloud.tencent.com", "models": ["hunyuan-2.0-thinking", "hunyuan-2.0-instruct", "hunyuan-t1-latest", "hunyuan-a13b", "hunyuan-turbos-latest"] }
67
+ ]
68
+ },
69
+ {
70
+ "group": "阶跃星辰",
71
+ "presets": [
72
+ { "plan": "Step Plan", "presetName": "stepfun-step-plan", "apiType": "anthropic", "baseUrl": "https://api.stepfun.com/step_plan", "models": ["step-3.5-flash-2603", "step-3.5-flash"] },
73
+ { "plan": "API", "presetName": "stepfun", "apiType": "openai", "baseUrl": "https://api.stepfun.com", "models": ["step-3.5-flash", "step-3", "step-2-mini", "step-2-16k", "step-1-8k", "step-1-32k"] }
74
+ ]
75
+ }
76
+ ]
@@ -0,0 +1,10 @@
1
+ [
2
+ { "name": "429 Too Many Requests", "status_code": 429, "body_pattern": ".*", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
3
+ { "name": "503 Service Unavailable", "status_code": 503, "body_pattern": ".*", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
4
+ { "name": "ZAI 网络错误 (code 1234)", "status_code": 400, "body_pattern": "\"type\"\\s*:\\s*\"error\".*\"code\"\\s*:\\s*\"1234\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
5
+ { "name": "ZAI 临时不可用", "status_code": 400, "body_pattern": "\"type\"\\s*:\\s*\"error\".*请稍后重试", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
6
+ { "name": "ZAI 操作失败 (code 500)", "status_code": 400, "body_pattern": "\"type\"\\s*:\\s*\"error\".*\"code\"\\s*:\\s*\"500\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
7
+ { "name": "ZAI 速率限制 (HTTP 200, code 1302)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"1302\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
8
+ { "name": "ZAI SSE 错误 (HTTP 200, code 500)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"500\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
9
+ { "name": "ZAI SSE 错误 (HTTP 200, code 1234)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"1234\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 }
10
+ ]
@@ -0,0 +1,27 @@
1
+ /** 统一信封格式 */
2
+ export interface ApiResponse<T> {
3
+ code: number;
4
+ message: string;
5
+ data: T | null;
6
+ }
7
+ /** 错误码常量 — XXYYZ 格式:前两位=HTTP 类别,后三位=业务序号 */
8
+ export declare const API_CODE: {
9
+ readonly SUCCESS: 0;
10
+ readonly BAD_REQUEST: 40001;
11
+ readonly VALIDATION_FAILED: 40002;
12
+ readonly INVALID_REGEX: 40003;
13
+ readonly WRONG_PASSWORD: 40101;
14
+ readonly TOKEN_INVALID: 40102;
15
+ readonly NOT_INITIALIZED: 40103;
16
+ readonly NOT_FOUND: 40401;
17
+ readonly CONFLICT_NAME: 40901;
18
+ readonly CONFLICT_REFERENCED: 40902;
19
+ readonly ALREADY_INITIALIZED: 40903;
20
+ readonly INTERNAL_ERROR: 50001;
21
+ };
22
+ /** HTTP status → 默认 API_CODE 映射(errorHandler 兜底用) */
23
+ export declare function statusToApiCode(status: number): number;
24
+ /** 判断是否为 Admin API 路由(需要信封包装) */
25
+ export declare function isAdminApiResponse(url: string, contentType?: string): boolean;
26
+ /** 构造错误响应 */
27
+ export declare function apiError(code: number, message: string): ApiResponse<null>;
@@ -0,0 +1,40 @@
1
+ // src/admin/api-response.ts
2
+ /** 错误码常量 — XXYYZ 格式:前两位=HTTP 类别,后三位=业务序号 */
3
+ export const API_CODE = {
4
+ SUCCESS: 0,
5
+ BAD_REQUEST: 40001,
6
+ VALIDATION_FAILED: 40002,
7
+ INVALID_REGEX: 40003,
8
+ WRONG_PASSWORD: 40101,
9
+ TOKEN_INVALID: 40102,
10
+ NOT_INITIALIZED: 40103,
11
+ NOT_FOUND: 40401,
12
+ CONFLICT_NAME: 40901,
13
+ CONFLICT_REFERENCED: 40902,
14
+ ALREADY_INITIALIZED: 40903,
15
+ INTERNAL_ERROR: 50001,
16
+ };
17
+ /** HTTP status → 默认 API_CODE 映射(errorHandler 兜底用) */
18
+ export function statusToApiCode(status) {
19
+ if (status === 400)
20
+ return API_CODE.BAD_REQUEST;
21
+ if (status === 401)
22
+ return API_CODE.TOKEN_INVALID;
23
+ if (status === 404)
24
+ return API_CODE.NOT_FOUND;
25
+ if (status === 409)
26
+ return API_CODE.CONFLICT_NAME;
27
+ return API_CODE.INTERNAL_ERROR;
28
+ }
29
+ /** 判断是否为 Admin API 路由(需要信封包装) */
30
+ export function isAdminApiResponse(url, contentType) {
31
+ if (!url.startsWith('/admin/api/'))
32
+ return false;
33
+ if (contentType?.includes('text/event-stream'))
34
+ return false;
35
+ return true;
36
+ }
37
+ /** 构造错误响应 */
38
+ export function apiError(code, message) {
39
+ return { code, message, data: null };
40
+ }
@@ -1,3 +1 @@
1
- import type { FastifyReply } from "fastify";
2
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";
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,5 +1,2 @@
1
1
  // HTTP 状态码统一从 src/constants.ts 导入,避免重复定义
2
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";
3
- export function sendErrorResponse(reply, statusCode, message) {
4
- return reply.code(statusCode).send({ error: { message } });
5
- }
@@ -1,7 +1,8 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getProviderById, getMappingGroupById, } from "../db/index.js";
3
3
  import { STRATEGY_NAMES } from "../proxy/strategy/types.js";
4
- import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_CONFLICT } from "./constants.js";
4
+ import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_CONFLICT, HTTP_NOT_FOUND } from "./constants.js";
5
+ import { API_CODE, apiError } from "./api-response.js";
5
6
  const MIN_FAILOVER_TARGETS = 2;
6
7
  const CreateGroupSchema = Type.Object({
7
8
  client_model: Type.String({ minLength: 1 }),
@@ -82,7 +83,7 @@ export const adminGroupRoutes = (app, options, done) => {
82
83
  const body = request.body;
83
84
  const validationError = await validateRule(db, body.strategy, body.rule);
84
85
  if (validationError) {
85
- return reply.code(HTTP_BAD_REQUEST).send({ error: { message: validationError } });
86
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, validationError));
86
87
  }
87
88
  try {
88
89
  const id = createMappingGroup(db, {
@@ -94,7 +95,7 @@ export const adminGroupRoutes = (app, options, done) => {
94
95
  }
95
96
  catch (err) {
96
97
  if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
97
- return reply.code(HTTP_CONFLICT).send({ error: { message: "client_model already exists" } });
98
+ return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.CONFLICT_NAME, "client_model already exists"));
98
99
  }
99
100
  throw err;
100
101
  }
@@ -113,7 +114,7 @@ export const adminGroupRoutes = (app, options, done) => {
113
114
  const ruleJson = body.rule ?? findGroupRule(db, id);
114
115
  const validationError = await validateRule(db, strategy, ruleJson);
115
116
  if (validationError) {
116
- return reply.code(HTTP_BAD_REQUEST).send({ error: { message: validationError } });
117
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, validationError));
117
118
  }
118
119
  try {
119
120
  updateMappingGroup(db, id, fields);
@@ -121,13 +122,16 @@ export const adminGroupRoutes = (app, options, done) => {
121
122
  }
122
123
  catch (err) {
123
124
  if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
124
- return reply.code(HTTP_CONFLICT).send({ error: { message: "client_model already exists" } });
125
+ return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.CONFLICT_NAME, "client_model already exists"));
125
126
  }
126
127
  throw err;
127
128
  }
128
129
  });
129
130
  app.delete("/admin/api/mapping-groups/:id", async (request, reply) => {
130
131
  const { id } = request.params;
132
+ const existing = getMappingGroupById(db, id);
133
+ if (!existing)
134
+ return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Mapping group not found"));
131
135
  deleteMappingGroup(db, id);
132
136
  return reply.send({ success: true });
133
137
  });
@@ -1,6 +1,7 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { getRequestLogs, getRequestLogsGrouped, getRequestLogById, getRequestLogChildren, deleteLogsBefore } from "../db/index.js";
3
3
  import { HTTP_NOT_FOUND } from "./constants.js";
4
+ import { API_CODE, apiError } from "./api-response.js";
4
5
  const LogQuerySchema = Type.Object({
5
6
  page: Type.Optional(Type.String()),
6
7
  limit: Type.Optional(Type.String()),
@@ -42,7 +43,7 @@ export const adminLogRoutes = (app, options, done) => {
42
43
  const params = request.params;
43
44
  const log = getRequestLogById(db, params.id);
44
45
  if (!log) {
45
- return reply.code(HTTP_NOT_FOUND).send({ error: { message: "Log not found" } });
46
+ return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Log not found"));
46
47
  }
47
48
  return reply.send(log);
48
49
  });
@@ -50,7 +51,7 @@ export const adminLogRoutes = (app, options, done) => {
50
51
  const params = request.params;
51
52
  const parent = getRequestLogById(db, params.id);
52
53
  if (!parent) {
53
- return reply.code(HTTP_NOT_FOUND).send({ error: { message: "Log not found" } });
54
+ return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Log not found"));
54
55
  }
55
56
  const rows = getRequestLogChildren(db, params.id);
56
57
  return reply.send(rows);
@@ -2,6 +2,7 @@ import { Type } from "@sinclair/typebox";
2
2
  import { getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getProviderById, getMappingGroupById, getMappingGroup, } from "../db/index.js";
3
3
  import { STRATEGY_NAMES } from "../proxy/strategy/types.js";
4
4
  import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND, HTTP_CONFLICT } from "./constants.js";
5
+ import { API_CODE, apiError } from "./api-response.js";
5
6
  const CreateMappingSchema = Type.Object({
6
7
  client_model: Type.String({ minLength: 1 }),
7
8
  backend_model: Type.String({ minLength: 1 }),
@@ -46,7 +47,7 @@ export const adminMappingRoutes = (app, options, done) => {
46
47
  const body = request.body;
47
48
  const provider = getProviderById(db, body.provider_id);
48
49
  if (!provider) {
49
- return reply.code(HTTP_BAD_REQUEST).send({ error: { message: "provider_id not found" } });
50
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.NOT_FOUND, "provider_id not found"));
50
51
  }
51
52
  try {
52
53
  const id = createMappingGroup(db, {
@@ -61,7 +62,7 @@ export const adminMappingRoutes = (app, options, done) => {
61
62
  }
62
63
  catch (err) {
63
64
  if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
64
- return reply.code(HTTP_CONFLICT).send({ error: { message: "client_model already exists" } });
65
+ return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.CONFLICT_NAME, "client_model already exists"));
65
66
  }
66
67
  throw err;
67
68
  }
@@ -70,7 +71,7 @@ export const adminMappingRoutes = (app, options, done) => {
70
71
  const { id } = request.params;
71
72
  const group = findGroupByIdOrClientModel(db, id);
72
73
  if (!group) {
73
- return reply.code(HTTP_NOT_FOUND).send({ error: { message: "Mapping not found" } });
74
+ return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Mapping not found"));
74
75
  }
75
76
  const body = request.body;
76
77
  let rule;
@@ -86,7 +87,7 @@ export const adminMappingRoutes = (app, options, done) => {
86
87
  if (body.provider_id !== undefined) {
87
88
  const provider = getProviderById(db, body.provider_id);
88
89
  if (!provider) {
89
- return reply.code(HTTP_BAD_REQUEST).send({ error: { message: "provider_id not found" } });
90
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.NOT_FOUND, "provider_id not found"));
90
91
  }
91
92
  defaultTarget.provider_id = body.provider_id;
92
93
  }
@@ -102,7 +103,7 @@ export const adminMappingRoutes = (app, options, done) => {
102
103
  }
103
104
  catch (err) {
104
105
  if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
105
- return reply.code(HTTP_CONFLICT).send({ error: { message: "client_model already exists" } });
106
+ return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.CONFLICT_NAME, "client_model already exists"));
106
107
  }
107
108
  throw err;
108
109
  }
@@ -111,7 +112,7 @@ export const adminMappingRoutes = (app, options, done) => {
111
112
  const { id } = request.params;
112
113
  const group = findGroupByIdOrClientModel(db, id);
113
114
  if (!group) {
114
- return reply.code(HTTP_NOT_FOUND).send({ error: { message: "Mapping not found" } });
115
+ return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Mapping not found"));
115
116
  }
116
117
  deleteMappingGroup(db, group.id);
117
118
  return reply.send({ success: true });
@@ -1,9 +1,14 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { getMetricsSummary, getMetricsTimeseries } from "../db/index.js";
3
- const PeriodEnum = Type.Union([
3
+ import { resolveTimeRange } from "../utils/time-range.js";
4
+ const LegacyPeriodEnum = Type.Union([
4
5
  Type.Literal("1h"), Type.Literal("5h"), Type.Literal("6h"), Type.Literal("24h"),
5
6
  Type.Literal("7d"), Type.Literal("30d"),
6
7
  ]);
8
+ const DashboardPeriodEnum = Type.Union([
9
+ Type.Literal("window"), Type.Literal("weekly"), Type.Literal("monthly"),
10
+ ]);
11
+ const PeriodEnum = Type.Union([LegacyPeriodEnum, DashboardPeriodEnum]);
7
12
  const MetricEnum = Type.Union([
8
13
  Type.Literal("ttft"), Type.Literal("tps"), Type.Literal("tokens"),
9
14
  Type.Literal("cache_rate"), Type.Literal("request_count"),
@@ -27,18 +32,31 @@ const TimeseriesQuerySchema = Type.Object({
27
32
  start_time: Type.Optional(Type.String()),
28
33
  end_time: Type.Optional(Type.String()),
29
34
  });
35
+ const DASHBOARD_PERIODS = new Set(["window", "weekly", "monthly"]);
36
+ function resolveMetricsTime(query, db, routerKeyId) {
37
+ if (query.start_time && query.end_time) {
38
+ return { startTime: query.start_time, endTime: query.end_time, legacyPeriod: "30d" };
39
+ }
40
+ const period = query.period ?? "weekly";
41
+ if (DASHBOARD_PERIODS.has(period)) {
42
+ const range = resolveTimeRange(period, db, routerKeyId);
43
+ return { startTime: range.startTime, endTime: range.endTime, legacyPeriod: "5h" };
44
+ }
45
+ return { legacyPeriod: period };
46
+ }
30
47
  export const adminMetricsRoutes = (app, options, done) => {
48
+ const { db } = options;
31
49
  app.get("/admin/api/metrics/summary", { schema: { querystring: SummaryQuerySchema } }, async (request, reply) => {
32
50
  const query = request.query;
33
- const period = (query.period ?? "24h");
34
- const summary = getMetricsSummary(options.db, period, query.provider_id, query.backend_model, query.router_key_id, query.start_time, query.end_time);
51
+ const { startTime, endTime, legacyPeriod } = resolveMetricsTime(query, db, query.router_key_id);
52
+ const summary = getMetricsSummary(db, legacyPeriod, query.provider_id, query.backend_model, query.router_key_id, startTime, endTime);
35
53
  return reply.send(summary);
36
54
  });
37
55
  app.get("/admin/api/metrics/timeseries", { schema: { querystring: TimeseriesQuerySchema } }, async (request, reply) => {
38
56
  const query = request.query;
39
- const period = (query.period ?? "24h");
40
57
  const metric = query.metric;
41
- const timeseries = getMetricsTimeseries(options.db, period, metric, query.provider_id, query.backend_model, query.router_key_id, query.start_time, query.end_time);
58
+ const { startTime, endTime, legacyPeriod } = resolveMetricsTime(query, db, query.router_key_id);
59
+ const timeseries = getMetricsTimeseries(db, legacyPeriod, metric, query.provider_id, query.backend_model, query.router_key_id, startTime, endTime);
42
60
  return reply.send(timeseries);
43
61
  });
44
62
  done();
@@ -1,4 +1,5 @@
1
1
  import { HTTP_NOT_FOUND } from "./constants.js";
2
+ import { API_CODE, apiError } from "./api-response.js";
2
3
  const HTTP_OK = 200;
3
4
  export const adminMonitorRoutes = (app, options, done) => {
4
5
  const { tracker } = options;
@@ -26,7 +27,7 @@ export const adminMonitorRoutes = (app, options, done) => {
26
27
  const { id } = request.params;
27
28
  const req = tracker.getRequestById(id);
28
29
  if (!req)
29
- return reply.status(HTTP_NOT_FOUND).send({ error: "Not found" });
30
+ return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Not found"));
30
31
  return req;
31
32
  });
32
33
  done();
@@ -3,6 +3,7 @@ import { getAllProviders, getProviderById, createProvider, updateProvider, delet
3
3
  import { encrypt, decrypt } from "../utils/crypto.js";
4
4
  import { getSetting } from "../db/settings.js";
5
5
  import { HTTP_CREATED, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_BAD_REQUEST } from "./constants.js";
6
+ import { API_CODE, apiError } from "./api-response.js";
6
7
  const API_KEY_PREVIEW_MIN_LENGTH = 8;
7
8
  const API_KEY_PREVIEW_PREFIX_LEN = 4;
8
9
  const PROVIDER_NAME_RE = /^[a-zA-Z0-9_-]+$/;
@@ -52,7 +53,11 @@ export const adminProviderRoutes = (app, options, done) => {
52
53
  app.post("/admin/api/providers", { schema: { body: CreateProviderSchema } }, async (request, reply) => {
53
54
  const body = request.body;
54
55
  if (!PROVIDER_NAME_RE.test(body.name)) {
55
- return reply.status(HTTP_BAD_REQUEST).send({ error: { message: "Provider 名称仅允许英文大小写字母、数字、横线和下划线" } });
56
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, "Provider 名称仅允许英文大小写字母、数字、横线和下划线"));
57
+ }
58
+ const existing = db.prepare("SELECT id FROM providers WHERE name = ?").get(body.name);
59
+ if (existing) {
60
+ return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.CONFLICT_NAME, `Provider 名称 '${body.name}' 已存在`));
56
61
  }
57
62
  const encryptedKey = encrypt(body.api_key, getSetting(db, "encryption_key"));
58
63
  const id = createProvider(db, {
@@ -84,11 +89,11 @@ export const adminProviderRoutes = (app, options, done) => {
84
89
  const { id } = request.params;
85
90
  const existing = getProviderById(db, id);
86
91
  if (!existing) {
87
- return reply.code(HTTP_NOT_FOUND).send({ error: { message: "Provider not found" } });
92
+ return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Provider not found"));
88
93
  }
89
94
  const body = request.body;
90
95
  if (body.name !== undefined && !PROVIDER_NAME_RE.test(body.name)) {
91
- return reply.status(HTTP_BAD_REQUEST).send({ error: { message: "Provider 名称仅允许英文大小写字母、数字、横线和下划线" } });
96
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.VALIDATION_FAILED, "Provider 名称仅允许英文大小写字母、数字、横线和下划线"));
92
97
  }
93
98
  const fields = {};
94
99
  if (body.name !== undefined)
@@ -130,13 +135,17 @@ export const adminProviderRoutes = (app, options, done) => {
130
135
  });
131
136
  app.delete("/admin/api/providers/:id", async (request, reply) => {
132
137
  const { id } = request.params;
138
+ const existing = getProviderById(db, id);
139
+ if (!existing) {
140
+ return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Provider not found"));
141
+ }
133
142
  const groups = getAllMappingGroups(db);
134
143
  for (const g of groups) {
135
144
  try {
136
145
  const rule = JSON.parse(g.rule);
137
146
  const targets = [rule.default, ...(rule.windows || [])].filter(Boolean);
138
147
  if (targets.some((t) => t.provider_id === id)) {
139
- return reply.code(HTTP_CONFLICT).send({ error: { message: `Provider is referenced by mapping group '${g.client_model}'` } });
148
+ return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.CONFLICT_REFERENCED, `Provider is referenced by mapping group '${g.client_model}'`));
140
149
  }
141
150
  }
142
151
  catch {
@@ -1,4 +1,12 @@
1
+ import { Type } from "@sinclair/typebox";
1
2
  import { getSetting, setSetting } from "../db/settings.js";
3
+ const UpdateProxyEnhancementSchema = Type.Object({
4
+ claude_code_enabled: Type.Boolean(),
5
+ });
6
+ const SessionParamsSchema = Type.Object({
7
+ keyId: Type.String(),
8
+ sessionId: Type.String(),
9
+ });
2
10
  import { getSessionStates, getSessionHistory, } from "../db/session-states.js";
3
11
  import { modelState } from "../proxy/model-state.js";
4
12
  export const adminProxyEnhancementRoutes = (app, options, done) => {
@@ -10,11 +18,8 @@ export const adminProxyEnhancementRoutes = (app, options, done) => {
10
18
  : { claude_code_enabled: false };
11
19
  return reply.send(config);
12
20
  });
13
- app.put("/admin/api/proxy-enhancement", async (req, reply) => {
21
+ app.put("/admin/api/proxy-enhancement", { schema: { body: UpdateProxyEnhancementSchema } }, async (req, reply) => {
14
22
  const body = req.body;
15
- if (typeof body.claude_code_enabled !== "boolean") {
16
- return reply.status(400).send({ error: "claude_code_enabled must be a boolean" }); // eslint-disable-line no-magic-numbers
17
- }
18
23
  const config = {
19
24
  claude_code_enabled: body.claude_code_enabled,
20
25
  };
@@ -25,12 +30,12 @@ export const adminProxyEnhancementRoutes = (app, options, done) => {
25
30
  const states = getSessionStates(db);
26
31
  return reply.send(states);
27
32
  });
28
- app.get("/admin/api/session-states/:keyId/:sessionId/history", async (req, reply) => {
33
+ app.get("/admin/api/session-states/:keyId/:sessionId/history", { schema: { params: SessionParamsSchema } }, async (req, reply) => {
29
34
  const { keyId, sessionId } = req.params;
30
35
  const history = getSessionHistory(db, keyId, sessionId);
31
36
  return reply.send(history);
32
37
  });
33
- app.delete("/admin/api/session-states/:keyId/:sessionId", async (req, reply) => {
38
+ app.delete("/admin/api/session-states/:keyId/:sessionId", { schema: { params: SessionParamsSchema } }, async (req, reply) => {
34
39
  const { keyId, sessionId } = req.params;
35
40
  modelState.delete(keyId, sessionId);
36
41
  return reply.send({ success: true });
@@ -2,15 +2,7 @@ import { getRecommendedProviders, getRecommendedRetryRules, reloadConfig } from
2
2
  export const adminRecommendedRoutes = (app, options, done) => {
3
3
  const { db } = options;
4
4
  app.get("/admin/api/recommended/providers", async (_req, reply) => {
5
- const groups = getRecommendedProviders();
6
- const existing = new Set(db.prepare("SELECT name FROM providers").all().map((r) => r.name));
7
- const filtered = groups
8
- .map((g) => ({
9
- ...g,
10
- presets: g.presets.filter((p) => !existing.has(p.presetName)),
11
- }))
12
- .filter((g) => g.presets.length > 0);
13
- return reply.send(filtered);
5
+ return reply.send(getRecommendedProviders());
14
6
  });
15
7
  app.get("/admin/api/recommended/retry-rules", async (_req, reply) => {
16
8
  const rules = getRecommendedRetryRules();
@@ -1,6 +1,7 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import { getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, } from "../db/index.js";
3
- import { HTTP_BAD_REQUEST, HTTP_CREATED } from "./constants.js";
2
+ import { getAllRetryRules, getRetryRuleById, createRetryRule, updateRetryRule, deleteRetryRule, } from "../db/index.js";
3
+ import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND } from "./constants.js";
4
+ import { API_CODE, apiError } from "./api-response.js";
4
5
  const DEFAULT_RETRY_DELAY_MS = 5000;
5
6
  const DEFAULT_MAX_RETRIES = 10;
6
7
  const DEFAULT_MAX_DELAY_MS = 60000;
@@ -47,7 +48,7 @@ export const adminRetryRuleRoutes = (app, options, done) => {
47
48
  const body = request.body;
48
49
  const regexError = validateBodyPattern(body.body_pattern);
49
50
  if (regexError) {
50
- return reply.code(HTTP_BAD_REQUEST).send({ error: { message: regexError } });
51
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.INVALID_REGEX, regexError));
51
52
  }
52
53
  const id = createRetryRule(db, {
53
54
  name: body.name,
@@ -73,7 +74,7 @@ export const adminRetryRuleRoutes = (app, options, done) => {
73
74
  if (body.body_pattern !== undefined) {
74
75
  const regexError = validateBodyPattern(body.body_pattern);
75
76
  if (regexError) {
76
- return reply.code(HTTP_BAD_REQUEST).send({ error: { message: regexError } });
77
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.INVALID_REGEX, regexError));
77
78
  }
78
79
  fields.body_pattern = body.body_pattern;
79
80
  }
@@ -93,6 +94,9 @@ export const adminRetryRuleRoutes = (app, options, done) => {
93
94
  });
94
95
  app.delete("/admin/api/retry-rules/:id", async (request, reply) => {
95
96
  const { id } = request.params;
97
+ const existing = getRetryRuleById(db, id);
98
+ if (!existing)
99
+ return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Retry rule not found"));
96
100
  deleteRetryRule(db, id);
97
101
  refreshMatcher(matcher, db);
98
102
  return reply.send({ success: true });
@@ -4,6 +4,7 @@ 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
6
  import { HTTP_CREATED, HTTP_NOT_FOUND } from "./constants.js";
7
+ import { API_CODE, apiError } from "./api-response.js";
7
8
  const KEY_RANDOM_BYTES = 32;
8
9
  const KEY_PREFIX_LENGTH = 8;
9
10
  /** 归一化 allowed_models:null/空数组/仅含空字符串 → null(允许所有模型) */
@@ -67,7 +68,7 @@ export const adminRouterKeyRoutes = (app, options, done) => {
67
68
  const { id } = request.params;
68
69
  const existing = getRouterKeyById(db, id);
69
70
  if (!existing) {
70
- return reply.code(HTTP_NOT_FOUND).send({ error: { message: "Router key not found" } });
71
+ return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Router key not found"));
71
72
  }
72
73
  const body = request.body;
73
74
  const fields = {};
@@ -82,6 +83,9 @@ export const adminRouterKeyRoutes = (app, options, done) => {
82
83
  });
83
84
  app.delete("/admin/api/router-keys/:id", async (request, reply) => {
84
85
  const { id } = request.params;
86
+ const existing = getRouterKeyById(db, id);
87
+ if (!existing)
88
+ return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Router key not found"));
85
89
  deleteRouterKey(db, id);
86
90
  return reply.send({ success: true });
87
91
  });
@@ -13,6 +13,7 @@ import { adminMonitorRoutes } from "./monitor.js";
13
13
  import { adminSettingsRoutes } from "./settings.js";
14
14
  import { adminRecommendedRoutes } from "./recommended.js";
15
15
  import { adminUsageRoutes } from "./usage.js";
16
+ import { adminUpgradeRoutes } from "./upgrade.js";
16
17
  import { adminImportExportRoutes } from "./settings-import-export.js";
17
18
  export const adminRoutes = (app, options, done) => {
18
19
  // Setup 路由不需要 auth
@@ -33,5 +34,6 @@ export const adminRoutes = (app, options, done) => {
33
34
  app.register(adminImportExportRoutes, { db: options.db, matcher: options.matcher, semaphoreManager: options.semaphoreManager });
34
35
  app.register(adminRecommendedRoutes, { db: options.db });
35
36
  app.register(adminUsageRoutes, { db: options.db });
37
+ app.register(adminUpgradeRoutes, { db: options.db });
36
38
  done();
37
39
  };
@@ -3,6 +3,7 @@ import { getAllProviders, PROVIDER_CONCURRENCY_DEFAULTS } from "../db/index.js";
3
3
  import { encrypt, decrypt } from "../utils/crypto.js";
4
4
  import { getSetting } from "../db/settings.js";
5
5
  import { modelState } from "../proxy/model-state.js";
6
+ import { API_CODE, apiError } from "./api-response.js";
6
7
  const CONFIG_TABLES = [
7
8
  "providers",
8
9
  "mapping_groups",
@@ -58,10 +59,10 @@ export const adminImportExportRoutes = (app, options, done) => {
58
59
  app.post("/admin/api/settings/import", async (request, reply) => {
59
60
  const body = request.body;
60
61
  if (typeof body.version !== "number" || body.version !== EXPORT_VERSION) {
61
- return reply.code(BAD_REQUEST).send({ error: { message: `Unsupported version. Expected ${EXPORT_VERSION}.` } });
62
+ return reply.code(BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, `Unsupported version. Expected ${EXPORT_VERSION}.`));
62
63
  }
63
64
  if (!body.data || typeof body.data !== "object") {
64
- return reply.code(BAD_REQUEST).send({ error: { message: "Missing or invalid data field" } });
65
+ return reply.code(BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "Missing or invalid data field"));
65
66
  }
66
67
  const counts = {};
67
68
  const importData = body.data;