llm-simple-router 0.3.7 → 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 (149) hide show
  1. package/README.md +144 -75
  2. package/dist/admin/constants.d.ts +1 -8
  3. package/dist/admin/constants.js +2 -8
  4. package/dist/admin/logs.js +25 -4
  5. package/dist/admin/metrics.js +7 -3
  6. package/dist/admin/recommended.d.ts +7 -0
  7. package/dist/admin/recommended.js +25 -0
  8. package/dist/admin/router-keys.js +1 -2
  9. package/dist/admin/routes.js +4 -0
  10. package/dist/admin/usage.d.ts +7 -0
  11. package/dist/admin/usage.js +66 -0
  12. package/dist/cli.js +0 -0
  13. package/dist/config/recommended.d.ts +24 -0
  14. package/dist/config/recommended.js +30 -0
  15. package/dist/constants.d.ts +8 -0
  16. package/dist/constants.js +9 -0
  17. package/dist/db/index.d.ts +7 -5
  18. package/dist/db/index.js +4 -3
  19. package/dist/db/logs.d.ts +24 -33
  20. package/dist/db/logs.js +52 -17
  21. package/dist/db/metrics.d.ts +36 -3
  22. package/dist/db/metrics.js +57 -42
  23. package/dist/db/migrations/018_add_failover_field.sql +2 -0
  24. package/dist/db/migrations/019_create_usage_windows.sql +11 -0
  25. package/dist/db/retry-rules.d.ts +0 -5
  26. package/dist/db/retry-rules.js +0 -23
  27. package/dist/db/usage-windows.d.ts +19 -0
  28. package/dist/db/usage-windows.js +37 -0
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.js +11 -8
  31. package/dist/monitor/request-tracker.d.ts +6 -0
  32. package/dist/monitor/request-tracker.js +23 -54
  33. package/dist/monitor/stream-extractor.d.ts +11 -0
  34. package/dist/monitor/stream-extractor.js +51 -0
  35. package/dist/proxy/anthropic.js +19 -32
  36. package/dist/proxy/log-helpers.d.ts +11 -4
  37. package/dist/proxy/log-helpers.js +5 -3
  38. package/dist/proxy/openai.js +18 -34
  39. package/dist/proxy/orchestrator.d.ts +52 -0
  40. package/dist/proxy/orchestrator.js +100 -0
  41. package/dist/proxy/proxy-core.d.ts +14 -26
  42. package/dist/proxy/proxy-core.js +40 -337
  43. package/dist/proxy/proxy-handler.d.ts +18 -0
  44. package/dist/proxy/proxy-handler.js +223 -0
  45. package/dist/proxy/proxy-logging.d.ts +28 -0
  46. package/dist/proxy/proxy-logging.js +122 -0
  47. package/dist/proxy/resilience.d.ts +63 -0
  48. package/dist/proxy/resilience.js +188 -0
  49. package/dist/proxy/scope.d.ts +18 -0
  50. package/dist/proxy/scope.js +37 -0
  51. package/dist/proxy/semaphore.d.ts +9 -2
  52. package/dist/proxy/semaphore.js +34 -7
  53. package/dist/proxy/stream-proxy.d.ts +7 -0
  54. package/dist/proxy/stream-proxy.js +263 -0
  55. package/dist/proxy/{upstream-call.d.ts → transport.d.ts} +25 -18
  56. package/dist/proxy/transport.js +128 -0
  57. package/dist/proxy/types.d.ts +58 -0
  58. package/dist/proxy/types.js +30 -0
  59. package/dist/proxy/usage-window-tracker.d.ts +11 -0
  60. package/dist/proxy/usage-window-tracker.js +75 -0
  61. package/dist/utils/datetime.d.ts +4 -0
  62. package/dist/utils/datetime.js +10 -0
  63. package/frontend-dist/assets/CardContent-fmM_iiuR.js +1 -0
  64. package/frontend-dist/assets/CardHeader-BzzFzZ1B.js +1 -0
  65. package/frontend-dist/assets/CardTitle-09d7O-11.js +1 -0
  66. package/frontend-dist/assets/Checkbox-DH8iqXQd.js +1 -0
  67. package/frontend-dist/assets/CollapsibleTrigger-DCRRORrU.js +1 -0
  68. package/frontend-dist/assets/Collection-DY9-Yue9.js +3 -0
  69. package/frontend-dist/assets/Dashboard-BEzoZuSm.js +3 -0
  70. package/frontend-dist/assets/DialogTitle-BeMGJzYO.js +1 -0
  71. package/frontend-dist/assets/Input-BhvZ-Up7.js +1 -0
  72. package/frontend-dist/assets/Label-DjtouWZ7.js +1 -0
  73. package/frontend-dist/assets/LogDetailDialog-BjRsy_FR.js +3 -0
  74. package/frontend-dist/assets/Login-hOCPB-34.js +1 -0
  75. package/frontend-dist/assets/Logs-C5c3BJsg.js +1 -0
  76. package/frontend-dist/assets/ModelMappings-CDjxwyyz.js +1 -0
  77. package/frontend-dist/assets/Monitor-CPAvIREG.js +1 -0
  78. package/frontend-dist/assets/PopperContent-CHNw_qb6.js +1 -0
  79. package/frontend-dist/assets/Providers-C9ZAqHxO.js +1 -0
  80. package/frontend-dist/assets/ProxyEnhancement-Ct5WbiB7.js +5 -0
  81. package/frontend-dist/assets/RetryRules-CbgyrP6w.js +1 -0
  82. package/frontend-dist/assets/RouterKeys-zmqgFEKp.js +1 -0
  83. package/frontend-dist/assets/SelectValue-CP4Sh7LP.js +1 -0
  84. package/frontend-dist/assets/Setup-BXDEPt4o.js +1 -0
  85. package/frontend-dist/assets/Switch-DF6awXqs.js +1 -0
  86. package/frontend-dist/assets/TableHeader-BKE_yVML.js +1 -0
  87. package/frontend-dist/assets/TabsTrigger-D8R7lxaI.js +1 -0
  88. package/frontend-dist/assets/TooltipTrigger-BjQXeFem.js +1 -0
  89. package/frontend-dist/assets/VisuallyHidden-B_NnkONE.js +1 -0
  90. package/frontend-dist/assets/VisuallyHiddenInput-cjeTgyDe.js +1 -0
  91. package/frontend-dist/assets/alert-dialog-BoGRIC1Q.js +1 -0
  92. package/frontend-dist/assets/badge-DIO8W_W9.js +1 -0
  93. package/frontend-dist/assets/button-qxGNBunr.js +12 -0
  94. package/frontend-dist/assets/{createLucideIcon-CCmQ9QKM.js → createLucideIcon-jHUFhqKn.js} +1 -1
  95. package/frontend-dist/assets/dialog-D8pIXeSs.js +1 -0
  96. package/frontend-dist/assets/format-CPdJtjZ5.js +1 -0
  97. package/frontend-dist/assets/index-C_disqMY.js +1 -0
  98. package/frontend-dist/assets/index-DDp6SHfg.css +1 -0
  99. package/frontend-dist/assets/lib-DjpgwSRA.js +1 -0
  100. package/frontend-dist/assets/{ohash.D__AXeF1-p4vp6Svt.js → ohash.D__AXeF1-nmJ7gFbh.js} +1 -1
  101. package/frontend-dist/assets/{useClipboard-DO-38TXr.js → useClipboard-CmLp2YGk.js} +1 -1
  102. package/frontend-dist/assets/useForwardExpose-awoGXQkg.js +1 -0
  103. package/frontend-dist/assets/useNonce-_2e-GL-A.js +1 -0
  104. package/frontend-dist/assets/x-B0G-wIAB.js +1 -0
  105. package/frontend-dist/index.html +7 -7
  106. package/package.json +1 -1
  107. package/dist/admin/services.d.ts +0 -7
  108. package/dist/admin/services.js +0 -63
  109. package/dist/proxy/retry.d.ts +0 -43
  110. package/dist/proxy/retry.js +0 -121
  111. package/dist/proxy/upstream-call.js +0 -208
  112. package/frontend-dist/assets/CardContent-CucI6u41.js +0 -1
  113. package/frontend-dist/assets/CardHeader-d-DYsWxe.js +0 -1
  114. package/frontend-dist/assets/CardTitle-CIDEQkWB.js +0 -1
  115. package/frontend-dist/assets/Checkbox-CybCw3zS.js +0 -1
  116. package/frontend-dist/assets/CollapsibleTrigger-BFNhb19_.js +0 -1
  117. package/frontend-dist/assets/Collection-DUBb4r6h.js +0 -3
  118. package/frontend-dist/assets/Dashboard-DLB6iqH1.js +0 -3
  119. package/frontend-dist/assets/DialogTitle-Dq-5o7nJ.js +0 -1
  120. package/frontend-dist/assets/Input-HN3Il0-c.js +0 -1
  121. package/frontend-dist/assets/Label-CXAeFn-r.js +0 -1
  122. package/frontend-dist/assets/LogResponseViewer-CyBzv02a.js +0 -3
  123. package/frontend-dist/assets/Login-Br3qsdxf.js +0 -1
  124. package/frontend-dist/assets/Logs-Cu_IftdS.js +0 -1
  125. package/frontend-dist/assets/ModelMappings-DXC0sNH5.js +0 -1
  126. package/frontend-dist/assets/Monitor-CKlid1sC.js +0 -1
  127. package/frontend-dist/assets/PopperContent-CnZejY31.js +0 -1
  128. package/frontend-dist/assets/Providers-8CHhW4uH.js +0 -1
  129. package/frontend-dist/assets/ProxyEnhancement-CkYeXwgH.js +0 -5
  130. package/frontend-dist/assets/RetryRules-Csb7u9W4.js +0 -1
  131. package/frontend-dist/assets/RouterKeys-C6YIufmj.js +0 -1
  132. package/frontend-dist/assets/RovingFocusItem-B7ZIkplZ.js +0 -1
  133. package/frontend-dist/assets/SelectValue-B32pgmTJ.js +0 -1
  134. package/frontend-dist/assets/Setup-Df9IQo2x.js +0 -1
  135. package/frontend-dist/assets/Switch-CLeo7H6d.js +0 -1
  136. package/frontend-dist/assets/TableHeader-BpscAtT3.js +0 -1
  137. package/frontend-dist/assets/TabsTrigger-DErAbTuM.js +0 -1
  138. package/frontend-dist/assets/VisuallyHidden-CJBR3YB3.js +0 -1
  139. package/frontend-dist/assets/VisuallyHiddenInput-Cy0VuE1l.js +0 -1
  140. package/frontend-dist/assets/alert-dialog-BAR1JRmT.js +0 -1
  141. package/frontend-dist/assets/button-D54q76GQ.js +0 -1
  142. package/frontend-dist/assets/client-Mb8fy_bC.js +0 -12
  143. package/frontend-dist/assets/dialog-DSH5k5Kj.js +0 -1
  144. package/frontend-dist/assets/index-BQBtSfem.js +0 -1
  145. package/frontend-dist/assets/index-H-lnTkMr.css +0 -1
  146. package/frontend-dist/assets/lib-BgOqOzXI.js +0 -1
  147. package/frontend-dist/assets/useForwardExpose-CzQFheaD.js +0 -1
  148. package/frontend-dist/assets/useNonce-CU-NirfM.js +0 -1
  149. package/frontend-dist/assets/x-DEJ1xpi5.js +0 -1
package/README.md CHANGED
@@ -1,76 +1,90 @@
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/500/网络超时)自动指数退避重试
13
- - **多供应商模型映射** — 高峰期主模型不可用时,将 claude-opus 映射到 GLM,claude-sonnet 映射到 Kimi 等,低谷期再切回来
14
- - **多密钥隔离** — 为不同使用方分配独立密钥,按密钥筛选日志和性能指标
9
+ - Claude Code 配合国产模型(智谱、Moonshot、Minimax 等)的开发者
10
+ - 希望自动重试限流错误、分时段切换模型、控制并发排队
11
+ - 想要一个开箱即用的方案,不折腾
15
12
 
16
- ## 功能
13
+ ## 功能一览
17
14
 
18
15
  | 功能 | 说明 |
19
16
  |------|------|
20
- | 模型映射 | 客户端模型名 -> 后端模型名 + 供应商,支持分组和优先级 |
21
- | 自动重试 | 429/500/网络错误自动重试,指数退避,可配置次数和间隔 |
22
- | 多供应商 | 配置多个后端供应商,按模型映射路由 |
23
- | 多密钥 (Router Keys) | 为不同使用方创建独立密钥,支持模型白名单 |
24
- | 流式代理 | 完整支持 SSE 流式和非流式请求 |
25
- | 供应商并发控制 | Provider 维度限制并发数、队列长度和超时,防止单一供应商过载 |
26
- | 实时监控 | SSE 推送活跃请求、延迟热力图、Token 吞吐、运行时资源指标 |
27
- | 代理增强 (实验性) | 注入系统指令、会话记忆、模型锁定等增强功能 |
28
- | 管理后台 | Vue 3 + shadcn-vue Web UI,管理供应商、映射、密钥 |
29
- | 请求日志 | 结构化展示完整四阶段链路(客户端请求/上游请求/上游响应/客户端响应),适配 Claude Code 请求格式 |
30
- | 性能指标 | 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 用量、缓存命中率 |
31
26
 
32
27
  > **API 兼容性:** 支持 Anthropic 兼容 API(已适配 Claude Code)。OpenAI 兼容 API(`/v1/chat/completions`)尚未充分测试。
33
28
 
34
- ## 管理后台预览
29
+ ## 管理后台
35
30
 
36
- | Dashboard | Provider 管理 |
37
- |-----------|-------------|
38
- | ![Dashboard](docs/screenshot/dashboard.png) | ![Provider](docs/screenshot/provider.png) |
31
+ | Provider 管理 + 并发控制 | 实时监控 |
32
+ |---|---|
33
+ | ![Provider](docs/screenshot/provider_concurrency.png) | ![Monitor](docs/screenshot/monitor.png) |
39
34
 
40
- | 供应商并发控制 | 实时监控 |
41
- |--------------|---------|
42
- | ![Provider Concurrency](docs/screenshot/provider_concurrency.png) | ![Monitor](docs/screenshot/monitor.png) |
35
+ | 模型映射 | 重试规则 |
36
+ |---|---|
37
+ | ![Mapping](docs/screenshot/model_mapping.png) | ![Retry](docs/screenshot/retry.png) |
38
+
39
+ | Dashboard | 请求日志 |
40
+ |---|---|
41
+ | ![Dashboard](docs/screenshot/dashboard.png) | ![Logs](docs/screenshot/log.png) |
43
42
 
44
43
  | 代理增强 (实验性) |
45
44
  |-----------------|
46
45
  | ![Proxy Enhancement](docs/screenshot/proxy_enhance.png) |
47
46
 
48
- | 模型映射 | 重试规则 |
49
- |---------|--------|
50
- | ![Model Mapping](docs/screenshot/model_mapping.png) | ![Retry](docs/screenshot/retry.png) |
51
-
52
- | 请求日志 |
53
- |---------|
54
- | ![Logs](docs/screenshot/log.png) |
47
+ ## 快速开始
55
48
 
56
- ## 工作原理
49
+ ### 1. 启动 Router
57
50
 
51
+ ```bash
52
+ npx llm-simple-router
58
53
  ```
59
- Claude Code -> Router (模型映射 + 自动重试) -> 智谱 GLM / Kimi / 其他供应商
60
- ```
61
54
 
62
- Router 根据模型映射找到后端供应商 -> 转发请求 -> 自动重试失败请求 -> 记录日志和性能指标 -> 返回响应。
55
+ 访问 http://localhost:9981/admin ,首次进入 Setup 页面设置管理员密码。数据存储在 `~/.llm-simple-router/`。
56
+
57
+ ### 2. 配置 Provider
58
+
59
+ 管理后台 > Provider 页面 > 添加 Provider。选择 Coding Plan 后会自动填写 Base URL,只需填入 API Key。
63
60
 
64
- ## 典型使用场景
61
+ ### 3. 配置模型映射
65
62
 
66
- ### Claude Code 配置
63
+ 管理后台 > 模型映射页面。示例配置:
67
64
 
68
- 通过环境变量将 Claude Code 指向 Router:
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 密钥,然后选择一种方式配置:
69
75
 
70
76
  **方式一:shell alias(推荐)**
71
77
 
72
78
  ```bash
73
- alias clodedev='ANTHROPIC_AUTH_TOKEN="<your-router-key>" ANTHROPIC_BASE_URL="http://127.0.0.1:9981" claude'
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'
74
88
  ```
75
89
 
76
90
  **方式二:~/.claude/settings.json**
@@ -78,53 +92,108 @@ alias clodedev='ANTHROPIC_AUTH_TOKEN="<your-router-key>" ANTHROPIC_BASE_URL="htt
78
92
  ```json
79
93
  {
80
94
  "env": {
81
- "ANTHROPIC_AUTH_TOKEN": "sk-router-change-me",
95
+ "ANTHROPIC_AUTH_TOKEN": "<your-router-key>",
82
96
  "ANTHROPIC_BASE_URL": "http://127.0.0.1:9981",
83
- "ANTHROPIC_MODEL": "some-model"
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>"
84
102
  }
85
103
  }
86
104
  ```
87
105
 
88
- `<your-router-key>` 替换为管理后台中创建的 Router Key。
89
-
90
- ### 管理后台配置模型映射
106
+ ### 5. 使用
91
107
 
92
- | 客户端模型 | 后端模型 | 供应商 | 时间窗口 |
93
- |-----------|---------|--------|---------|
94
- | opus | glm-5.1 | 智谱 | / |
95
- | sonnet | glm-5.1 | 智谱 | / |
96
- | sonnet | kimi-for-coding | Moonshot | 14:00-18:00 |
97
- | sonnet | glm-5-turbo | 智谱 | / |
108
+ ```bash
109
+ # 方式一用户直接用 alias
110
+ clode
98
111
 
99
- 高峰期 GLM 3倍用量,且频繁超限时,将 sonnet 切到 Kimi;低谷期切回 GLM。
112
+ # 方式二用户正常启动 claude
113
+ claude
114
+ ```
100
115
 
101
- ## 快速开始
116
+ ## Docker 部署
102
117
 
103
118
  ```bash
104
- # 一行命令启动
105
- npx llm-simple-router
106
- # 访问 http://localhost:9981/admin
107
- # 首次访问会进入 Setup 页面设置管理员密码
119
+ docker compose up -d
108
120
  ```
109
121
 
110
- 无需任何环境变量。数据默认存储在 `~/.llm-simple-router/`。
122
+ 环境变量通过 Setup 页面设置,不需要 `.env` 文件。
111
123
 
112
- ## Docker 部署
124
+ ## 工作原理
113
125
 
114
- ```bash
115
- docker compose up -d
126
+ ```
127
+ Claude Code Router (模型映射 + 自动重试 + 并发控制) → 智谱 GLM / Kimi / 其他供应商
128
+ ```
129
+
130
+ Router 根据模型映射找到后端供应商 → 转发请求 → 自动重试失败请求 → 记录日志和性能指标 → 返回响应。
131
+
132
+ ### 架构图
133
+
134
+ **系统上下文**([详细说明](docs/system-context.md)):
135
+
136
+ ```mermaid
137
+ graph LR
138
+ Clients["Claude Code / Cursor / 其他客户端"]
139
+ Admin["管理员"]
140
+ Router>"LLM Simple Router"]
141
+ Providers>"智谱 / Moonshot / OpenAI / Anthropic / ..."]
142
+
143
+ Clients -->|"API 请求<br/>Bearer Token"| Router
144
+ Admin -->|"管理后台<br/>/admin/"| Router
145
+ Router -->|"转发请求<br/>SSE 流式"| Providers
116
146
  ```
117
147
 
148
+ **请求处理流水线**([详细说明](docs/request-pipeline.md)):
149
+
150
+ ```mermaid
151
+ flowchart LR
152
+ A[客户端请求] --> B[认证]
153
+ B --> C[模型映射<br/>+ 路由策略]
154
+ C --> D[并发排队]
155
+ D --> E[调用上游<br/>失败自动重试]
156
+ E --> F[记录日志<br/>+ 指标]
157
+ F --> G[返回响应]
158
+
159
+ E -.->|失败| C
160
+ ```
161
+
162
+ Router 收到请求后:认证 → 按映射规则找到后端 Provider → 排队控制并发 → 转发到上游(失败自动重试,Failover 策略下会切换 Provider)→ 记录日志和指标 → 返回响应。
163
+
118
164
  ## 环境变量
119
165
 
120
- 所有密钥(管理员密码、加密密钥、JWT 密钥)通过首次启动的 Setup 页面设置,无需环境变量。
121
-
122
- | 变量 | 必需 | 默认值 | 说明 |
123
- |------|------|--------|------|
124
- | `PORT` | No | `9981` | 服务端口 |
125
- | `DB_PATH` | No | `~/.llm-simple-router/router.db` | SQLite 数据库路径 |
126
- | `LOG_LEVEL` | No | `info` | 日志级别 |
127
- | `TZ` | No | `Asia/Shanghai` | 时区设置 |
128
- | `STREAM_TIMEOUT_MS` | No | `3000000` | 流式代理空闲超时(ms) |
129
- | `RETRY_MAX_ATTEMPTS` | No | `3` | 最大重试次数 |
130
- | `RETRY_BASE_DELAY_MS` | No | `1000` | 重试基础延迟(ms) |
166
+ 所有密钥通过 Setup 页面设置,以下为可选配置:
167
+
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) |
177
+
178
+ ## 开发
179
+
180
+ ```bash
181
+ # 后端(热重载)
182
+ npm run dev
183
+
184
+ # 前端(热重载,代理 API 到后端 :9980)
185
+ cd frontend && npm run dev
186
+
187
+ # 构建
188
+ npm run build:full
189
+
190
+ # 测试
191
+ npm test
192
+
193
+ # Lint
194
+ npm run lint
195
+ ```
196
+
197
+ ## License
198
+
199
+ MIT
@@ -1,10 +1,3 @@
1
1
  import type { FastifyReply } from "fastify";
2
- export declare const HTTP_BAD_REQUEST = 400;
3
- export declare const HTTP_CREATED = 201;
4
- export declare const HTTP_FORBIDDEN = 403;
5
- export declare const HTTP_NOT_FOUND = 404;
6
- export declare const HTTP_CONFLICT = 409;
7
- export declare const HTTP_INTERNAL_ERROR = 500;
8
- export declare const HTTP_BAD_GATEWAY = 502;
9
- export declare const HTTP_SERVICE_UNAVAILABLE = 503;
2
+ export { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_INTERNAL_ERROR, HTTP_BAD_GATEWAY, HTTP_SERVICE_UNAVAILABLE, } from "../constants.js";
10
3
  export declare function sendErrorResponse(reply: FastifyReply, statusCode: number, message: string): FastifyReply<import("fastify").RouteGenericInterface, import("fastify").RawServerDefault, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, unknown, import("fastify").FastifySchema, import("fastify").FastifyTypeProviderDefault, unknown>;
@@ -1,11 +1,5 @@
1
- export const HTTP_BAD_REQUEST = 400;
2
- export const HTTP_CREATED = 201;
3
- export const HTTP_FORBIDDEN = 403;
4
- export const HTTP_NOT_FOUND = 404;
5
- export const HTTP_CONFLICT = 409;
6
- export const HTTP_INTERNAL_ERROR = 500;
7
- export const HTTP_BAD_GATEWAY = 502;
8
- export const HTTP_SERVICE_UNAVAILABLE = 503;
1
+ // HTTP 状态码统一从 src/constants.ts 导入,避免重复定义
2
+ export { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_INTERNAL_ERROR, HTTP_BAD_GATEWAY, HTTP_SERVICE_UNAVAILABLE, } from "../constants.js";
9
3
  export function sendErrorResponse(reply, statusCode, message) {
10
4
  return reply.code(statusCode).send({ error: { message } });
11
5
  }
@@ -1,5 +1,5 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import { getRequestLogs, getRequestLogById, deleteLogsBefore } from "../db/index.js";
2
+ import { getRequestLogs, getRequestLogsGrouped, getRequestLogById, getRequestLogChildren, deleteLogsBefore } from "../db/index.js";
3
3
  import { HTTP_NOT_FOUND } from "./constants.js";
4
4
  const LogQuerySchema = Type.Object({
5
5
  page: Type.Optional(Type.String()),
@@ -7,23 +7,35 @@ 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()),
13
+ view: Type.Optional(Type.Literal("grouped")),
10
14
  });
11
15
  const DeleteLogsBeforeSchema = Type.Object({
12
16
  before: Type.String({ minLength: 1 }),
13
17
  });
18
+ const DEFAULT_LOG_VIEW = "flat";
14
19
  export const adminLogRoutes = (app, options, done) => {
15
20
  const { db } = options;
16
21
  app.get("/admin/api/logs", { schema: { querystring: LogQuerySchema } }, async (request, reply) => {
17
22
  const query = request.query;
18
23
  const page = parseInt(query.page || "1", 10);
19
24
  const limit = parseInt(query.limit || "20", 10);
20
- const result = getRequestLogs(db, {
25
+ const view = query.view || DEFAULT_LOG_VIEW;
26
+ const listOptions = {
21
27
  page,
22
28
  limit,
23
29
  api_type: query.api_type || undefined,
24
30
  model: query.model || undefined,
25
31
  router_key_id: query.router_key_id || undefined,
26
- });
32
+ provider_id: query.provider_id || undefined,
33
+ start_time: query.start_time || undefined,
34
+ end_time: query.end_time || undefined,
35
+ };
36
+ const result = view === "grouped"
37
+ ? getRequestLogsGrouped(db, listOptions)
38
+ : getRequestLogs(db, listOptions);
27
39
  return reply.send({ ...result, page, limit });
28
40
  });
29
41
  app.get("/admin/api/logs/:id", async (request, reply) => {
@@ -32,7 +44,16 @@ export const adminLogRoutes = (app, options, done) => {
32
44
  if (!log) {
33
45
  return reply.code(HTTP_NOT_FOUND).send({ error: { message: "Log not found" } });
34
46
  }
35
- return reply.send(log);
47
+ return reply.send({ data: log });
48
+ });
49
+ app.get("/admin/api/logs/:id/children", async (request, reply) => {
50
+ const params = request.params;
51
+ const parent = getRequestLogById(db, params.id);
52
+ if (!parent) {
53
+ return reply.code(HTTP_NOT_FOUND).send({ error: { message: "Log not found" } });
54
+ }
55
+ const rows = getRequestLogChildren(db, params.id);
56
+ return reply.send({ data: rows });
36
57
  });
37
58
  app.delete("/admin/api/logs/before", { schema: { body: DeleteLogsBeforeSchema } }, async (request, reply) => {
38
59
  const body = request.body;
@@ -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
+ };
@@ -3,8 +3,7 @@ import { Type } from "@sinclair/typebox";
3
3
  import { encrypt, decrypt } from "../utils/crypto.js";
4
4
  import { getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "../db/index.js";
5
5
  import { getSetting } from "../db/settings.js";
6
- const HTTP_CREATED = 201;
7
- const HTTP_NOT_FOUND = 404;
6
+ import { HTTP_CREATED, HTTP_NOT_FOUND } from "./constants.js";
8
7
  const KEY_RANDOM_BYTES = 32;
9
8
  const KEY_PREFIX_LENGTH = 8;
10
9
  /** 归一化 allowed_models:null/空数组/仅含空字符串 → null(允许所有模型) */
@@ -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
+ };
package/dist/cli.js CHANGED
File without changes
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ export declare const HTTP_BAD_REQUEST = 400;
2
+ export declare const HTTP_CREATED = 201;
3
+ export declare const HTTP_FORBIDDEN = 403;
4
+ export declare const HTTP_NOT_FOUND = 404;
5
+ export declare const HTTP_CONFLICT = 409;
6
+ export declare const HTTP_INTERNAL_ERROR = 500;
7
+ export declare const HTTP_BAD_GATEWAY = 502;
8
+ export declare const HTTP_SERVICE_UNAVAILABLE = 503;
@@ -0,0 +1,9 @@
1
+ // HTTP 状态码常量 — 全局唯一来源
2
+ export const HTTP_BAD_REQUEST = 400;
3
+ export const HTTP_CREATED = 201;
4
+ export const HTTP_FORBIDDEN = 403;
5
+ export const HTTP_NOT_FOUND = 404;
6
+ export const HTTP_CONFLICT = 409;
7
+ export const HTTP_INTERNAL_ERROR = 500;
8
+ export const HTTP_BAD_GATEWAY = 502;
9
+ export const HTTP_SERVICE_UNAVAILABLE = 503;
@@ -4,16 +4,18 @@ 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
- export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, insertMetrics, } from "./logs.js";
10
- export type { RequestLog, MetricsRow, MetricsInsert } from "./logs.js";
9
+ export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, } from "./logs.js";
10
+ export type { RequestLog, RequestLogGroupedRow, RequestLogListRow } from "./logs.js";
11
11
  export { getRouterKeyByHash, getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "./router-keys.js";
12
12
  export type { RouterKey } from "./router-keys.js";
13
- export { getMetricsSummary, getMetricsTimeseries } from "./metrics.js";
14
- export type { MetricsSummaryRow, MetricsTimeseriesRow, MetricsPeriod, MetricsMetric } from "./metrics.js";
13
+ export { getMetricsSummary, getMetricsTimeseries, insertMetrics } from "./metrics.js";
14
+ export type { MetricsSummaryRow, MetricsTimeseriesRow, MetricsPeriod, MetricsMetric, MetricsRow, MetricsInsert } from "./metrics.js";
15
15
  export { getStats } from "./stats.js";
16
16
  export type { Stats, StatsPeriod } from "./stats.js";
17
17
  export { getSetting, setSetting, isInitialized } from "./settings.js";
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";