llm-simple-router 0.5.1 → 0.5.4

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 (205) hide show
  1. package/config/recommended-providers.json +291 -0
  2. package/config/recommended-retry-rules.json +10 -0
  3. package/dist/admin/api-response.d.ts +26 -0
  4. package/dist/admin/api-response.js +44 -0
  5. package/dist/admin/constants.d.ts +0 -2
  6. package/dist/admin/constants.js +0 -3
  7. package/dist/admin/groups.js +44 -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 +4 -1
  12. package/dist/admin/providers.js +201 -26
  13. package/dist/admin/proxy-enhancement.js +19 -14
  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/settings-import-export.js +3 -2
  18. package/dist/admin/settings.js +7 -5
  19. package/dist/admin/setup.js +3 -2
  20. package/dist/admin/stats.js +20 -3
  21. package/dist/admin/upgrade.js +8 -7
  22. package/dist/admin/usage.js +12 -24
  23. package/dist/config/model-context.d.ts +10 -0
  24. package/dist/config/model-context.js +105 -0
  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 +6 -4
  30. package/dist/db/index.js +4 -3
  31. package/dist/db/logs.d.ts +4 -0
  32. package/dist/db/logs.js +7 -3
  33. package/dist/db/mappings.d.ts +2 -1
  34. package/dist/db/mappings.js +7 -10
  35. package/dist/db/metrics.js +3 -4
  36. package/dist/db/migrations/023_create_provider_model_info.sql +8 -0
  37. package/dist/db/migrations/024_add_mapping_groups_is_active.sql +1 -0
  38. package/dist/db/migrations/025_add_client_status_code.sql +3 -0
  39. package/dist/db/model-info.d.ts +14 -0
  40. package/dist/db/model-info.js +27 -0
  41. package/dist/db/providers.d.ts +9 -0
  42. package/dist/db/providers.js +6 -0
  43. package/dist/db/retry-rules.d.ts +1 -0
  44. package/dist/db/retry-rules.js +3 -0
  45. package/dist/db/stats.d.ts +1 -2
  46. package/dist/db/stats.js +7 -11
  47. package/dist/index.js +64 -34
  48. package/dist/metrics/metrics-extractor.js +1 -1
  49. package/dist/metrics/sse-parser.js +2 -0
  50. package/dist/middleware/admin-auth.js +6 -5
  51. package/dist/middleware/auth.js +2 -11
  52. package/dist/monitor/request-tracker.d.ts +3 -0
  53. package/dist/monitor/request-tracker.js +27 -45
  54. package/dist/monitor/runtime-collector.js +1 -1
  55. package/dist/monitor/stream-content-accumulator.d.ts +14 -0
  56. package/dist/monitor/stream-content-accumulator.js +58 -0
  57. package/dist/proxy/anthropic.d.ts +2 -1
  58. package/dist/proxy/anthropic.js +16 -3
  59. package/dist/proxy/enhancement/directive-parser.d.ts +24 -0
  60. package/dist/proxy/enhancement/directive-parser.js +141 -0
  61. package/dist/proxy/enhancement/enhancement-handler.js +419 -0
  62. package/dist/proxy/enhancement/index.d.ts +3 -0
  63. package/dist/proxy/enhancement/index.js +3 -0
  64. package/dist/proxy/{response-cleaner.js → enhancement/response-cleaner.js} +14 -0
  65. package/dist/proxy/enhancement-config.d.ts +6 -0
  66. package/dist/proxy/enhancement-config.js +19 -0
  67. package/dist/proxy/log-helpers.d.ts +1 -1
  68. package/dist/proxy/mapping-resolver.js +4 -4
  69. package/dist/proxy/openai.d.ts +2 -1
  70. package/dist/proxy/openai.js +44 -7
  71. package/dist/proxy/orchestrator.d.ts +0 -1
  72. package/dist/proxy/orchestrator.js +1 -3
  73. package/dist/proxy/overflow.d.ts +18 -0
  74. package/dist/proxy/overflow.js +128 -0
  75. package/dist/proxy/patch/deepseek/index.d.ts +6 -0
  76. package/dist/proxy/patch/deepseek/index.js +11 -0
  77. package/dist/proxy/patch/deepseek/patch-orphan-tool-results.d.ts +12 -0
  78. package/dist/proxy/patch/deepseek/patch-orphan-tool-results.js +90 -0
  79. package/dist/proxy/patch/deepseek/patch-thinking-blocks.d.ts +6 -0
  80. package/dist/proxy/patch/deepseek/patch-thinking-blocks.js +24 -0
  81. package/dist/proxy/patch/index.d.ts +9 -0
  82. package/dist/proxy/patch/index.js +17 -0
  83. package/dist/proxy/proxy-core.d.ts +9 -6
  84. package/dist/proxy/proxy-core.js +24 -4
  85. package/dist/proxy/proxy-handler.d.ts +1 -1
  86. package/dist/proxy/proxy-handler.js +86 -141
  87. package/dist/proxy/proxy-logging.d.ts +0 -2
  88. package/dist/proxy/proxy-logging.js +24 -5
  89. package/dist/proxy/resilience.d.ts +9 -2
  90. package/dist/proxy/resilience.js +24 -8
  91. package/dist/proxy/strategy/failover.js +2 -7
  92. package/dist/proxy/strategy/random.js +2 -2
  93. package/dist/proxy/strategy/round-robin.js +2 -2
  94. package/dist/proxy/strategy/scheduled.js +1 -8
  95. package/dist/proxy/strategy/targets-rule.d.ts +1 -0
  96. package/dist/proxy/strategy/targets-rule.js +5 -0
  97. package/dist/proxy/strategy/types.d.ts +2 -0
  98. package/dist/proxy/stream-proxy.js +2 -1
  99. package/dist/proxy/transport-fn.d.ts +25 -0
  100. package/dist/proxy/transport-fn.js +56 -0
  101. package/dist/proxy/transport.d.ts +0 -25
  102. package/dist/proxy/transport.js +3 -40
  103. package/dist/proxy/types.d.ts +3 -1
  104. package/dist/proxy/types.js +5 -1
  105. package/dist/upgrade/checker.d.ts +1 -1
  106. package/dist/upgrade/checker.js +19 -1
  107. package/dist/upgrade/deployment.js +2 -2
  108. package/dist/utils/password.js +4 -2
  109. package/dist/utils/time-range.d.ts +9 -0
  110. package/dist/utils/time-range.js +55 -0
  111. package/frontend-dist/assets/CardContent-GNY_j_L3.js +1 -0
  112. package/frontend-dist/assets/CardTitle-BhXJbSoh.js +1 -0
  113. package/frontend-dist/assets/Checkbox-n_sh6Lvx.js +1 -0
  114. package/frontend-dist/assets/CollapsibleTrigger-DDCUOXDR.js +1 -0
  115. package/frontend-dist/assets/Collection-DbtqQ1jF.js +1 -0
  116. package/frontend-dist/assets/Dashboard-Dy9frcgO.js +3 -0
  117. package/frontend-dist/assets/DialogTitle-BEWUnuJQ.js +1 -0
  118. package/frontend-dist/assets/{Input-D0kpZB31.js → Input-CmibY9Fx.js} +1 -1
  119. package/frontend-dist/assets/Label-Cs__wFH0.js +1 -0
  120. package/frontend-dist/assets/Login-BciEc1TW.js +1 -0
  121. package/frontend-dist/assets/Logs-BkqwWW0-.js +1 -0
  122. package/frontend-dist/assets/ModelMappings-DrCJ_TCf.js +1 -0
  123. package/frontend-dist/assets/Monitor-C-b4qyuI.js +1 -0
  124. package/frontend-dist/assets/PopoverTrigger-DaKOMSVs.js +1 -0
  125. package/frontend-dist/assets/PopperContent-DZ6plcjf.js +1 -0
  126. package/frontend-dist/assets/Providers-u8utX74M.js +1 -0
  127. package/frontend-dist/assets/ProxyEnhancement-8_xhndGt.js +5 -0
  128. package/frontend-dist/assets/RetryRules-D1psYDEP.js +1 -0
  129. package/frontend-dist/assets/RouterKeys-ovPFGhjy.js +1 -0
  130. package/frontend-dist/assets/RovingFocusItem-Dsv9AkP7.js +1 -0
  131. package/frontend-dist/assets/SelectValue-BoUWfZAg.js +1 -0
  132. package/frontend-dist/assets/Settings-DXF-6A8C.js +6 -0
  133. package/frontend-dist/assets/Setup-rVLqiz0d.js +1 -0
  134. package/frontend-dist/assets/Switch-po5ZVBE3.js +1 -0
  135. package/frontend-dist/assets/TableHeader-Zyvq_0p2.js +1 -0
  136. package/frontend-dist/assets/TabsTrigger-CgDhZGkT.js +1 -0
  137. package/frontend-dist/assets/Teleport-CgTHarey.js +3 -0
  138. package/frontend-dist/assets/TooltipTrigger-C2qO21dQ.js +1 -0
  139. package/frontend-dist/assets/UnifiedRequestDialog-Dksad8eN.js +3 -0
  140. package/frontend-dist/assets/{VisuallyHidden-CjuTDGlC.js → VisuallyHidden-fbPmoMwi.js} +1 -1
  141. package/frontend-dist/assets/VisuallyHiddenInput-7j8wkPrW.js +1 -0
  142. package/frontend-dist/assets/alert-dialog-DbT3PzoF.js +1 -0
  143. package/frontend-dist/assets/badge-BVxnlnsH.js +1 -0
  144. package/frontend-dist/assets/{button-BmxhlpN-.js → button-BCrIpNwA.js} +2 -2
  145. package/frontend-dist/assets/chevron-down-CWBwGxSp.js +1 -0
  146. package/frontend-dist/assets/circle-question-mark-DRkkqjgG.js +1 -0
  147. package/frontend-dist/assets/dialog-BNlCZpHK.js +1 -0
  148. package/frontend-dist/assets/file-text-BavS6SrF.js +1 -0
  149. package/frontend-dist/assets/format-K3VR67cG.js +1 -0
  150. package/frontend-dist/assets/index-BP4imfye.css +1 -0
  151. package/frontend-dist/assets/index-DrBJPq6d.js +1 -0
  152. package/frontend-dist/assets/lib-CGpNhf06.js +1 -0
  153. package/frontend-dist/assets/loader-circle-Cpd89XQ7.js +1 -0
  154. package/frontend-dist/assets/ohash.D__AXeF1-DkJnWU8a.js +1 -0
  155. package/frontend-dist/assets/{useClipboard-CuE5xXIg.js → useClipboard-Bq8yZunx.js} +1 -1
  156. package/frontend-dist/assets/useLogRetention-BWPm3G_A.js +1 -0
  157. package/frontend-dist/assets/useNonce-D5lpSPNk.js +1 -0
  158. package/frontend-dist/assets/x-BFIp7DLt.js +1 -0
  159. package/frontend-dist/index.html +21 -18
  160. package/package.json +3 -1
  161. package/dist/proxy/directive-parser.d.ts +0 -7
  162. package/dist/proxy/directive-parser.js +0 -70
  163. package/dist/proxy/enhancement-handler.js +0 -169
  164. package/frontend-dist/assets/CardContent-Deyvo1TQ.js +0 -1
  165. package/frontend-dist/assets/CardTitle-DujSYXja.js +0 -1
  166. package/frontend-dist/assets/Checkbox-BJxf-QuV.js +0 -1
  167. package/frontend-dist/assets/CollapsibleTrigger-ByCvAsW0.js +0 -1
  168. package/frontend-dist/assets/Collection-V6gcBlwC.js +0 -1
  169. package/frontend-dist/assets/Dashboard-xqf6PcmE.js +0 -3
  170. package/frontend-dist/assets/DialogTitle-D0nwX87v.js +0 -1
  171. package/frontend-dist/assets/Label-BvYK0rd6.js +0 -1
  172. package/frontend-dist/assets/Login-C9oPKRcu.js +0 -1
  173. package/frontend-dist/assets/Logs-DVgenFav.js +0 -1
  174. package/frontend-dist/assets/ModelMappings-BoG2P9Rh.js +0 -1
  175. package/frontend-dist/assets/Monitor-W441wik3.js +0 -1
  176. package/frontend-dist/assets/PopperContent-DVJ4IxLF.js +0 -1
  177. package/frontend-dist/assets/Providers-D2rzb_Qk.js +0 -1
  178. package/frontend-dist/assets/ProxyEnhancement-DahQkV1g.js +0 -5
  179. package/frontend-dist/assets/RetryRules-Bg9p50oc.js +0 -1
  180. package/frontend-dist/assets/RouterKeys-C1LhXbqf.js +0 -1
  181. package/frontend-dist/assets/SelectValue-CAEBdE04.js +0 -1
  182. package/frontend-dist/assets/Settings-3lR8QVQt.js +0 -6
  183. package/frontend-dist/assets/Setup-Dzj1XvgF.js +0 -1
  184. package/frontend-dist/assets/Switch-CST3045A.js +0 -1
  185. package/frontend-dist/assets/TableHeader-CIrxcNRh.js +0 -1
  186. package/frontend-dist/assets/TabsContent-B4nroq3-.js +0 -1
  187. package/frontend-dist/assets/TabsTrigger-FsELRpyc.js +0 -1
  188. package/frontend-dist/assets/Teleport-DVgMe9KS.js +0 -3
  189. package/frontend-dist/assets/UnifiedRequestDialog-Fe2TfhTD.js +0 -3
  190. package/frontend-dist/assets/VisuallyHiddenInput-BaW-2aEF.js +0 -1
  191. package/frontend-dist/assets/alert-dialog-Bv6dVarS.js +0 -1
  192. package/frontend-dist/assets/badge-CEfcely6.js +0 -1
  193. package/frontend-dist/assets/createLucideIcon-UWoYUKtZ.js +0 -1
  194. package/frontend-dist/assets/dialog-QaGxKbze.js +0 -1
  195. package/frontend-dist/assets/file-text-D38GtYz2.js +0 -1
  196. package/frontend-dist/assets/format-CPdJtjZ5.js +0 -1
  197. package/frontend-dist/assets/index-CMBzqUyT.css +0 -1
  198. package/frontend-dist/assets/index-D484ZFa9.js +0 -1
  199. package/frontend-dist/assets/lib-CSYRBKqn.js +0 -1
  200. package/frontend-dist/assets/ohash.D__AXeF1-BUMsW586.js +0 -1
  201. package/frontend-dist/assets/useLogRetention-DesMKwIU.js +0 -1
  202. package/frontend-dist/assets/useNonce-FLqOooWA.js +0 -1
  203. package/frontend-dist/assets/x-BEUXSxcj.js +0 -1
  204. /package/dist/proxy/{enhancement-handler.d.ts → enhancement/enhancement-handler.d.ts} +0 -0
  205. /package/dist/proxy/{response-cleaner.d.ts → enhancement/response-cleaner.d.ts} +0 -0
@@ -0,0 +1,291 @@
1
+ [
2
+ {
3
+ "group": "DeepSeek",
4
+ "presets": [
5
+ {
6
+ "plan": "Anthropic",
7
+ "presetName": "deepseek",
8
+ "apiType": "anthropic",
9
+ "baseUrl": "https://api.deepseek.com/anthropic",
10
+ "models": ["deepseek-chat", "deepseek-reasoner"]
11
+ },
12
+ {
13
+ "plan": "OpenAI",
14
+ "presetName": "deepseek-openai",
15
+ "apiType": "openai",
16
+ "baseUrl": "https://api.deepseek.com",
17
+ "models": ["deepseek-chat", "deepseek-reasoner"]
18
+ }
19
+ ]
20
+ },
21
+ {
22
+ "group": "百度千帆",
23
+ "presets": [
24
+ {
25
+ "plan": "API",
26
+ "presetName": "qianfan",
27
+ "apiType": "openai",
28
+ "baseUrl": "https://qianfan.baidubce.com/v2",
29
+ "models": [
30
+ "ernie-4.0-8k",
31
+ "ernie-4.0-turbo-8k",
32
+ "ernie-3.5-8k",
33
+ "ernie-speed-8k",
34
+ "ernie-lite-8k",
35
+ "ernie-x1-32k-preview",
36
+ "deepseek-v3",
37
+ "deepseek-r1"
38
+ ]
39
+ }
40
+ ]
41
+ },
42
+ {
43
+ "group": "科大讯飞",
44
+ "presets": [
45
+ {
46
+ "plan": "API",
47
+ "presetName": "iflytek-spark",
48
+ "apiType": "openai",
49
+ "baseUrl": "https://spark-api-open.xf-yun.com/v1",
50
+ "models": [
51
+ "4.0Ultra",
52
+ "generalv3.5",
53
+ "max-32k",
54
+ "generalv3",
55
+ "pro-128k",
56
+ "lite"
57
+ ]
58
+ }
59
+ ]
60
+ },
61
+ {
62
+ "group": "硅基流动",
63
+ "presets": [
64
+ {
65
+ "plan": "API",
66
+ "presetName": "siliconflow",
67
+ "apiType": "openai",
68
+ "baseUrl": "https://api.siliconflow.cn/v1",
69
+ "models": [
70
+ "deepseek-ai/DeepSeek-V3.2-Exp",
71
+ "deepseek-ai/DeepSeek-R1",
72
+ "Qwen/Qwen3-8B",
73
+ "Qwen/Qwen2.5-72B-Instruct",
74
+ "Qwen/Qwen2.5-Coder-32B-Instruct",
75
+ "moonshotai/Kimi-K2-Instruct",
76
+ "moonshotai/Kimi-K2.5"
77
+ ]
78
+ }
79
+ ]
80
+ },
81
+ {
82
+ "group": "智谱",
83
+ "presets": [
84
+ {
85
+ "plan": "Coding Plan",
86
+ "presetName": "zhipu-coding-plan",
87
+ "apiType": "anthropic",
88
+ "baseUrl": "https://open.bigmodel.cn/api/anthropic",
89
+ "models": ["glm-5.1", "glm-5", "glm-4.7", "glm-4.5-air"]
90
+ },
91
+ {
92
+ "plan": "API",
93
+ "presetName": "zhipu",
94
+ "apiType": "openai",
95
+ "baseUrl": "https://open.bigmodel.cn/api/paas/v4",
96
+ "models": [
97
+ "glm-5.1",
98
+ "glm-5",
99
+ "glm-5-turbo",
100
+ "glm-4.7",
101
+ "glm-4.7-flash",
102
+ "glm-4.6"
103
+ ]
104
+ }
105
+ ]
106
+ },
107
+ {
108
+ "group": "KIMI",
109
+ "presets": [
110
+ {
111
+ "plan": "Coding Plan",
112
+ "presetName": "kimi-coding-plan",
113
+ "apiType": "anthropic",
114
+ "baseUrl": "https://api.kimi.com/coding",
115
+ "models": ["kimi-for-coding", "kimi-k2.5"]
116
+ },
117
+ {
118
+ "plan": "API",
119
+ "presetName": "kimi",
120
+ "apiType": "openai",
121
+ "baseUrl": "https://api.moonshot.cn",
122
+ "models": [
123
+ "kimi-k2.6",
124
+ "kimi-k2.5",
125
+ "kimi-k2-turbo-preview",
126
+ "kimi-k2-thinking",
127
+ "moonshot-v1-128k"
128
+ ]
129
+ }
130
+ ]
131
+ },
132
+ {
133
+ "group": "Minimax",
134
+ "presets": [
135
+ {
136
+ "plan": "Token Plan",
137
+ "presetName": "minimax-token-plan",
138
+ "apiType": "anthropic",
139
+ "baseUrl": "https://api.minimaxi.com/anthropic",
140
+ "models": ["MiniMax-M2.7"]
141
+ },
142
+ {
143
+ "plan": "API",
144
+ "presetName": "minimax",
145
+ "apiType": "openai",
146
+ "baseUrl": "https://api.minimax.chat",
147
+ "models": [
148
+ "MiniMax-M2.7",
149
+ "MiniMax-M2.7-highspeed",
150
+ "MiniMax-M2.5",
151
+ "MiniMax-M2.5-highspeed",
152
+ "MiniMax-M2.1",
153
+ "MiniMax-M2"
154
+ ]
155
+ }
156
+ ]
157
+ },
158
+ {
159
+ "group": "火山引擎",
160
+ "presets": [
161
+ {
162
+ "plan": "Coding Plan",
163
+ "presetName": "volcengine-coding-plan",
164
+ "apiType": "anthropic",
165
+ "baseUrl": "https://ark.cn-beijing.volces.com/api/compatible",
166
+ "models": [
167
+ "ark-code-latest",
168
+ "doubao-seed-2.0-code",
169
+ "kimi-k2.5",
170
+ "glm-4.7",
171
+ "deepseek-v3.2"
172
+ ]
173
+ },
174
+ {
175
+ "plan": "API",
176
+ "presetName": "volcengine",
177
+ "apiType": "openai",
178
+ "baseUrl": "https://ark.cn-beijing.volces.com/api/v3",
179
+ "models": [
180
+ "doubao-seed-2-0-pro-260215",
181
+ "doubao-seed-1-8-251228",
182
+ "doubao-seed-code-preview-251028"
183
+ ]
184
+ }
185
+ ]
186
+ },
187
+ {
188
+ "group": "阿里云",
189
+ "presets": [
190
+ {
191
+ "plan": "Coding Plan",
192
+ "presetName": "aliyun-coding-plan",
193
+ "apiType": "anthropic",
194
+ "baseUrl": "https://coding.dashscope.aliyuncs.com/apps/anthropic",
195
+ "models": [
196
+ "qwen3.6-plus",
197
+ "qwen3-coder-next",
198
+ "qwen3-coder-plus",
199
+ "kimi-k2.5",
200
+ "glm-5",
201
+ "MiniMax-M2.5"
202
+ ]
203
+ },
204
+ {
205
+ "plan": "API",
206
+ "presetName": "aliyun",
207
+ "apiType": "openai",
208
+ "baseUrl": "https://dashscope.aliyuncs.com/compatible-mode",
209
+ "models": [
210
+ "qwen3.6-plus",
211
+ "qwen3.5-plus",
212
+ "qwen3-max",
213
+ "qwen3.5-flash",
214
+ "qwen3-coder-plus",
215
+ "qwen3-coder-next"
216
+ ]
217
+ }
218
+ ]
219
+ },
220
+ {
221
+ "group": "腾讯云",
222
+ "presets": [
223
+ {
224
+ "plan": "Coding Plan",
225
+ "presetName": "tencent-coding-plan",
226
+ "apiType": "anthropic",
227
+ "baseUrl": "https://api.lkeap.cloud.tencent.com/coding/anthropic",
228
+ "models": [
229
+ "tc-code-latest",
230
+ "hunyuan-2.0-instruct",
231
+ "hunyuan-2.0-thinking",
232
+ "hunyuan-turbos",
233
+ "hunyuan-t1",
234
+ "glm-5",
235
+ "kimi-k2.5"
236
+ ]
237
+ },
238
+ {
239
+ "plan": "API",
240
+ "presetName": "tencent",
241
+ "apiType": "openai",
242
+ "baseUrl": "https://api.hunyuan.cloud.tencent.com",
243
+ "models": [
244
+ "hunyuan-2.0-thinking",
245
+ "hunyuan-2.0-instruct",
246
+ "hunyuan-t1-latest",
247
+ "hunyuan-a13b",
248
+ "hunyuan-turbos-latest"
249
+ ]
250
+ }
251
+ ]
252
+ },
253
+ {
254
+ "group": "OpenCode Go",
255
+ "presets": [
256
+ {
257
+ "plan": "Go",
258
+ "presetName": "opencode-go-anthropic",
259
+ "apiType": "anthropic",
260
+ "baseUrl": "https://opencode.ai/zen/go/v1/messages",
261
+ "models": ["deepseek-v4-pro", "deepseek-v4-flash", "minimax-m2.7", "minimax-m2.5"]
262
+ }
263
+ ]
264
+ },
265
+ {
266
+ "group": "阶跃星辰",
267
+ "presets": [
268
+ {
269
+ "plan": "Step Plan",
270
+ "presetName": "stepfun-step-plan",
271
+ "apiType": "anthropic",
272
+ "baseUrl": "https://api.stepfun.com/step_plan",
273
+ "models": ["step-3.5-flash-2603", "step-3.5-flash"]
274
+ },
275
+ {
276
+ "plan": "API",
277
+ "presetName": "stepfun",
278
+ "apiType": "openai",
279
+ "baseUrl": "https://api.stepfun.com",
280
+ "models": [
281
+ "step-3.5-flash",
282
+ "step-3",
283
+ "step-2-mini",
284
+ "step-2-16k",
285
+ "step-1-8k",
286
+ "step-1-32k"
287
+ ]
288
+ }
289
+ ]
290
+ }
291
+ ]
@@ -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,26 @@
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
+ export declare function statusToApiCode(status: number): number;
23
+ /** 判断是否为 Admin API 路由(需要信封包装) */
24
+ export declare function isAdminApiResponse(url: string, contentType?: string): boolean;
25
+ /** 构造错误响应 */
26
+ export declare function apiError(code: number, message: string): ApiResponse<null>;
@@ -0,0 +1,44 @@
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
+ const STATUS_BAD_REQUEST = 400;
19
+ const STATUS_UNAUTHORIZED = 401;
20
+ const STATUS_NOT_FOUND = 404;
21
+ const STATUS_CONFLICT = 409;
22
+ export function statusToApiCode(status) {
23
+ if (status === STATUS_BAD_REQUEST)
24
+ return API_CODE.BAD_REQUEST;
25
+ if (status === STATUS_UNAUTHORIZED)
26
+ return API_CODE.TOKEN_INVALID;
27
+ if (status === STATUS_NOT_FOUND)
28
+ return API_CODE.NOT_FOUND;
29
+ if (status === STATUS_CONFLICT)
30
+ return API_CODE.CONFLICT_NAME;
31
+ return API_CODE.INTERNAL_ERROR;
32
+ }
33
+ /** 判断是否为 Admin API 路由(需要信封包装) */
34
+ export function isAdminApiResponse(url, contentType) {
35
+ if (!url.startsWith('/admin/api/'))
36
+ return false;
37
+ if (contentType?.includes('text/event-stream'))
38
+ return false;
39
+ return true;
40
+ }
41
+ /** 构造错误响应 */
42
+ export function apiError(code, message) {
43
+ return { code, message, data: null };
44
+ }
@@ -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 }),
@@ -13,6 +14,23 @@ const UpdateGroupSchema = Type.Object({
13
14
  strategy: Type.Optional(Type.String({ minLength: 1 })),
14
15
  rule: Type.Optional(Type.String()),
15
16
  });
17
+ function validateOverflow(db, target, label) {
18
+ const hasOverflowProvider = !!target.overflow_provider_id;
19
+ const hasOverflowModel = !!target.overflow_model;
20
+ if (hasOverflowProvider && !hasOverflowModel) {
21
+ return `${label}: overflow_provider_id requires overflow_model`;
22
+ }
23
+ if (hasOverflowModel && !hasOverflowProvider) {
24
+ return `${label}: overflow_model requires overflow_provider_id`;
25
+ }
26
+ if (hasOverflowProvider) {
27
+ const p = getProviderById(db, target.overflow_provider_id);
28
+ if (!p) {
29
+ return `${label}: overflow_provider_id '${target.overflow_provider_id}' not found`;
30
+ }
31
+ }
32
+ return undefined;
33
+ }
16
34
  async function validateRule(db, strategy, ruleJson) {
17
35
  const VALID_STRATEGIES = new Set(Object.values(STRATEGY_NAMES));
18
36
  if (!VALID_STRATEGIES.has(strategy)) {
@@ -34,6 +52,9 @@ async function validateRule(db, strategy, ruleJson) {
34
52
  if (!defaultProvider) {
35
53
  return `provider_id '${r.default.provider_id}' not found`;
36
54
  }
55
+ const overflowErr = validateOverflow(db, r.default, "rule.default");
56
+ if (overflowErr)
57
+ return overflowErr;
37
58
  if (r.windows !== undefined && !Array.isArray(r.windows)) {
38
59
  return "rule.windows must be an array";
39
60
  }
@@ -47,6 +68,9 @@ async function validateRule(db, strategy, ruleJson) {
47
68
  if (!p) {
48
69
  return `window[${i}] provider_id '${w.target.provider_id}' not found`;
49
70
  }
71
+ const wOverflowErr = validateOverflow(db, w.target, `window[${i}]`);
72
+ if (wOverflowErr)
73
+ return wOverflowErr;
50
74
  }
51
75
  }
52
76
  }
@@ -68,6 +92,9 @@ async function validateRule(db, strategy, ruleJson) {
68
92
  if (!p) {
69
93
  return `targets[${i}] provider_id '${t.provider_id}' not found`;
70
94
  }
95
+ const overflowErr = validateOverflow(db, t, `targets[${i}]`);
96
+ if (overflowErr)
97
+ return overflowErr;
71
98
  }
72
99
  }
73
100
  return undefined;
@@ -82,7 +109,7 @@ export const adminGroupRoutes = (app, options, done) => {
82
109
  const body = request.body;
83
110
  const validationError = await validateRule(db, body.strategy, body.rule);
84
111
  if (validationError) {
85
- return reply.code(HTTP_BAD_REQUEST).send({ error: { message: validationError } });
112
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, validationError));
86
113
  }
87
114
  try {
88
115
  const id = createMappingGroup(db, {
@@ -94,7 +121,7 @@ export const adminGroupRoutes = (app, options, done) => {
94
121
  }
95
122
  catch (err) {
96
123
  if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
97
- return reply.code(HTTP_CONFLICT).send({ error: { message: "client_model already exists" } });
124
+ return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.CONFLICT_NAME, "client_model already exists"));
98
125
  }
99
126
  throw err;
100
127
  }
@@ -113,7 +140,7 @@ export const adminGroupRoutes = (app, options, done) => {
113
140
  const ruleJson = body.rule ?? findGroupRule(db, id);
114
141
  const validationError = await validateRule(db, strategy, ruleJson);
115
142
  if (validationError) {
116
- return reply.code(HTTP_BAD_REQUEST).send({ error: { message: validationError } });
143
+ return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, validationError));
117
144
  }
118
145
  try {
119
146
  updateMappingGroup(db, id, fields);
@@ -121,16 +148,28 @@ export const adminGroupRoutes = (app, options, done) => {
121
148
  }
122
149
  catch (err) {
123
150
  if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
124
- return reply.code(HTTP_CONFLICT).send({ error: { message: "client_model already exists" } });
151
+ return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.CONFLICT_NAME, "client_model already exists"));
125
152
  }
126
153
  throw err;
127
154
  }
128
155
  });
129
156
  app.delete("/admin/api/mapping-groups/:id", async (request, reply) => {
130
157
  const { id } = request.params;
158
+ const existing = getMappingGroupById(db, id);
159
+ if (!existing)
160
+ return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Mapping group not found"));
131
161
  deleteMappingGroup(db, id);
132
162
  return reply.send({ success: true });
133
163
  });
164
+ app.post("/admin/api/mapping-groups/:id/toggle", async (request, reply) => {
165
+ const { id } = request.params;
166
+ const existing = getMappingGroupById(db, id);
167
+ if (!existing)
168
+ return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Mapping group not found"));
169
+ const newActive = existing.is_active ? 0 : 1;
170
+ updateMappingGroup(db, id, { is_active: newActive });
171
+ return reply.send({ success: true, is_active: newActive });
172
+ });
134
173
  done();
135
174
  };
136
175
  function findGroupStrategy(db, id) {
@@ -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;
@@ -12,6 +13,8 @@ export const adminMonitorRoutes = (app, options, done) => {
12
13
  app.get("/admin/api/monitor/concurrency", async () => tracker.getConcurrency());
13
14
  app.get("/admin/api/monitor/runtime", async () => tracker.getRuntime());
14
15
  app.get("/admin/api/monitor/stream", (request, reply) => {
16
+ // hijack() 让 Fastify 完全放弃响应管理,避免 onSend hook 向 SSE 流注入信封 JSON
17
+ reply.hijack();
15
18
  reply.raw.writeHead(HTTP_OK, {
16
19
  "Content-Type": "text/event-stream",
17
20
  "Cache-Control": "no-cache",
@@ -26,7 +29,7 @@ export const adminMonitorRoutes = (app, options, done) => {
26
29
  const { id } = request.params;
27
30
  const req = tracker.getRequestById(id);
28
31
  if (!req)
29
- return reply.status(HTTP_NOT_FOUND).send({ error: "Not found" });
32
+ return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Not found"));
30
33
  return req;
31
34
  });
32
35
  done();