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