llm-simple-router 0.7.0 → 0.8.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 +69 -0
- package/dist/admin/routes.d.ts +2 -0
- package/dist/admin/routes.js +4 -2
- package/dist/admin/settings-import-export.d.ts +1 -0
- package/dist/admin/settings-import-export.js +7 -0
- package/dist/admin/transform-rules.d.ts +8 -0
- package/dist/admin/transform-rules.js +38 -0
- package/dist/admin/upgrade.d.ts +1 -0
- package/dist/admin/upgrade.js +36 -3
- package/dist/admin/usage.js +1 -1
- package/dist/core/container.d.ts +1 -0
- package/dist/core/container.js +1 -0
- package/dist/db/migrations/034_create_provider_transform_rules.sql +11 -0
- package/dist/db/transform-rules.d.ts +16 -0
- package/dist/db/transform-rules.js +51 -0
- package/dist/index.js +13 -1
- package/dist/metrics/sse-parser.d.ts +2 -0
- package/dist/metrics/sse-parser.js +4 -0
- package/dist/monitor/request-tracker.js +8 -1
- package/dist/monitor/types.d.ts +1 -1
- package/dist/proxy/enhancement/response-cleaner.js +14 -6
- package/dist/proxy/handler/openai.js +13 -4
- package/dist/proxy/handler/proxy-handler-utils.js +2 -7
- package/dist/proxy/handler/proxy-handler.js +72 -15
- package/dist/proxy/patch/deepseek/index.d.ts +15 -3
- package/dist/proxy/patch/deepseek/index.js +29 -6
- package/dist/proxy/patch/deepseek/patch-cache-control.d.ts +6 -0
- package/dist/proxy/patch/deepseek/patch-cache-control.js +30 -0
- package/dist/proxy/patch/deepseek/patch-non-deepseek-tools.d.ts +16 -0
- package/dist/proxy/patch/deepseek/patch-non-deepseek-tools.js +74 -0
- package/dist/proxy/patch/deepseek/patch-orphan-tool-results.d.ts +10 -1
- package/dist/proxy/patch/deepseek/patch-orphan-tool-results.js +58 -15
- package/dist/proxy/patch/deepseek/patch-thinking-blocks.d.ts +5 -1
- package/dist/proxy/patch/deepseek/patch-thinking-blocks.js +37 -4
- package/dist/proxy/patch/deepseek/patch-thinking-param.d.ts +6 -0
- package/dist/proxy/patch/deepseek/patch-thinking-param.js +32 -0
- package/dist/proxy/patch/deepseek/utils.d.ts +8 -0
- package/dist/proxy/patch/deepseek/utils.js +38 -0
- package/dist/proxy/patch/index.d.ts +2 -2
- package/dist/proxy/patch/index.js +50 -4
- package/dist/proxy/patch/router-cleanup.js +1 -24
- package/dist/proxy/patch/safe-sse-parser.d.ts +9 -0
- package/dist/proxy/patch/safe-sse-parser.js +16 -0
- package/dist/proxy/proxy-core.js +1 -0
- package/dist/proxy/proxy-logging.d.ts +1 -1
- package/dist/proxy/proxy-logging.js +3 -3
- package/dist/proxy/transform/id-utils.d.ts +3 -0
- package/dist/proxy/transform/id-utils.js +9 -0
- package/dist/proxy/transform/message-mapper.d.ts +15 -0
- package/dist/proxy/transform/message-mapper.js +173 -0
- package/dist/proxy/transform/plugin-registry.d.ts +23 -0
- package/dist/proxy/transform/plugin-registry.js +130 -0
- package/dist/proxy/transform/plugin-types.d.ts +46 -0
- package/dist/proxy/transform/plugin-types.js +15 -0
- package/dist/proxy/transform/provider-meta.d.ts +29 -0
- package/dist/proxy/transform/provider-meta.js +72 -0
- package/dist/proxy/transform/request-transform.d.ts +4 -0
- package/dist/proxy/transform/request-transform.js +151 -0
- package/dist/proxy/transform/response-transform.d.ts +4 -0
- package/dist/proxy/transform/response-transform.js +99 -0
- package/dist/proxy/transform/sanitize.d.ts +3 -0
- package/dist/proxy/transform/sanitize.js +24 -0
- package/dist/proxy/transform/stream-ant2oa.d.ts +20 -0
- package/dist/proxy/transform/stream-ant2oa.js +200 -0
- package/dist/proxy/transform/stream-oa2ant.d.ts +25 -0
- package/dist/proxy/transform/stream-oa2ant.js +201 -0
- package/dist/proxy/transform/stream-transform-base.d.ts +19 -0
- package/dist/proxy/transform/stream-transform-base.js +61 -0
- package/dist/proxy/transform/thinking-mapper.d.ts +4 -0
- package/dist/proxy/transform/thinking-mapper.js +15 -0
- package/dist/proxy/transform/tool-mapper.d.ts +8 -0
- package/dist/proxy/transform/tool-mapper.js +67 -0
- package/dist/proxy/transform/transform-coordinator.d.ts +11 -0
- package/dist/proxy/transform/transform-coordinator.js +32 -0
- package/dist/proxy/transform/types.d.ts +43 -0
- package/dist/proxy/transform/types.js +1 -0
- package/dist/proxy/transform/usage-mapper.d.ts +8 -0
- package/dist/proxy/transform/usage-mapper.js +46 -0
- package/dist/proxy/transport/stream.d.ts +1 -1
- package/dist/proxy/transport/stream.js +19 -10
- package/dist/proxy/transport/transport-fn.d.ts +3 -0
- package/dist/proxy/transport/transport-fn.js +11 -4
- package/dist/upgrade/deployment.d.ts +13 -0
- package/dist/upgrade/deployment.js +40 -0
- package/frontend-dist/assets/{CardContent-B_EIvwon.js → CardContent-WAXChVto.js} +1 -1
- package/frontend-dist/assets/{CardTitle-DHU-obrV.js → CardTitle-2TP7C40C.js} +1 -1
- package/frontend-dist/assets/{CascadingModelSelect-BFbt3-xP.js → CascadingModelSelect-CBe9pEx_.js} +1 -1
- package/frontend-dist/assets/{Checkbox-CNGgDj55.js → Checkbox-Bbf909ia.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-BIBFqVyy.js → CollapsibleTrigger-H2GGomkv.js} +1 -1
- package/frontend-dist/assets/{Collection-CYT_tSZn.js → Collection-BdkMCE5V.js} +1 -1
- package/frontend-dist/assets/{Dashboard-DHzoPggK.js → Dashboard-BQvc6U98.js} +1 -1
- package/frontend-dist/assets/{DialogTitle-C1gk4GN_.js → DialogTitle-CS0Nuvko.js} +1 -1
- package/frontend-dist/assets/{Input-CXpbIxXJ.js → Input-B56t8UfI.js} +1 -1
- package/frontend-dist/assets/{Label-BbOaRyGK.js → Label-CXRQoDIZ.js} +1 -1
- package/frontend-dist/assets/{Login-Cc2ocWjt.js → Login-DRNqP0bt.js} +1 -1
- package/frontend-dist/assets/{Logs-DMMr-_-8.js → Logs-DeJosCWl.js} +1 -1
- package/frontend-dist/assets/{ModelMappings-Cg8svYGw.js → ModelMappings-DV-NmdF7.js} +1 -1
- package/frontend-dist/assets/{Monitor-ps-VVzq3.js → Monitor-DwqkpkuK.js} +1 -1
- package/frontend-dist/assets/{PopoverTrigger-BA1XOIO0.js → PopoverTrigger-srlKRM2q.js} +1 -1
- package/frontend-dist/assets/{PopperContent--3HCLUEp.js → PopperContent-9j4ZA5oc.js} +1 -1
- package/frontend-dist/assets/Providers-oYOUgEsH.js +1 -0
- package/frontend-dist/assets/{ProxyEnhancement-BhlZ3pYo.js → ProxyEnhancement-3tQzXNGn.js} +1 -1
- package/frontend-dist/assets/{RetryRules-BxCAMxNM.js → RetryRules-rlrPpTd0.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-N-5AHCZf.js → RouterKeys-COpe69A8.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-BXHXOE-y.js → RovingFocusItem-DbXUYGXA.js} +1 -1
- package/frontend-dist/assets/{Schedules-oxUdLakf.js → Schedules-gszIRN_S.js} +1 -1
- package/frontend-dist/assets/{SelectValue-RQZKO7OV.js → SelectValue-CpY2uWSk.js} +1 -1
- package/frontend-dist/assets/{Settings-BlIRQ9y-.js → Settings-Xu6V0Sve.js} +1 -1
- package/frontend-dist/assets/{Setup-CmpQtMiu.js → Setup-BfcLFnBR.js} +1 -1
- package/frontend-dist/assets/{Switch-B3sVDDC7.js → Switch-DVfy7Q4A.js} +1 -1
- package/frontend-dist/assets/{TableHeader-Bpen86Ix.js → TableHeader-BqZo28x_.js} +1 -1
- package/frontend-dist/assets/{TabsTrigger-VNURbZR2.js → TabsTrigger-0L00h4oy.js} +1 -1
- package/frontend-dist/assets/{Teleport-CsHgwC1b.js → Teleport-Czq5P0IN.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-BHwxX9h-.js → TooltipTrigger-ChkMBqtC.js} +1 -1
- package/frontend-dist/assets/{UnifiedRequestDialog-3rdh4fhf.js → UnifiedRequestDialog-B3GxGgwz.js} +1 -1
- package/frontend-dist/assets/{VisuallyHidden-DCZ9DkoD.js → VisuallyHidden-DAFM-4dn.js} +1 -1
- package/frontend-dist/assets/{VisuallyHiddenInput-BxgnyBWj.js → VisuallyHiddenInput-BwfFtaqi.js} +1 -1
- package/frontend-dist/assets/{alert-dialog-LaLkz7c6.js → alert-dialog-vdmYFjPE.js} +1 -1
- package/frontend-dist/assets/arrow-down-Da-mukXD.js +1 -0
- package/frontend-dist/assets/{badge-BYEw6ni_.js → badge-0gUIxSR9.js} +1 -1
- package/frontend-dist/assets/{button-C5VcfpgV.js → button-CLKo-tBF.js} +2 -2
- package/frontend-dist/assets/check-CEd3A-kB.js +1 -0
- package/frontend-dist/assets/{copy-CmWRpuG-.js → copy-7NlsO7pN.js} +1 -1
- package/frontend-dist/assets/{dialog-BEKceMtO.js → dialog-DUkre9Rw.js} +1 -1
- package/frontend-dist/assets/{file-text-C_jIlYPS.js → file-text-Di1QQ-B6.js} +1 -1
- package/frontend-dist/assets/index-Be9MymBh.js +1 -0
- package/frontend-dist/assets/index-Bz_ZaXNn.css +1 -0
- package/frontend-dist/assets/{lib-Cj3cGvin.js → lib-CweCSowO.js} +1 -1
- package/frontend-dist/assets/loader-circle-Cb19pB9Z.js +1 -0
- package/frontend-dist/assets/{useClipboard-ZCpnVKiU.js → useClipboard-tSRRbabN.js} +1 -1
- package/frontend-dist/assets/{useFocusGuards-BjqTo_uk.js → useFocusGuards-BxD_AgQe.js} +1 -1
- package/frontend-dist/assets/useFormControl-DP5JWFRS.js +1 -0
- package/frontend-dist/assets/{useLogRetention-BZZvG6jh.js → useLogRetention-DeCxyOiV.js} +1 -1
- package/frontend-dist/assets/useNonce-DTHUE2m9.js +1 -0
- package/frontend-dist/assets/x-ztwrQbIz.js +1 -0
- package/frontend-dist/index.html +20 -20
- package/package.json +1 -1
- package/frontend-dist/assets/Providers-Bjhhw2O4.js +0 -1
- package/frontend-dist/assets/arrow-down-BpGORRs9.js +0 -1
- package/frontend-dist/assets/check-Bqx_fTQX.js +0 -1
- package/frontend-dist/assets/index-C2aljBfM.js +0 -1
- package/frontend-dist/assets/index-xjdbFKXJ.css +0 -1
- package/frontend-dist/assets/loader-circle-BLhRyZzy.js +0 -1
- package/frontend-dist/assets/useFormControl-D7vjbPSC.js +0 -1
- package/frontend-dist/assets/useNonce-BuusARRu.js +0 -1
- package/frontend-dist/assets/x-CUQFnWz8.js +0 -1
package/README.md
CHANGED
|
@@ -170,6 +170,75 @@ docker compose up -d
|
|
|
170
170
|
|
|
171
171
|
环境变量通过 Setup 页面设置,不需要 `.env` 文件。
|
|
172
172
|
|
|
173
|
+
## 进程管理
|
|
174
|
+
|
|
175
|
+
通过 Web UI 一键升级后,服务需要重启才能生效。推荐使用以下方式部署,确保进程崩溃或升级重启后自动恢复。
|
|
176
|
+
|
|
177
|
+
### PM2(推荐)
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# 安装 PM2
|
|
181
|
+
npm install -g pm2
|
|
182
|
+
|
|
183
|
+
# 全局安装 Router
|
|
184
|
+
npm install -g llm-simple-router
|
|
185
|
+
|
|
186
|
+
# 启动(PM2 自动重启崩溃的进程)
|
|
187
|
+
pm2 start llm-simple-router --name llm-router
|
|
188
|
+
|
|
189
|
+
# 查看日志
|
|
190
|
+
pm2 logs llm-router
|
|
191
|
+
|
|
192
|
+
# 设置开机自启
|
|
193
|
+
pm2 startup
|
|
194
|
+
pm2 save
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
升级流程:Web UI 一键升级 → 点击重启 → PM2 自动拉起新进程(< 1s 中断)。
|
|
198
|
+
|
|
199
|
+
### systemd(Linux 服务器)
|
|
200
|
+
|
|
201
|
+
创建服务文件 `/etc/systemd/system/llm-simple-router.service`:
|
|
202
|
+
|
|
203
|
+
```ini
|
|
204
|
+
[Unit]
|
|
205
|
+
Description=LLM Simple Router
|
|
206
|
+
After=network.target
|
|
207
|
+
|
|
208
|
+
[Service]
|
|
209
|
+
Type=simple
|
|
210
|
+
ExecStart=/usr/local/bin/llm-simple-router
|
|
211
|
+
Restart=always
|
|
212
|
+
RestartSec=3
|
|
213
|
+
Environment=PORT=9981
|
|
214
|
+
Environment=LOG_LEVEL=info
|
|
215
|
+
# 按需配置其他环境变量
|
|
216
|
+
# Environment=DB_PATH=/var/lib/llm-simple-router/router.db
|
|
217
|
+
|
|
218
|
+
[Install]
|
|
219
|
+
WantedBy=multi-user.target
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
> **注意**:`ExecStart` 路径取决于 Node.js 安装方式。用 `which llm-simple-router` 确认实际路径。
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
# 启用并启动
|
|
226
|
+
sudo systemctl enable llm-simple-router
|
|
227
|
+
sudo systemctl start llm-simple-router
|
|
228
|
+
|
|
229
|
+
# 查看状态和日志
|
|
230
|
+
sudo systemctl status llm-simple-router
|
|
231
|
+
journalctl -u llm-simple-router -f
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
升级流程:Web UI 一键升级 → 点击重启 → systemd 自动重启(< 1s 中断)。
|
|
235
|
+
|
|
236
|
+
### npx / 手动启动
|
|
237
|
+
|
|
238
|
+
无需额外配置。Web UI 升级并点击重启后,Router 会自动 spawn 新进程并退出旧进程。短暂中断约 1-2 秒。
|
|
239
|
+
|
|
240
|
+
> **注意**:如果直接 `Ctrl+C` 或终端关闭,服务不会自动恢复。建议生产环境使用 PM2 或 systemd。
|
|
241
|
+
|
|
173
242
|
## 工作原理
|
|
174
243
|
|
|
175
244
|
```
|
package/dist/admin/routes.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ interface AdminRoutesOptions {
|
|
|
10
10
|
adaptiveController?: AdaptiveConcurrencyController;
|
|
11
11
|
logFileWriter?: import("../storage/log-file-writer.js").LogFileWriter | null;
|
|
12
12
|
logsDir?: string;
|
|
13
|
+
pluginRegistry?: import("../proxy/transform/plugin-registry.js").PluginRegistry;
|
|
14
|
+
closeFn?: () => Promise<void>;
|
|
13
15
|
}
|
|
14
16
|
export declare const adminRoutes: FastifyPluginCallback<AdminRoutesOptions>;
|
|
15
17
|
export {};
|
package/dist/admin/routes.js
CHANGED
|
@@ -15,6 +15,7 @@ import { adminRecommendedRoutes } from "./recommended.js";
|
|
|
15
15
|
import { adminUsageRoutes } from "./usage.js";
|
|
16
16
|
import { adminUpgradeRoutes } from "./upgrade.js";
|
|
17
17
|
import { adminImportExportRoutes } from "./settings-import-export.js";
|
|
18
|
+
import { adminTransformRuleRoutes } from "./transform-rules.js";
|
|
18
19
|
import { adminScheduleRoutes } from "./schedules.js";
|
|
19
20
|
export const adminRoutes = (app, options, done) => {
|
|
20
21
|
// Setup 路由不需要 auth
|
|
@@ -33,9 +34,10 @@ export const adminRoutes = (app, options, done) => {
|
|
|
33
34
|
app.register(adminProxyEnhancementRoutes, { db: options.db, stateRegistry: options.stateRegistry });
|
|
34
35
|
app.register(adminMonitorRoutes, { tracker: options.tracker });
|
|
35
36
|
app.register(adminSettingsRoutes, { db: options.db, logsDir: options.logsDir });
|
|
36
|
-
app.register(adminImportExportRoutes, { db: options.db, stateRegistry: options.stateRegistry });
|
|
37
|
+
app.register(adminImportExportRoutes, { db: options.db, stateRegistry: options.stateRegistry, pluginRegistry: options.pluginRegistry });
|
|
37
38
|
app.register(adminRecommendedRoutes, { db: options.db });
|
|
38
39
|
app.register(adminUsageRoutes, { db: options.db });
|
|
39
|
-
app.register(adminUpgradeRoutes, { db: options.db });
|
|
40
|
+
app.register(adminUpgradeRoutes, { db: options.db, closeFn: options.closeFn ?? (async () => { }) });
|
|
41
|
+
app.register(adminTransformRuleRoutes, { db: options.db, pluginRegistry: options.pluginRegistry });
|
|
40
42
|
done();
|
|
41
43
|
};
|
|
@@ -4,6 +4,7 @@ import type { StateRegistry } from "../core/registry.js";
|
|
|
4
4
|
interface ImportExportOptions {
|
|
5
5
|
db: Database.Database;
|
|
6
6
|
stateRegistry: StateRegistry;
|
|
7
|
+
pluginRegistry?: import("../proxy/transform/plugin-registry.js").PluginRegistry;
|
|
7
8
|
}
|
|
8
9
|
export declare const adminImportExportRoutes: FastifyPluginCallback<ImportExportOptions>;
|
|
9
10
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "crypto";
|
|
2
|
+
import { resolve } from "path";
|
|
2
3
|
import { encrypt, decrypt } from "../utils/crypto.js";
|
|
3
4
|
import { getSetting } from "../db/settings.js";
|
|
4
5
|
import { API_CODE, apiError } from "./api-response.js";
|
|
@@ -12,6 +13,7 @@ const CONFIG_TABLES = [
|
|
|
12
13
|
"schedules",
|
|
13
14
|
"provider_model_info",
|
|
14
15
|
"session_model_history",
|
|
16
|
+
"provider_transform_rules",
|
|
15
17
|
];
|
|
16
18
|
// settings 表按 key 列的值过滤,不覆盖本地安全敏感配置
|
|
17
19
|
const PROTECTED_SETTING_KEYS = new Set(["admin_password_hash", "jwt_secret", "encryption_key"]);
|
|
@@ -129,6 +131,11 @@ export const adminImportExportRoutes = (app, options, done) => {
|
|
|
129
131
|
stateRegistry.removeAllProviders();
|
|
130
132
|
stateRegistry.clearModelState();
|
|
131
133
|
stateRegistry.reinitializeProviders();
|
|
134
|
+
// 刷新 transform plugin 缓存(从 DB 重新加载规则 + 扫描插件目录)
|
|
135
|
+
if (options.pluginRegistry) {
|
|
136
|
+
const pluginsDir = resolve(process.cwd(), "plugins/transform");
|
|
137
|
+
options.pluginRegistry.reload(options.db, pluginsDir);
|
|
138
|
+
}
|
|
132
139
|
return reply.send(counts);
|
|
133
140
|
});
|
|
134
141
|
done();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FastifyPluginCallback } from "fastify";
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
interface TransformRuleOptions {
|
|
4
|
+
db: Database.Database;
|
|
5
|
+
pluginRegistry?: import("../proxy/transform/plugin-registry.js").PluginRegistry;
|
|
6
|
+
}
|
|
7
|
+
export declare const adminTransformRuleRoutes: FastifyPluginCallback<TransformRuleOptions>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { resolve } from "path";
|
|
2
|
+
import { getTransformRule, upsertTransformRule, deleteTransformRule, getAllActiveRules } from "../db/transform-rules.js";
|
|
3
|
+
const ALLOWED_FIELDS = new Set([
|
|
4
|
+
"inject_headers", "request_defaults", "drop_fields", "field_overrides", "plugin_name", "is_active",
|
|
5
|
+
]);
|
|
6
|
+
export const adminTransformRuleRoutes = (app, options, done) => {
|
|
7
|
+
const { db } = options;
|
|
8
|
+
app.get("/admin/api/transform-rules/:providerId", async (req) => {
|
|
9
|
+
const { providerId } = req.params;
|
|
10
|
+
const rule = getTransformRule(db, providerId);
|
|
11
|
+
return { code: 0, message: "ok", data: rule };
|
|
12
|
+
});
|
|
13
|
+
app.put("/admin/api/transform-rules/:providerId", async (req) => {
|
|
14
|
+
const { providerId } = req.params;
|
|
15
|
+
const updates = {};
|
|
16
|
+
for (const [key, val] of Object.entries(req.body)) {
|
|
17
|
+
if (ALLOWED_FIELDS.has(key))
|
|
18
|
+
updates[key] = val;
|
|
19
|
+
}
|
|
20
|
+
upsertTransformRule(db, providerId, updates);
|
|
21
|
+
return { code: 0, message: "ok", data: { success: true } };
|
|
22
|
+
});
|
|
23
|
+
app.delete("/admin/api/transform-rules/:providerId", async (req) => {
|
|
24
|
+
const { providerId } = req.params;
|
|
25
|
+
deleteTransformRule(db, providerId);
|
|
26
|
+
return { code: 0, message: "ok", data: { success: true } };
|
|
27
|
+
});
|
|
28
|
+
app.post("/admin/api/transform-rules/reload", async () => {
|
|
29
|
+
if (options.pluginRegistry) {
|
|
30
|
+
const pluginsDir = resolve(process.cwd(), "plugins/transform");
|
|
31
|
+
const result = options.pluginRegistry.reload(options.db, pluginsDir);
|
|
32
|
+
return { code: 0, message: "ok", data: result };
|
|
33
|
+
}
|
|
34
|
+
const rules = getAllActiveRules(db);
|
|
35
|
+
return { code: 0, message: "ok", data: { loadedPlugins: [], rulesCount: rules.length } };
|
|
36
|
+
});
|
|
37
|
+
done();
|
|
38
|
+
};
|
package/dist/admin/upgrade.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import Database from 'better-sqlite3';
|
|
|
3
3
|
import { CheckerOptions } from '../upgrade/checker.js';
|
|
4
4
|
interface UpgradeRoutesOptions {
|
|
5
5
|
db: Database.Database;
|
|
6
|
+
closeFn: () => Promise<void>;
|
|
6
7
|
}
|
|
7
8
|
export declare function startUpgradeChecker(opts?: CheckerOptions): {
|
|
8
9
|
check: (sourceOverride?: string) => Promise<void>;
|
package/dist/admin/upgrade.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getConfigSyncSource, setConfigSyncSource } from '../db/settings.js';
|
|
2
|
-
import { detectDeployment } from '../upgrade/deployment.js';
|
|
2
|
+
import { detectDeployment, hasProcessManager, resolveRestartBinPath, getRestartMethod } from '../upgrade/deployment.js';
|
|
3
3
|
import { createUpgradeChecker, fetchJson } from '../upgrade/checker.js';
|
|
4
4
|
import { reloadConfig } from '../config/recommended.js';
|
|
5
|
-
import { execSync } from 'node:child_process';
|
|
5
|
+
import { execSync, spawn } from 'node:child_process';
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { HTTP_BAD_REQUEST, HTTP_INTERNAL_ERROR } from '../core/constants.js';
|
|
@@ -38,7 +38,8 @@ export const adminUpgradeRoutes = (app, options, done) => {
|
|
|
38
38
|
const c = checker ?? createUpgradeChecker();
|
|
39
39
|
const deployment = detectDeployment();
|
|
40
40
|
const syncSource = getConfigSyncSource(db);
|
|
41
|
-
|
|
41
|
+
const restartMethod = getRestartMethod();
|
|
42
|
+
return reply.send({ ...c.getStatus(), deployment, syncSource, restartMethod });
|
|
42
43
|
});
|
|
43
44
|
app.post('/admin/api/upgrade/check', async (_req, reply) => {
|
|
44
45
|
const c = checker ?? createUpgradeChecker();
|
|
@@ -78,6 +79,38 @@ export const adminUpgradeRoutes = (app, options, done) => {
|
|
|
78
79
|
return reply.code(HTTP_INTERNAL_ERROR).send(apiError(API_CODE.INTERNAL_ERROR, `升级失败: ${msg}`));
|
|
79
80
|
}
|
|
80
81
|
});
|
|
82
|
+
app.post('/admin/api/upgrade/restart', async (req, reply) => {
|
|
83
|
+
const managed = hasProcessManager();
|
|
84
|
+
const method = getRestartMethod();
|
|
85
|
+
// 先回复客户端,再执行重启(否则客户端收不到响应)
|
|
86
|
+
reply.send({ ok: true, method });
|
|
87
|
+
// 给响应发送窗口
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, 300)); // eslint-disable-line no-magic-numbers
|
|
89
|
+
try {
|
|
90
|
+
req.log.info({ method, managed }, 'Restarting server...');
|
|
91
|
+
// 优雅关闭(释放端口、等待活跃请求完成)
|
|
92
|
+
await options.closeFn();
|
|
93
|
+
if (!managed) {
|
|
94
|
+
// 无进程管理器(npx / 手动 node):自 spawn 新进程
|
|
95
|
+
const binPath = resolveRestartBinPath();
|
|
96
|
+
const args = process.argv.slice(2); // eslint-disable-line no-magic-numbers
|
|
97
|
+
req.log.info({ binPath, args }, 'Spawning new process before exit');
|
|
98
|
+
const child = spawn(binPath, args, {
|
|
99
|
+
detached: true,
|
|
100
|
+
stdio: 'ignore',
|
|
101
|
+
env: { ...process.env },
|
|
102
|
+
});
|
|
103
|
+
child.unref();
|
|
104
|
+
}
|
|
105
|
+
req.log.info('Exiting current process');
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
// 重启失败时记录错误,保持服务运行
|
|
110
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
111
|
+
req.log.error({ err }, `Restart failed: ${msg}`);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
81
114
|
app.post('/admin/api/upgrade/sync-config', async (req, reply) => {
|
|
82
115
|
const { source } = req.body;
|
|
83
116
|
if (source !== 'github' && source !== 'gitee') {
|
package/dist/admin/usage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getWindowsInRange, getWindowUsage } from "../db/usage-windows.js";
|
|
3
|
-
import { getProviderById } from "../db/
|
|
3
|
+
import { getProviderById } from "../db/index.js";
|
|
4
4
|
import { resolveTimeRange } from "../utils/time-range.js";
|
|
5
5
|
const UsageQuerySchema = Type.Object({
|
|
6
6
|
router_key_id: Type.Optional(Type.String()),
|
package/dist/core/container.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare const SERVICE_KEYS: {
|
|
|
7
7
|
readonly usageWindowTracker: "usageWindowTracker";
|
|
8
8
|
readonly sessionTracker: "sessionTracker";
|
|
9
9
|
readonly adaptiveController: "adaptiveController";
|
|
10
|
+
readonly pluginRegistry: "pluginRegistry";
|
|
10
11
|
readonly logFileWriter: "logFileWriter";
|
|
11
12
|
};
|
|
12
13
|
export type ServiceKey = (typeof SERVICE_KEYS)[keyof typeof SERVICE_KEYS];
|
package/dist/core/container.js
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS provider_transform_rules (
|
|
2
|
+
provider_id TEXT PRIMARY KEY REFERENCES providers(id) ON DELETE CASCADE,
|
|
3
|
+
inject_headers TEXT,
|
|
4
|
+
request_defaults TEXT,
|
|
5
|
+
drop_fields TEXT,
|
|
6
|
+
field_overrides TEXT,
|
|
7
|
+
plugin_name TEXT,
|
|
8
|
+
is_active INTEGER DEFAULT 1,
|
|
9
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
10
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
11
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
export interface TransformRules {
|
|
3
|
+
provider_id: string;
|
|
4
|
+
inject_headers: Record<string, string> | null;
|
|
5
|
+
request_defaults: Record<string, unknown> | null;
|
|
6
|
+
drop_fields: string[] | null;
|
|
7
|
+
field_overrides: Record<string, unknown> | null;
|
|
8
|
+
plugin_name: string | null;
|
|
9
|
+
is_active: number;
|
|
10
|
+
created_at?: string;
|
|
11
|
+
updated_at?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function getTransformRule(db: Database.Database, providerId: string): TransformRules | null;
|
|
14
|
+
export declare function upsertTransformRule(db: Database.Database, providerId: string, rules: Partial<Omit<TransformRules, "provider_id">>): void;
|
|
15
|
+
export declare function deleteTransformRule(db: Database.Database, providerId: string): void;
|
|
16
|
+
export declare function getAllActiveRules(db: Database.Database): TransformRules[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const JSON_COLUMNS = ["inject_headers", "request_defaults", "drop_fields", "field_overrides"];
|
|
2
|
+
function parseJsonColumns(row) {
|
|
3
|
+
const result = { ...row };
|
|
4
|
+
for (const col of JSON_COLUMNS) {
|
|
5
|
+
if (result[col]) {
|
|
6
|
+
try {
|
|
7
|
+
result[col] = JSON.parse(result[col]);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
console.error(`[transform-rules] Failed to parse JSON column "${col}", keeping raw value`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
export function getTransformRule(db, providerId) {
|
|
17
|
+
const row = db.prepare("SELECT * FROM provider_transform_rules WHERE provider_id = ?").get(providerId);
|
|
18
|
+
if (!row)
|
|
19
|
+
return null;
|
|
20
|
+
return parseJsonColumns(row);
|
|
21
|
+
}
|
|
22
|
+
export function upsertTransformRule(db, providerId, rules) {
|
|
23
|
+
const existing = db.prepare("SELECT provider_id FROM provider_transform_rules WHERE provider_id = ?").get(providerId);
|
|
24
|
+
if (existing) {
|
|
25
|
+
const fields = [];
|
|
26
|
+
const values = [];
|
|
27
|
+
const jsonFields = new Set(["inject_headers", "request_defaults", "drop_fields", "field_overrides"]);
|
|
28
|
+
for (const [key, val] of Object.entries(rules)) {
|
|
29
|
+
if (key === "provider_id")
|
|
30
|
+
continue;
|
|
31
|
+
fields.push(`${key} = ?`);
|
|
32
|
+
values.push(jsonFields.has(key) && val ? JSON.stringify(val) : val);
|
|
33
|
+
}
|
|
34
|
+
if (fields.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
fields.push("updated_at = datetime('now')");
|
|
37
|
+
values.push(providerId);
|
|
38
|
+
db.prepare(`UPDATE provider_transform_rules SET ${fields.join(", ")} WHERE provider_id = ?`).run(...values);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
db.prepare(`INSERT INTO provider_transform_rules (provider_id, inject_headers, request_defaults, drop_fields, field_overrides, plugin_name, is_active)
|
|
42
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(providerId, rules.inject_headers ? JSON.stringify(rules.inject_headers) : null, rules.request_defaults ? JSON.stringify(rules.request_defaults) : null, rules.drop_fields ? JSON.stringify(rules.drop_fields) : null, rules.field_overrides ? JSON.stringify(rules.field_overrides) : null, rules.plugin_name ?? null, rules.is_active ?? 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function deleteTransformRule(db, providerId) {
|
|
46
|
+
db.prepare("DELETE FROM provider_transform_rules WHERE provider_id = ?").run(providerId);
|
|
47
|
+
}
|
|
48
|
+
export function getAllActiveRules(db) {
|
|
49
|
+
const rows = db.prepare("SELECT * FROM provider_transform_rules WHERE is_active = 1").all();
|
|
50
|
+
return rows.map(parseJsonColumns);
|
|
51
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { openaiProxy } from "./proxy/handler/openai.js";
|
|
|
19
19
|
import { anthropicProxy } from "./proxy/handler/anthropic.js";
|
|
20
20
|
import { adminRoutes } from "./admin/routes.js";
|
|
21
21
|
import { RetryRuleMatcher } from "./proxy/orchestration/retry-rules.js";
|
|
22
|
+
import { PluginRegistry } from "./proxy/transform/plugin-registry.js";
|
|
22
23
|
import { ProviderSemaphoreManager } from "./proxy/orchestration/semaphore.js";
|
|
23
24
|
import { AdaptiveConcurrencyController } from "./proxy/adaptive-controller.js";
|
|
24
25
|
import { loadEnhancementConfig } from "./proxy/routing/enhancement-config.js";
|
|
@@ -210,6 +211,12 @@ export async function buildApp(options) {
|
|
|
210
211
|
const ac = new AdaptiveConcurrencyController(c.resolve(SERVICE_KEYS.semaphoreManager), app.log);
|
|
211
212
|
return ac;
|
|
212
213
|
});
|
|
214
|
+
// 注册 PluginRegistry(从 DB 和 plugins 目录加载转换插件)
|
|
215
|
+
const pluginRegistry = new PluginRegistry();
|
|
216
|
+
pluginRegistry.loadFromDB(db);
|
|
217
|
+
const pluginsDir = path.resolve(__dirname, "../plugins/transform");
|
|
218
|
+
pluginRegistry.scanPluginsDir(pluginsDir);
|
|
219
|
+
container.register(SERVICE_KEYS.pluginRegistry, () => pluginRegistry);
|
|
213
220
|
// 从容器解析所有服务
|
|
214
221
|
const matcher = container.resolve(SERVICE_KEYS.matcher);
|
|
215
222
|
const semaphoreManager = container.resolve(SERVICE_KEYS.semaphoreManager);
|
|
@@ -241,7 +248,10 @@ export async function buildApp(options) {
|
|
|
241
248
|
initializeProviderState(db, semaphoreManager, adaptiveController, tracker);
|
|
242
249
|
},
|
|
243
250
|
};
|
|
244
|
-
|
|
251
|
+
// Late-bound close ref — close 函数在 adminRoutes 注册之后才定义,
|
|
252
|
+
// 但 restart API 需要在运行时调用它
|
|
253
|
+
const closeRef = { fn: async () => { } };
|
|
254
|
+
app.register(adminRoutes, { db, stateRegistry, tracker, adaptiveController, logFileWriter, logsDir, closeFn: () => closeRef.fn(), pluginRegistry });
|
|
245
255
|
// 前端静态文件服务(生产环境)
|
|
246
256
|
const frontendDist = path.resolve(process.env.FRONTEND_DIST || path.join(__dirname, "../frontend-dist"));
|
|
247
257
|
if (existsSync(frontendDist)) {
|
|
@@ -294,6 +304,8 @@ export async function buildApp(options) {
|
|
|
294
304
|
await prevClose();
|
|
295
305
|
};
|
|
296
306
|
}
|
|
307
|
+
// 将最终版 close 函数绑定到 late-bound ref(供 restart API 运行时调用)
|
|
308
|
+
closeRef.fn = close;
|
|
297
309
|
return {
|
|
298
310
|
app,
|
|
299
311
|
db,
|
|
@@ -224,7 +224,14 @@ export class RequestTracker {
|
|
|
224
224
|
// request_start: 无需处理,已是原始数据
|
|
225
225
|
// request_complete: strip clientRequest(完成后从 DB 加载详情)
|
|
226
226
|
let payload = data;
|
|
227
|
-
if (event === "
|
|
227
|
+
if (event === "request_update" && Array.isArray(data)) {
|
|
228
|
+
payload = data.map((req) => {
|
|
229
|
+
const copy = { ...req };
|
|
230
|
+
delete copy.clientRequest;
|
|
231
|
+
return copy;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else if ((event === "request_complete" || event === "request_start") && data && typeof data === "object") {
|
|
228
235
|
const copy = { ...data };
|
|
229
236
|
delete copy.clientRequest;
|
|
230
237
|
payload = copy;
|
package/dist/monitor/types.d.ts
CHANGED
|
@@ -40,19 +40,27 @@ export function cleanRouterResponses(body) {
|
|
|
40
40
|
}
|
|
41
41
|
if (msg.role === "assistant") {
|
|
42
42
|
const blocks = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
43
|
-
// 清理 router synthetic AskUserQuestion 的 tool_use
|
|
43
|
+
// 清理 router synthetic AskUserQuestion 的 tool_use 消息(Anthropic content blocks)
|
|
44
44
|
const toolUseBlocks = blocks.filter((b) => b && typeof b === "object" && b.type === "tool_use"
|
|
45
45
|
&& b.name === "AskUserQuestion"
|
|
46
46
|
&& typeof b.id === "string"
|
|
47
47
|
&& b.id.startsWith(TOOL_USE_ID_PREFIX));
|
|
48
48
|
if (toolUseBlocks.length > 0 && toolUseBlocks.length === blocks.length)
|
|
49
49
|
return false;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
// 提取文本:兼容 Anthropic content blocks 和 OpenAI 纯字符串
|
|
51
|
+
const textParts = [];
|
|
52
|
+
for (const b of blocks) {
|
|
53
|
+
if (typeof b === "string") {
|
|
54
|
+
textParts.push(b);
|
|
55
|
+
}
|
|
56
|
+
else if (b && typeof b === "object" && b.type === "text" && typeof b.text === "string") {
|
|
57
|
+
textParts.push(b.text);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const combined = textParts.join("");
|
|
54
61
|
const stripped = combined.replace(RE_ROUTER_RESPONSE, "").trim();
|
|
55
|
-
|
|
62
|
+
// 有 tool_calls 的消息即使 text 为空也保留(OpenAI 格式)
|
|
63
|
+
if (!stripped && !msg.tool_calls)
|
|
56
64
|
return false;
|
|
57
65
|
}
|
|
58
66
|
return true;
|
|
@@ -10,6 +10,9 @@ import { HTTP_NOT_FOUND, HTTP_BAD_GATEWAY } from "../../core/constants.js";
|
|
|
10
10
|
import { SERVICE_KEYS } from "../../core/container.js";
|
|
11
11
|
const CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
|
|
12
12
|
const MODELS_PATH = "/v1/models";
|
|
13
|
+
/** OpenAI 兼容路径(不带 /v1 前缀),供部分客户端使用 */
|
|
14
|
+
const CHAT_COMPLETIONS_COMPAT_PATH = "/chat/completions";
|
|
15
|
+
const MODELS_COMPAT_PATH = "/models";
|
|
13
16
|
const OPENAI_ERROR_META = {
|
|
14
17
|
modelNotFound: { type: "invalid_request_error", code: "model_not_found" },
|
|
15
18
|
modelNotAllowed: { type: "invalid_request_error", code: "model_not_allowed" },
|
|
@@ -27,7 +30,7 @@ function sendError(reply, e) {
|
|
|
27
30
|
const openaiProxyRaw = (app, opts, done) => {
|
|
28
31
|
const { db, container } = opts;
|
|
29
32
|
const orchestrator = createOrchestrator(container.resolve(SERVICE_KEYS.semaphoreManager), container.resolve(SERVICE_KEYS.tracker), container.resolve(SERVICE_KEYS.adaptiveController));
|
|
30
|
-
|
|
33
|
+
const handleChatCompletions = async (request, reply) => {
|
|
31
34
|
if (!orchestrator) {
|
|
32
35
|
const body = request.body;
|
|
33
36
|
insertRequestLog(db, {
|
|
@@ -48,8 +51,11 @@ const openaiProxyRaw = (app, opts, done) => {
|
|
|
48
51
|
}
|
|
49
52
|
},
|
|
50
53
|
});
|
|
51
|
-
}
|
|
52
|
-
|
|
54
|
+
};
|
|
55
|
+
// 规范路径 + 兼容路径(不带 /v1 前缀)
|
|
56
|
+
app.post(CHAT_COMPLETIONS_PATH, handleChatCompletions);
|
|
57
|
+
app.post(CHAT_COMPLETIONS_COMPAT_PATH, handleChatCompletions);
|
|
58
|
+
const handleModels = async (request, reply) => {
|
|
53
59
|
const startTime = Date.now();
|
|
54
60
|
const providers = getActiveProviders(db, "openai");
|
|
55
61
|
if (providers.length === 0) {
|
|
@@ -97,7 +103,10 @@ const openaiProxyRaw = (app, opts, done) => {
|
|
|
97
103
|
body: { error: { message: "Failed to reach backend service", type: "server_error", code: "upstream_error" } },
|
|
98
104
|
});
|
|
99
105
|
}
|
|
100
|
-
}
|
|
106
|
+
};
|
|
107
|
+
// 规范路径 + 兼容路径
|
|
108
|
+
app.get(MODELS_PATH, handleModels);
|
|
109
|
+
app.get(MODELS_COMPAT_PATH, handleModels);
|
|
101
110
|
done();
|
|
102
111
|
};
|
|
103
112
|
export const openaiProxy = fp(openaiProxyRaw, { name: "openai-proxy" });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "crypto";
|
|
2
|
+
import { parseToolArguments } from "../transform/sanitize.js";
|
|
2
3
|
const HASH_DIGEST_LENGTH = 16;
|
|
3
4
|
/** 从 TransportResult 中提取最终 HTTP status code */
|
|
4
5
|
export function getTransportStatusCode(result) {
|
|
@@ -17,13 +18,7 @@ export function serializeBlocksForStorage(blocks, apiType) {
|
|
|
17
18
|
if (b.type === "thinking")
|
|
18
19
|
return { type: "thinking", thinking: b.content };
|
|
19
20
|
if (b.type === "tool_use") {
|
|
20
|
-
|
|
21
|
-
// eslint-disable-next-line taste/no-silent-catch
|
|
22
|
-
try {
|
|
23
|
-
input = JSON.parse(b.content || "{}");
|
|
24
|
-
}
|
|
25
|
-
catch { /* tool_use content 非合法 JSON 时保留空对象 */ }
|
|
26
|
-
return { type: "tool_use", name: b.name ?? "", input };
|
|
21
|
+
return { type: "tool_use", name: b.name ?? "", input: parseToolArguments(b.content) };
|
|
27
22
|
}
|
|
28
23
|
return { type: "text", text: b.content };
|
|
29
24
|
});
|