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