llm-simple-router 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +128 -91
  2. package/dist/admin/logs.js +7 -1
  3. package/dist/admin/metrics.js +7 -3
  4. package/dist/admin/recommended.d.ts +7 -0
  5. package/dist/admin/recommended.js +25 -0
  6. package/dist/admin/routes.js +4 -0
  7. package/dist/admin/usage.d.ts +7 -0
  8. package/dist/admin/usage.js +66 -0
  9. package/dist/config/recommended.d.ts +24 -0
  10. package/dist/config/recommended.js +30 -0
  11. package/dist/db/index.d.ts +3 -1
  12. package/dist/db/index.js +2 -1
  13. package/dist/db/logs.d.ts +6 -0
  14. package/dist/db/logs.js +12 -0
  15. package/dist/db/metrics.d.ts +3 -3
  16. package/dist/db/metrics.js +50 -42
  17. package/dist/db/migrations/019_create_usage_windows.sql +11 -0
  18. package/dist/db/retry-rules.d.ts +0 -5
  19. package/dist/db/retry-rules.js +0 -36
  20. package/dist/db/usage-windows.d.ts +19 -0
  21. package/dist/db/usage-windows.js +37 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +8 -3
  24. package/dist/proxy/usage-window-tracker.d.ts +11 -0
  25. package/dist/proxy/usage-window-tracker.js +75 -0
  26. package/dist/utils/datetime.d.ts +4 -0
  27. package/dist/utils/datetime.js +10 -0
  28. package/frontend-dist/assets/CardContent-fmM_iiuR.js +1 -0
  29. package/frontend-dist/assets/CardHeader-BzzFzZ1B.js +1 -0
  30. package/frontend-dist/assets/CardTitle-09d7O-11.js +1 -0
  31. package/frontend-dist/assets/Checkbox-DH8iqXQd.js +1 -0
  32. package/frontend-dist/assets/CollapsibleTrigger-DCRRORrU.js +1 -0
  33. package/frontend-dist/assets/Collection-DY9-Yue9.js +3 -0
  34. package/frontend-dist/assets/Dashboard-BEzoZuSm.js +3 -0
  35. package/frontend-dist/assets/DialogTitle-BeMGJzYO.js +1 -0
  36. package/frontend-dist/assets/Input-BhvZ-Up7.js +1 -0
  37. package/frontend-dist/assets/Label-DjtouWZ7.js +1 -0
  38. package/frontend-dist/assets/LogDetailDialog-BjRsy_FR.js +3 -0
  39. package/frontend-dist/assets/Login-hOCPB-34.js +1 -0
  40. package/frontend-dist/assets/Logs-C5c3BJsg.js +1 -0
  41. package/frontend-dist/assets/ModelMappings-CDjxwyyz.js +1 -0
  42. package/frontend-dist/assets/Monitor-CPAvIREG.js +1 -0
  43. package/frontend-dist/assets/PopperContent-CHNw_qb6.js +1 -0
  44. package/frontend-dist/assets/Providers-C9ZAqHxO.js +1 -0
  45. package/frontend-dist/assets/ProxyEnhancement-Ct5WbiB7.js +5 -0
  46. package/frontend-dist/assets/RetryRules-CbgyrP6w.js +1 -0
  47. package/frontend-dist/assets/RouterKeys-zmqgFEKp.js +1 -0
  48. package/frontend-dist/assets/SelectValue-CP4Sh7LP.js +1 -0
  49. package/frontend-dist/assets/Setup-BXDEPt4o.js +1 -0
  50. package/frontend-dist/assets/Switch-DF6awXqs.js +1 -0
  51. package/frontend-dist/assets/TableHeader-BKE_yVML.js +1 -0
  52. package/frontend-dist/assets/TabsTrigger-D8R7lxaI.js +1 -0
  53. package/frontend-dist/assets/TooltipTrigger-BjQXeFem.js +1 -0
  54. package/frontend-dist/assets/VisuallyHidden-B_NnkONE.js +1 -0
  55. package/frontend-dist/assets/VisuallyHiddenInput-cjeTgyDe.js +1 -0
  56. package/frontend-dist/assets/alert-dialog-BoGRIC1Q.js +1 -0
  57. package/frontend-dist/assets/badge-DIO8W_W9.js +1 -0
  58. package/frontend-dist/assets/button-qxGNBunr.js +12 -0
  59. package/frontend-dist/assets/{createLucideIcon-DGZkBjcJ.js → createLucideIcon-jHUFhqKn.js} +1 -1
  60. package/frontend-dist/assets/dialog-D8pIXeSs.js +1 -0
  61. package/frontend-dist/assets/index-C_disqMY.js +1 -0
  62. package/frontend-dist/assets/index-DDp6SHfg.css +1 -0
  63. package/frontend-dist/assets/lib-DjpgwSRA.js +1 -0
  64. package/frontend-dist/assets/{ohash.D__AXeF1-B64hB831.js → ohash.D__AXeF1-nmJ7gFbh.js} +1 -1
  65. package/frontend-dist/assets/{useClipboard-CWc1cTDo.js → useClipboard-CmLp2YGk.js} +1 -1
  66. package/frontend-dist/assets/useForwardExpose-awoGXQkg.js +1 -0
  67. package/frontend-dist/assets/useNonce-_2e-GL-A.js +1 -0
  68. package/frontend-dist/assets/x-B0G-wIAB.js +1 -0
  69. package/frontend-dist/index.html +7 -7
  70. package/package.json +1 -1
  71. package/frontend-dist/assets/CardContent-CTnwqTdL.js +0 -1
  72. package/frontend-dist/assets/CardHeader-CfUeY7tk.js +0 -1
  73. package/frontend-dist/assets/CardTitle-CWiDwWqd.js +0 -1
  74. package/frontend-dist/assets/Checkbox-BxNz70R_.js +0 -1
  75. package/frontend-dist/assets/CollapsibleTrigger-Uz1aGdtH.js +0 -1
  76. package/frontend-dist/assets/Collection-1EHC87X5.js +0 -3
  77. package/frontend-dist/assets/Dashboard-C3FL30UN.js +0 -3
  78. package/frontend-dist/assets/DialogTitle-CAOFxr83.js +0 -1
  79. package/frontend-dist/assets/Input-DRIid2C6.js +0 -1
  80. package/frontend-dist/assets/Label-UyNN2jyE.js +0 -1
  81. package/frontend-dist/assets/LogDetailDialog-8BT4vIlV.js +0 -3
  82. package/frontend-dist/assets/Login-CnzH6TdS.js +0 -1
  83. package/frontend-dist/assets/Logs-CbK8NB_X.js +0 -1
  84. package/frontend-dist/assets/ModelMappings-DeRFgsYG.js +0 -1
  85. package/frontend-dist/assets/Monitor-Dd80bdUn.js +0 -1
  86. package/frontend-dist/assets/PopperContent-B3fZao7v.js +0 -1
  87. package/frontend-dist/assets/Providers-B_DbV-_y.js +0 -1
  88. package/frontend-dist/assets/ProxyEnhancement-up1fnPzq.js +0 -5
  89. package/frontend-dist/assets/RetryRules-Dkuhjh0u.js +0 -1
  90. package/frontend-dist/assets/RouterKeys-CvMMAa4t.js +0 -1
  91. package/frontend-dist/assets/RovingFocusItem-X0bfqWWS.js +0 -1
  92. package/frontend-dist/assets/SelectValue-zO8t-tx1.js +0 -1
  93. package/frontend-dist/assets/Setup-ByT2ThOQ.js +0 -1
  94. package/frontend-dist/assets/Switch-BEMjVugO.js +0 -1
  95. package/frontend-dist/assets/TableHeader-DpHWSnxK.js +0 -1
  96. package/frontend-dist/assets/TabsTrigger-Db6RqsZc.js +0 -1
  97. package/frontend-dist/assets/VisuallyHidden-hs8pj8OP.js +0 -1
  98. package/frontend-dist/assets/VisuallyHiddenInput-1m0nNADN.js +0 -1
  99. package/frontend-dist/assets/alert-dialog-PP91kaO8.js +0 -1
  100. package/frontend-dist/assets/button-Dcc0gF5i.js +0 -1
  101. package/frontend-dist/assets/client-DIIo9zPK.js +0 -12
  102. package/frontend-dist/assets/dialog-CxSyR-fN.js +0 -1
  103. package/frontend-dist/assets/index-BL-LAtac.css +0 -1
  104. package/frontend-dist/assets/index-CvT41fGL.js +0 -1
  105. package/frontend-dist/assets/lib-Bl0OuBjh.js +0 -1
  106. package/frontend-dist/assets/useForwardExpose-AkE0lq8y.js +0 -1
  107. package/frontend-dist/assets/useNonce-DGyPxdjq.js +0 -1
  108. package/frontend-dist/assets/x-BuUpx9Fr.js +0 -1
package/README.md CHANGED
@@ -1,64 +1,133 @@
1
1
  # LLM Simple Router
2
2
 
3
- > **Status: Active Development**
4
- >
5
- > 核心代理、模型映射、自动重试、多密钥管理、请求日志、性能指标已完成。
6
- > 代码规范 githook 检查已集成。欢迎试用和反馈。
3
+ LLM API 代理路由器。接收 Claude Code / Cursor 等客户端请求,通过模型映射和路由策略转发到配置的后端 Provider,支持流式(SSE)和非流式代理。
7
4
 
8
- ## 解决的核心问题
5
+ **解决的核心问题**:国产模型限流频繁、多供应商切换麻烦、并发控制缺失。
9
6
 
10
- 个人使用 Claude Code 配合国产模型时的实际痛点:
7
+ ## 适合谁
11
8
 
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 )。
9
+ - Claude Code 配合国产模型(智谱、Moonshot、Minimax 等)的开发者
10
+ - 希望自动重试限流错误、分时段切换模型、控制并发排队
11
+ - 想要一个开箱即用的方案,不折腾
17
12
 
18
- ## 其他功能
13
+ ## 功能一览
19
14
 
20
15
  | 功能 | 说明 |
21
16
  |------|------|
22
- | 多密钥 | 为不同使用方创建独立密钥,支持模型白名单 |
23
- | 代理增强 (实验性) | 支持通过 Claude Code 发送select-model指令直接变更 router 的模型,不经过 LLM 请求(实验中,可能有bug) |
24
- | 请求日志 | 结构化展示完整四阶段链路(客户端请求/上游请求/上游响应/客户端响应),适配 Claude Code 请求格式 |
25
- | 性能指标 | TTFT、吞吐量、Token 用量、缓存命中率,支持按模型/密钥筛选 |
17
+ | 自动重试 | 429/400/网络超时自动指数退避重试,默认针对智谱模型配置 |
18
+ | 多供应商支持 | 智谱、Moonshot、Minimax、火山引擎、阿里云、腾讯云等,Coding Plan 选择后自动填写 |
19
+ | 模型分时段映射 | 按时间段自动切换后端模型(如高峰期切到 Kimi,低谷期切回 GLM) |
20
+ | 并发队列等待 | Provider 配置并发数上限,超限请求排队等待 |
21
+ | Failover 故障转移 | 多 Provider 互备,失败自动切换下一个 |
22
+ | 实时请求监控 | SSE 推送活跃请求、队列状态、流式输出实时查看 |
23
+ | 多密钥管理 | 独立密钥 + 模型白名单,支持多用户/多项目 |
24
+ | 请求日志 | 四阶段完整链路(客户端请求/上游请求/上游响应/客户端响应) |
25
+ | 性能指标 | TTFT、TPS、Token 用量、缓存命中率 |
26
26
 
27
27
  > **API 兼容性:** 支持 Anthropic 兼容 API(已适配 Claude Code)。OpenAI 兼容 API(`/v1/chat/completions`)尚未充分测试。
28
28
 
29
- ## 管理后台预览
29
+ ## 管理后台
30
30
 
31
- | Provider 管理和并发控制 |
32
- |-----------|
33
- | ![Provider Concurrency](docs/screenshot/provider_concurrency.png) |
31
+ | Provider 管理 + 并发控制 | 实时监控 |
32
+ |---|---|
33
+ | ![Provider](docs/screenshot/provider_concurrency.png) | ![Monitor](docs/screenshot/monitor.png) |
34
34
 
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) |
35
+ | 模型映射 | 重试规则 |
36
+ |---|---|
37
+ | ![Mapping](docs/screenshot/model_mapping.png) | ![Retry](docs/screenshot/retry.png) |
46
38
 
47
39
  | Dashboard | 请求日志 |
48
- |--------------|---------|
40
+ |---|---|
49
41
  | ![Dashboard](docs/screenshot/dashboard.png) | ![Logs](docs/screenshot/log.png) |
50
42
 
51
43
  | 代理增强 (实验性) |
52
44
  |-----------------|
53
45
  | ![Proxy Enhancement](docs/screenshot/proxy_enhance.png) |
54
46
 
47
+ ## 快速开始
48
+
49
+ ### 1. 启动 Router
50
+
51
+ ```bash
52
+ npx llm-simple-router
53
+ ```
54
+
55
+ 访问 http://localhost:9981/admin ,首次进入 Setup 页面设置管理员密码。数据存储在 `~/.llm-simple-router/`。
56
+
57
+ ### 2. 配置 Provider
58
+
59
+ 管理后台 > Provider 页面 > 添加 Provider。选择 Coding Plan 后会自动填写 Base URL,只需填入 API Key。
60
+
61
+ ### 3. 配置模型映射
62
+
63
+ 管理后台 > 模型映射页面。示例配置:
64
+
65
+ | 客户端模型 | 后端模型 | 供应商 | 时间窗口 |
66
+ |-----------|---------|--------|---------|
67
+ | sonnet | glm-5.1 | 智谱 | 全天 |
68
+ | sonnet | kimi-for-coding | Moonshot | 14:00-18:00 |
69
+
70
+ 客户端模型是指 Claude Code 实际请求的模型名(由 `ANTHROPIC_MODEL` 等环境变量决定)。
71
+
72
+ ### 4. 配置 Claude Code
73
+
74
+ 在管理后台创建 Router API 密钥,然后选择一种方式配置:
75
+
76
+ **方式一:shell alias(推荐)**
77
+
78
+ ```bash
79
+ alias clode='\
80
+ export ANTHROPIC_AUTH_TOKEN="<your-router-key>" && \
81
+ export ANTHROPIC_BASE_URL="http://127.0.0.1:9981" && \
82
+ export ANTHROPIC_MODEL="<your-default-model>" && \
83
+ export ANTHROPIC_DEFAULT_OPUS_MODEL="<your-opus-model>" && \
84
+ export ANTHROPIC_DEFAULT_SONNET_MODEL="<your-sonnet-model>" && \
85
+ export ANTHROPIC_DEFAULT_HAIKU_MODEL="<your-haiku-model>" && \
86
+ export ANTHROPIC_SMALL_FAST_MODEL="<your-fast-model>" && \
87
+ claude'
88
+ ```
89
+
90
+ **方式二:~/.claude/settings.json**
91
+
92
+ ```json
93
+ {
94
+ "env": {
95
+ "ANTHROPIC_AUTH_TOKEN": "<your-router-key>",
96
+ "ANTHROPIC_BASE_URL": "http://127.0.0.1:9981",
97
+ "ANTHROPIC_MODEL": "<your-default-model>",
98
+ "ANTHROPIC_DEFAULT_OPUS_MODEL": "<your-opus-model>",
99
+ "ANTHROPIC_DEFAULT_SONNET_MODEL": "<your-sonnet-model>",
100
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL": "<your-haiku-model>",
101
+ "ANTHROPIC_SMALL_FAST_MODEL": "<your-fast-model>"
102
+ }
103
+ }
104
+ ```
105
+
106
+ ### 5. 使用
107
+
108
+ ```bash
109
+ # 方式一用户直接用 alias
110
+ clode
111
+
112
+ # 方式二用户正常启动 claude
113
+ claude
114
+ ```
115
+
116
+ ## Docker 部署
117
+
118
+ ```bash
119
+ docker compose up -d
120
+ ```
121
+
122
+ 环境变量通过 Setup 页面设置,不需要 `.env` 文件。
123
+
55
124
  ## 工作原理
56
125
 
57
126
  ```
58
- Claude Code -> Router (模型映射 + 自动重试 + 并发控制) -> 智谱 GLM / Kimi / 其他供应商
127
+ Claude Code Router (模型映射 + 自动重试 + 并发控制) 智谱 GLM / Kimi / 其他供应商
59
128
  ```
60
129
 
61
- Router 根据模型映射找到后端供应商 -> 转发请求 -> 自动重试失败请求 -> 记录日志和性能指标 -> 返回响应。
130
+ Router 根据模型映射找到后端供应商 转发请求 自动重试失败请求 记录日志和性能指标 返回响应。
62
131
 
63
132
  ### 架构图
64
133
 
@@ -89,74 +158,42 @@ flowchart LR
89
158
 
90
159
  E -.->|失败| C
91
160
  ```
161
+
92
162
  Router 收到请求后:认证 → 按映射规则找到后端 Provider → 排队控制并发 → 转发到上游(失败自动重试,Failover 策略下会切换 Provider)→ 记录日志和指标 → 返回响应。
93
163
 
94
- ## 快速开始
164
+ ## 环境变量
95
165
 
96
- ### npx 启动
97
- ```bash
98
- # 一行命令启动
99
- npx llm-simple-router
100
- # 访问 http://localhost:9981/admin
101
- # 首次访问会进入 Setup 页面设置管理员密码
102
- ```
166
+ 所有密钥通过 Setup 页面设置,以下为可选配置:
103
167
 
104
- 无需任何环境变量。数据默认存储在 `~/.llm-simple-router/`。
168
+ | 变量 | 默认值 | 说明 |
169
+ |------|--------|------|
170
+ | `PORT` | `9981` | 服务端口 |
171
+ | `DB_PATH` | `~/.llm-simple-router/router.db` | SQLite 数据库路径 |
172
+ | `LOG_LEVEL` | `info` | 日志级别 |
173
+ | `TZ` | `Asia/Shanghai` | 时区设置 |
174
+ | `STREAM_TIMEOUT_MS` | `3000000` | 流式代理空闲超时(ms) |
175
+ | `RETRY_MAX_ATTEMPTS` | `3` | 最大重试次数 |
176
+ | `RETRY_BASE_DELAY_MS` | `1000` | 重试基础延迟(ms) |
105
177
 
106
- ### Docker 部署
178
+ ## 开发
107
179
 
108
180
  ```bash
109
- docker compose up -d
110
- ```
111
- ### 创建 Router API 密钥
112
-
113
- 在管理后台创建一个 API 密钥,原先 Claude Code 密钥替换为 Router 密钥。
181
+ # 后端(热重载)
182
+ npm run dev
114
183
 
115
- **方式一:shell alias(推荐)**
184
+ # 前端(热重载,代理 API 到后端 :9980)
185
+ cd frontend && npm run dev
116
186
 
117
- ```bash
118
- alias clodedev='ANTHROPIC_AUTH_TOKEN="<your-router-key>" ANTHROPIC_BASE_URL="http://127.0.0.1:9981" claude'
119
- ```
187
+ # 构建
188
+ npm run build:full
120
189
 
121
- **方式二:~/.claude/settings.json**
190
+ # 测试
191
+ npm test
122
192
 
123
- ```json
124
- {
125
- "env": {
126
- "ANTHROPIC_AUTH_TOKEN": "sk-router-change-me",
127
- "ANTHROPIC_BASE_URL": "http://127.0.0.1:9981",
128
- "ANTHROPIC_MODEL": "some-model"
129
- }
130
- }
193
+ # Lint
194
+ npm run lint
131
195
  ```
132
196
 
133
- ### 管理后台配置 Provider
134
-
135
- Provider 支持快速配置,目前支持智谱、Moonshot、Minimax、火山引擎、阿里云、腾讯云等,Coding Plan 选择后会自动填写,只需要配置 API Key。
136
-
137
- ### 管理后台配置模型映射
138
-
139
- | 客户端模型 | 后端模型 | 供应商 | 时间窗口 |
140
- |-----------|---------|--------|---------|
141
- | opus | glm-5.1 | 智谱 | / |
142
- | sonnet | glm-5.1 | 智谱 | / |
143
- | sonnet | kimi-for-coding | Moonshot | 14:00-18:00 |
144
- | sonnet | glm-5-turbo | 智谱 | / |
145
-
146
- 客户端模型是指 Claude Code 实际请求发出的模型名。
147
- 如果你配置了 ANTHROPIC_MODEL 等变量,那应该以该变量为准。
148
- 个人的配置:高峰期 GLM 3倍用量,且频繁超限时,将 sonnet 切到 Kimi;低谷期切回 GLM。
149
-
150
- ## 环境变量
197
+ ## License
151
198
 
152
- 所有密钥(管理员密码、加密密钥、JWT 密钥)通过首次启动的 Setup 页面设置,无需环境变量。
153
-
154
- | 变量 | 必需 | 默认值 | 说明 |
155
- |------|------|--------|------|
156
- | `PORT` | No | `9981` | 服务端口 |
157
- | `DB_PATH` | No | `~/.llm-simple-router/router.db` | SQLite 数据库路径 |
158
- | `LOG_LEVEL` | No | `info` | 日志级别 |
159
- | `TZ` | No | `Asia/Shanghai` | 时区设置 |
160
- | `STREAM_TIMEOUT_MS` | No | `3000000` | 流式代理空闲超时(ms) |
161
- | `RETRY_MAX_ATTEMPTS` | No | `3` | 最大重试次数 |
162
- | `RETRY_BASE_DELAY_MS` | No | `1000` | 重试基础延迟(ms) |
199
+ MIT
@@ -7,6 +7,9 @@ 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
+ provider_id: Type.Optional(Type.String()),
11
+ start_time: Type.Optional(Type.String()),
12
+ end_time: Type.Optional(Type.String()),
10
13
  view: Type.Optional(Type.Literal("grouped")),
11
14
  });
12
15
  const DeleteLogsBeforeSchema = Type.Object({
@@ -26,6 +29,9 @@ export const adminLogRoutes = (app, options, done) => {
26
29
  api_type: query.api_type || undefined,
27
30
  model: query.model || undefined,
28
31
  router_key_id: query.router_key_id || undefined,
32
+ provider_id: query.provider_id || undefined,
33
+ start_time: query.start_time || undefined,
34
+ end_time: query.end_time || undefined,
29
35
  };
30
36
  const result = view === "grouped"
31
37
  ? getRequestLogsGrouped(db, listOptions)
@@ -38,7 +44,7 @@ export const adminLogRoutes = (app, options, done) => {
38
44
  if (!log) {
39
45
  return reply.code(HTTP_NOT_FOUND).send({ error: { message: "Log not found" } });
40
46
  }
41
- return reply.send(log);
47
+ return reply.send({ data: log });
42
48
  });
43
49
  app.get("/admin/api/logs/:id/children", async (request, reply) => {
44
50
  const params = request.params;
@@ -1,7 +1,7 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { getMetricsSummary, getMetricsTimeseries } from "../db/index.js";
3
3
  const PeriodEnum = Type.Union([
4
- Type.Literal("1h"), Type.Literal("6h"), Type.Literal("24h"),
4
+ Type.Literal("1h"), Type.Literal("5h"), Type.Literal("6h"), Type.Literal("24h"),
5
5
  Type.Literal("7d"), Type.Literal("30d"),
6
6
  ]);
7
7
  const MetricEnum = Type.Union([
@@ -15,6 +15,8 @@ const SummaryQuerySchema = Type.Object({
15
15
  provider_id: Type.Optional(Type.String()),
16
16
  backend_model: Type.Optional(Type.String()),
17
17
  router_key_id: Type.Optional(Type.String()),
18
+ start_time: Type.Optional(Type.String()),
19
+ end_time: Type.Optional(Type.String()),
18
20
  });
19
21
  const TimeseriesQuerySchema = Type.Object({
20
22
  period: Type.Optional(PeriodEnum),
@@ -22,19 +24,21 @@ const TimeseriesQuerySchema = Type.Object({
22
24
  provider_id: Type.Optional(Type.String()),
23
25
  backend_model: Type.Optional(Type.String()),
24
26
  router_key_id: Type.Optional(Type.String()),
27
+ start_time: Type.Optional(Type.String()),
28
+ end_time: Type.Optional(Type.String()),
25
29
  });
26
30
  export const adminMetricsRoutes = (app, options, done) => {
27
31
  app.get("/admin/api/metrics/summary", { schema: { querystring: SummaryQuerySchema } }, async (request, reply) => {
28
32
  const query = request.query;
29
33
  const period = (query.period ?? "24h");
30
- const summary = getMetricsSummary(options.db, period, query.provider_id, query.backend_model, query.router_key_id);
34
+ const summary = getMetricsSummary(options.db, period, query.provider_id, query.backend_model, query.router_key_id, query.start_time, query.end_time);
31
35
  return reply.send(summary);
32
36
  });
33
37
  app.get("/admin/api/metrics/timeseries", { schema: { querystring: TimeseriesQuerySchema } }, async (request, reply) => {
34
38
  const query = request.query;
35
39
  const period = (query.period ?? "24h");
36
40
  const metric = query.metric;
37
- const timeseries = getMetricsTimeseries(options.db, period, metric, query.provider_id, query.backend_model, query.router_key_id);
41
+ const timeseries = getMetricsTimeseries(options.db, period, metric, query.provider_id, query.backend_model, query.router_key_id, query.start_time, query.end_time);
38
42
  return reply.send(timeseries);
39
43
  });
40
44
  done();
@@ -0,0 +1,7 @@
1
+ import { FastifyPluginCallback } from "fastify";
2
+ import Database from "better-sqlite3";
3
+ interface RecommendedRoutesOptions {
4
+ db: Database.Database;
5
+ }
6
+ export declare const adminRecommendedRoutes: FastifyPluginCallback<RecommendedRoutesOptions>;
7
+ export {};
@@ -0,0 +1,25 @@
1
+ import { getRecommendedProviders, getRecommendedRetryRules, reloadConfig } from "../config/recommended.js";
2
+ export const adminRecommendedRoutes = (app, options, done) => {
3
+ const { db } = options;
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);
14
+ });
15
+ app.get("/admin/api/recommended/retry-rules", async (_req, reply) => {
16
+ const rules = getRecommendedRetryRules();
17
+ const existing = new Set(db.prepare("SELECT name FROM retry_rules").all().map((r) => r.name));
18
+ return reply.send(rules.filter((r) => !existing.has(r.name)));
19
+ });
20
+ app.post("/admin/api/recommended/reload", async (_req, reply) => {
21
+ reloadConfig();
22
+ return reply.send({ ok: true });
23
+ });
24
+ done();
25
+ };
@@ -10,6 +10,8 @@ import { adminProxyEnhancementRoutes } from "./proxy-enhancement.js";
10
10
  import { adminRouterKeyRoutes } from "./router-keys.js";
11
11
  import { adminSetupRoutes } from "./setup.js";
12
12
  import { adminMonitorRoutes } from "./monitor.js";
13
+ import { adminRecommendedRoutes } from "./recommended.js";
14
+ import { adminUsageRoutes } from "./usage.js";
13
15
  export const adminRoutes = (app, options, done) => {
14
16
  // Setup 路由不需要 auth
15
17
  app.register(adminSetupRoutes, { db: options.db });
@@ -25,5 +27,7 @@ export const adminRoutes = (app, options, done) => {
25
27
  app.register(adminMetricsRoutes, { db: options.db });
26
28
  app.register(adminProxyEnhancementRoutes, { db: options.db });
27
29
  app.register(adminMonitorRoutes, { tracker: options.tracker });
30
+ app.register(adminRecommendedRoutes, { db: options.db });
31
+ app.register(adminUsageRoutes, { db: options.db });
28
32
  done();
29
33
  };
@@ -0,0 +1,7 @@
1
+ import { FastifyPluginCallback } from "fastify";
2
+ import Database from "better-sqlite3";
3
+ interface UsageRoutesOptions {
4
+ db: Database.Database;
5
+ }
6
+ export declare const adminUsageRoutes: FastifyPluginCallback<UsageRoutesOptions>;
7
+ export {};
@@ -0,0 +1,66 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { getWindowsInRange, getWindowUsage } from "../db/usage-windows.js";
3
+ import { toSqliteDatetime } from "../utils/datetime.js";
4
+ const UsageQuerySchema = Type.Object({
5
+ router_key_id: Type.Optional(Type.String()),
6
+ });
7
+ function getMonday(date) {
8
+ const d = new Date(date);
9
+ const day = d.getDay();
10
+ // 周日 getDay()=0,需要回退到上周一;其余日期减到周一
11
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1); // eslint-disable-line no-magic-numbers
12
+ d.setDate(diff);
13
+ return d;
14
+ }
15
+ function getDailyUsage(db, start, end, routerKeyId) {
16
+ const routerKeyFilter = routerKeyId
17
+ ? " AND rl.router_key_id = ?"
18
+ : "";
19
+ const params = routerKeyId
20
+ ? [toSqliteDatetime(start), toSqliteDatetime(end), routerKeyId]
21
+ : [toSqliteDatetime(start), toSqliteDatetime(end)];
22
+ return db.prepare(`
23
+ SELECT
24
+ date(rm.created_at) AS date,
25
+ COUNT(*) AS request_count,
26
+ COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens,
27
+ COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens
28
+ FROM request_metrics rm
29
+ JOIN request_logs rl ON rl.id = rm.request_log_id
30
+ WHERE rm.is_complete = 1
31
+ AND rm.created_at >= datetime(?)
32
+ AND rm.created_at < datetime(?)
33
+ ${routerKeyFilter}
34
+ GROUP BY date(rm.created_at)
35
+ ORDER BY date ASC
36
+ `).all(...params);
37
+ }
38
+ export const adminUsageRoutes = (app, options, done) => {
39
+ const { db } = options;
40
+ app.get("/admin/api/usage/windows", { schema: { querystring: UsageQuerySchema } }, async (request) => {
41
+ const query = request.query;
42
+ const today = new Date();
43
+ today.setHours(0, 0, 0, 0);
44
+ const tomorrow = new Date(today);
45
+ tomorrow.setDate(tomorrow.getDate() + 1);
46
+ const windows = getWindowsInRange(db, toSqliteDatetime(today), toSqliteDatetime(tomorrow), query.router_key_id);
47
+ return windows.map(w => ({
48
+ window: w,
49
+ usage: getWindowUsage(db, w.start_time, w.end_time, query.router_key_id),
50
+ }));
51
+ });
52
+ app.get("/admin/api/usage/weekly", { schema: { querystring: UsageQuerySchema } }, async (request) => {
53
+ const query = request.query;
54
+ const now = new Date();
55
+ const monday = getMonday(now);
56
+ monday.setHours(0, 0, 0, 0);
57
+ return getDailyUsage(db, monday, now, query.router_key_id);
58
+ });
59
+ app.get("/admin/api/usage/monthly", { schema: { querystring: UsageQuerySchema } }, async (request) => {
60
+ const query = request.query;
61
+ const now = new Date();
62
+ const firstOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
63
+ return getDailyUsage(db, firstOfMonth, now, query.router_key_id);
64
+ });
65
+ done();
66
+ };
@@ -0,0 +1,24 @@
1
+ export interface ProviderPreset {
2
+ plan: string;
3
+ presetName: string;
4
+ apiType: 'openai' | 'anthropic';
5
+ baseUrl: string;
6
+ models: string[];
7
+ }
8
+ export interface ProviderGroup {
9
+ group: string;
10
+ presets: ProviderPreset[];
11
+ }
12
+ export interface RecommendedRetryRule {
13
+ name: string;
14
+ status_code: number;
15
+ body_pattern: string;
16
+ retry_strategy: 'fixed' | 'exponential';
17
+ retry_delay_ms: number;
18
+ max_retries: number;
19
+ max_delay_ms: number;
20
+ }
21
+ export declare function loadRecommendedConfig(dir?: string): void;
22
+ export declare function getRecommendedProviders(): ProviderGroup[];
23
+ export declare function getRecommendedRetryRules(): RecommendedRetryRule[];
24
+ export declare function reloadConfig(): void;
@@ -0,0 +1,30 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ let cachedProviders = [];
4
+ let cachedRetryRules = [];
5
+ let configDir = '';
6
+ export function loadRecommendedConfig(dir) {
7
+ configDir = dir ?? path.resolve(process.cwd(), 'config');
8
+ cachedProviders = loadJson('recommended-providers.json');
9
+ cachedRetryRules = loadJson('recommended-retry-rules.json');
10
+ }
11
+ export function getRecommendedProviders() {
12
+ return cachedProviders;
13
+ }
14
+ export function getRecommendedRetryRules() {
15
+ return cachedRetryRules;
16
+ }
17
+ export function reloadConfig() {
18
+ loadRecommendedConfig(configDir);
19
+ }
20
+ function loadJson(filename) {
21
+ const filePath = path.join(configDir, filename);
22
+ try {
23
+ if (!fs.existsSync(filePath))
24
+ return [];
25
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
26
+ }
27
+ catch {
28
+ return [];
29
+ }
30
+ }
@@ -4,7 +4,7 @@ export { getActiveProviders, getAllProviders, getProviderById, createProvider, u
4
4
  export type { Provider } from "./providers.js";
5
5
  export { getModelMapping, getAllModelMappings, createModelMapping, updateModelMapping, deleteModelMapping, getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getActiveProviderModels, resolveByProviderModel, } from "./mappings.js";
6
6
  export type { ModelMapping, MappingGroup, ProviderModelEntry } from "./mappings.js";
7
- export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, seedDefaultRules, } from "./retry-rules.js";
7
+ export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
8
8
  export type { RetryRule } from "./retry-rules.js";
9
9
  export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, } from "./logs.js";
10
10
  export type { RequestLog, RequestLogGroupedRow, RequestLogListRow } from "./logs.js";
@@ -17,3 +17,5 @@ export type { Stats, StatsPeriod } from "./stats.js";
17
17
  export { getSetting, setSetting, isInitialized } from "./settings.js";
18
18
  export { getSessionStates, getSessionState, getSessionHistory, upsertSessionState, insertSessionHistory, deleteSessionState, } from "./session-states.js";
19
19
  export type { SessionModelState, SessionModelHistory, UpsertSessionStateInput, InsertSessionHistoryInput } from "./session-states.js";
20
+ export { insertWindow, getLatestWindow, getWindowsInRange, getWindowUsage, } from "./usage-windows.js";
21
+ export type { UsageWindow, WindowUsage } from "./usage-windows.js";
package/dist/db/index.js CHANGED
@@ -38,10 +38,11 @@ export function initDatabase(dbPath) {
38
38
  // --- Re-export from per-table modules ---
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
- export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, seedDefaultRules, } from "./retry-rules.js";
41
+ export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
42
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
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";
48
+ export { insertWindow, getLatestWindow, getWindowsInRange, getWindowUsage, } from "./usage-windows.js";
package/dist/db/logs.d.ts CHANGED
@@ -54,6 +54,9 @@ export declare function getRequestLogs(db: Database.Database, options: {
54
54
  api_type?: string;
55
55
  model?: string;
56
56
  router_key_id?: string;
57
+ provider_id?: string;
58
+ start_time?: string;
59
+ end_time?: string;
57
60
  }): {
58
61
  data: RequestLogListRow[];
59
62
  total: number;
@@ -72,6 +75,9 @@ export declare function getRequestLogsGrouped(db: Database.Database, options: {
72
75
  api_type?: string;
73
76
  model?: string;
74
77
  router_key_id?: string;
78
+ provider_id?: string;
79
+ start_time?: string;
80
+ end_time?: string;
75
81
  }): {
76
82
  data: RequestLogGroupedRow[];
77
83
  total: number;
package/dist/db/logs.js CHANGED
@@ -25,6 +25,18 @@ function buildLogWhereClause(options, baseCondition) {
25
25
  where += " AND rl.router_key_id = ?";
26
26
  params.push(options.router_key_id);
27
27
  }
28
+ if (options.provider_id) {
29
+ where += " AND rl.provider_id = ?";
30
+ params.push(options.provider_id);
31
+ }
32
+ if (options.start_time) {
33
+ where += " AND rl.created_at >= ?";
34
+ params.push(options.start_time);
35
+ }
36
+ if (options.end_time) {
37
+ where += " AND rl.created_at <= ?";
38
+ params.push(options.end_time);
39
+ }
28
40
  return { where, params };
29
41
  }
30
42
  export function getRequestLogs(db, options) {
@@ -1,5 +1,5 @@
1
1
  import Database from "better-sqlite3";
2
- export type MetricsPeriod = "1h" | "6h" | "24h" | "7d" | "30d";
2
+ export type MetricsPeriod = "1h" | "5h" | "6h" | "24h" | "7d" | "30d";
3
3
  export type MetricsMetric = "ttft" | "tps" | "tokens" | "cache_rate" | "request_count" | "input_tokens" | "output_tokens" | "cache_hit_tokens";
4
4
  export interface MetricsRow {
5
5
  id: string;
@@ -48,10 +48,10 @@ export interface MetricsSummaryRow {
48
48
  total_cache_hit_tokens: number;
49
49
  cache_hit_rate: number | null;
50
50
  }
51
- export declare function getMetricsSummary(db: Database.Database, period: MetricsPeriod, providerId?: string, backendModel?: string, routerKeyId?: string): MetricsSummaryRow[];
51
+ export declare function getMetricsSummary(db: Database.Database, period: MetricsPeriod, providerId?: string, backendModel?: string, routerKeyId?: string, startTime?: string, endTime?: string): MetricsSummaryRow[];
52
52
  export interface MetricsTimeseriesRow {
53
53
  time_bucket: string;
54
54
  avg_value: number | null;
55
55
  count: number;
56
56
  }
57
- export declare function getMetricsTimeseries(db: Database.Database, period: MetricsPeriod, metric: MetricsMetric, providerId?: string, backendModel?: string, routerKeyId?: string): MetricsTimeseriesRow[];
57
+ export declare function getMetricsTimeseries(db: Database.Database, period: MetricsPeriod, metric: MetricsMetric, providerId?: string, backendModel?: string, routerKeyId?: string, startTime?: string, endTime?: string): MetricsTimeseriesRow[];