llm-simple-router 0.1.1 → 0.3.5
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 +12 -14
- package/dist/admin/groups.js +25 -0
- package/dist/admin/monitor.d.ts +7 -0
- package/dist/admin/monitor.js +25 -0
- package/dist/admin/providers.d.ts +4 -1
- package/dist/admin/providers.js +68 -17
- package/dist/admin/proxy-enhancement.d.ts +7 -0
- package/dist/admin/proxy-enhancement.js +39 -0
- package/dist/admin/retry-rules.js +6 -3
- package/dist/admin/router-keys.d.ts +0 -1
- package/dist/admin/router-keys.js +17 -8
- package/dist/admin/routes.d.ts +4 -3
- package/dist/admin/routes.js +11 -4
- package/dist/admin/setup.d.ts +7 -0
- package/dist/admin/setup.js +47 -0
- package/dist/config.d.ts +1 -4
- package/dist/config.js +13 -13
- package/dist/db/index.d.ts +6 -3
- package/dist/db/index.js +4 -2
- package/dist/db/logs.d.ts +5 -2
- package/dist/db/logs.js +4 -4
- package/dist/db/mappings.d.ts +16 -0
- package/dist/db/mappings.js +76 -0
- package/dist/db/migrations/014_create_settings.sql +4 -0
- package/dist/db/migrations/015_add_original_model.sql +1 -0
- package/dist/db/migrations/016_create_session_model_tables.sql +24 -0
- package/dist/db/migrations/017_add_provider_concurrency.sql +3 -0
- package/dist/db/providers.d.ts +12 -1
- package/dist/db/providers.js +8 -3
- package/dist/db/retry-rules.js +4 -1
- package/dist/db/router-keys.js +3 -1
- package/dist/db/session-states.d.ts +40 -0
- package/dist/db/session-states.js +37 -0
- package/dist/db/settings.d.ts +4 -0
- package/dist/db/settings.js +10 -0
- package/dist/index.js +86 -16
- package/dist/metrics/sse-metrics-transform.d.ts +17 -1
- package/dist/metrics/sse-metrics-transform.js +33 -2
- package/dist/middleware/admin-auth.d.ts +2 -2
- package/dist/middleware/admin-auth.js +21 -8
- package/dist/middleware/auth.js +47 -1
- package/dist/monitor/request-tracker.d.ts +49 -0
- package/dist/monitor/request-tracker.js +279 -0
- package/dist/monitor/runtime-collector.d.ts +11 -0
- package/dist/monitor/runtime-collector.js +41 -0
- package/dist/monitor/stats-aggregator.d.ts +22 -0
- package/dist/monitor/stats-aggregator.js +166 -0
- package/dist/monitor/types.d.ts +84 -0
- package/dist/monitor/types.js +1 -0
- package/dist/proxy/anthropic.d.ts +4 -1
- package/dist/proxy/anthropic.js +10 -2
- package/dist/proxy/directive-parser.d.ts +7 -0
- package/dist/proxy/directive-parser.js +70 -0
- package/dist/proxy/enhancement-handler.d.ts +23 -0
- package/dist/proxy/enhancement-handler.js +169 -0
- package/dist/proxy/log-helpers.d.ts +41 -0
- package/dist/proxy/log-helpers.js +35 -0
- package/dist/proxy/mapping-resolver.js +43 -2
- package/dist/proxy/model-state.d.ts +28 -0
- package/dist/proxy/model-state.js +111 -0
- package/dist/proxy/openai.d.ts +4 -1
- package/dist/proxy/openai.js +12 -3
- package/dist/proxy/proxy-core.d.ts +15 -47
- package/dist/proxy/proxy-core.js +299 -337
- package/dist/proxy/response-cleaner.d.ts +5 -0
- package/dist/proxy/response-cleaner.js +60 -0
- package/dist/proxy/retry.d.ts +1 -1
- package/dist/proxy/retry.js +3 -2
- package/dist/proxy/semaphore.d.ts +27 -0
- package/dist/proxy/semaphore.js +125 -0
- package/dist/proxy/strategy/failover.d.ts +1 -1
- package/dist/proxy/strategy/failover.js +10 -2
- package/dist/proxy/strategy/random.d.ts +1 -1
- package/dist/proxy/strategy/random.js +8 -2
- package/dist/proxy/strategy/round-robin.d.ts +2 -1
- package/dist/proxy/strategy/round-robin.js +13 -2
- package/dist/proxy/strategy/targets-rule.d.ts +7 -0
- package/dist/proxy/strategy/targets-rule.js +14 -0
- package/dist/proxy/strategy/types.d.ts +5 -1
- package/dist/proxy/strategy/types.js +3 -0
- package/dist/proxy/upstream-call.d.ts +43 -0
- package/dist/proxy/upstream-call.js +208 -0
- package/dist/utils/password.d.ts +2 -0
- package/dist/utils/password.js +15 -0
- package/frontend-dist/assets/CardContent-B40ArIqh.js +1 -0
- package/frontend-dist/assets/{CardHeader-D5lVaeAA.js → CardHeader-BjkSQf27.js} +1 -1
- package/frontend-dist/assets/CardTitle-DjG2kSF3.js +1 -0
- package/frontend-dist/assets/Checkbox-Cw0rq2D9.js +1 -0
- package/frontend-dist/assets/CollapsibleTrigger-BvYqNbGA.js +1 -0
- package/frontend-dist/assets/Collection-CQ4pV54w.js +3 -0
- package/frontend-dist/assets/Dashboard-CsOTBnSa.js +3 -0
- package/frontend-dist/assets/DialogTitle-PS2W-IfG.js +1 -0
- package/frontend-dist/assets/Input-toxjzsir.js +1 -0
- package/frontend-dist/assets/Label-fZNDEQjf.js +1 -0
- package/frontend-dist/assets/LogResponseViewer-B9kSncNr.js +3 -0
- package/frontend-dist/assets/Login-DRm9DHq1.js +1 -0
- package/frontend-dist/assets/Logs-NHxebwmP.js +1 -0
- package/frontend-dist/assets/ModelMappings-DV0RPnO2.js +1 -0
- package/frontend-dist/assets/Monitor-B5TYWb2n.js +1 -0
- package/frontend-dist/assets/PopperContent-BvKlcZEO.js +1 -0
- package/frontend-dist/assets/Providers-D1Rauu-D.js +1 -0
- package/frontend-dist/assets/ProxyEnhancement-B2OliarO.js +5 -0
- package/frontend-dist/assets/RetryRules-BWu2gicT.js +1 -0
- package/frontend-dist/assets/RouterKeys-BP6XJCVa.js +1 -0
- package/frontend-dist/assets/RovingFocusItem-DHfpgdA0.js +1 -0
- package/frontend-dist/assets/SelectValue-CFf_mD9E.js +1 -0
- package/frontend-dist/assets/Setup-BMjCT-Tl.js +1 -0
- package/frontend-dist/assets/Switch-BGCQ7puL.js +1 -0
- package/frontend-dist/assets/TableHeader-DAOs6nSA.js +1 -0
- package/frontend-dist/assets/TabsTrigger-DBAYM66g.js +1 -0
- package/frontend-dist/assets/VisuallyHidden-Dh7svQf3.js +1 -0
- package/frontend-dist/assets/VisuallyHiddenInput-BOaRSEmd.js +1 -0
- package/frontend-dist/assets/alert-dialog-CUNSZqpB.js +1 -0
- package/frontend-dist/assets/button-CfQs66fX.js +1 -0
- package/frontend-dist/assets/client-DvdghFBq.js +12 -0
- package/frontend-dist/assets/createLucideIcon-DCD7INQf.js +1 -0
- package/frontend-dist/assets/dialog-DQFRGKR6.js +1 -0
- package/frontend-dist/assets/index--5JhZIwi.js +1 -0
- package/frontend-dist/assets/index-Bx15k8FA.css +1 -0
- package/frontend-dist/assets/lib-BJNsNHLO.js +1 -0
- package/frontend-dist/assets/ohash.D__AXeF1-CNudYmrX.js +1 -0
- package/frontend-dist/assets/useClipboard-aPMKfK25.js +1 -0
- package/frontend-dist/assets/useForwardExpose-u2vjohek.js +1 -0
- package/frontend-dist/assets/useNonce-ClXGIm-8.js +1 -0
- package/frontend-dist/assets/x-ILQhskuj.js +1 -0
- package/frontend-dist/index.html +7 -6
- package/package.json +5 -4
- package/.env.example +0 -13
- package/dist/admin/services.d.ts +0 -7
- package/dist/admin/services.js +0 -63
- package/frontend-dist/assets/CardContent-BE9fukPi.js +0 -1
- package/frontend-dist/assets/CardTitle-H-zwhi3Z.js +0 -1
- package/frontend-dist/assets/Checkbox--1gw0dYW.js +0 -1
- package/frontend-dist/assets/CollapsibleTrigger-D_ptA35Y.js +0 -1
- package/frontend-dist/assets/Dashboard-D4AwkULO.js +0 -3
- package/frontend-dist/assets/Label-GiPfoz7u.js +0 -1
- package/frontend-dist/assets/Login-BUet1sbM.js +0 -1
- package/frontend-dist/assets/Logs-yztb_F9t.js +0 -3
- package/frontend-dist/assets/ModelMappings-MbZhdPNv.js +0 -1
- package/frontend-dist/assets/Providers-BjsqH6A2.js +0 -1
- package/frontend-dist/assets/RetryRules-C2vvJvLr.js +0 -1
- package/frontend-dist/assets/RouterKeys-DavrgpAQ.js +0 -1
- package/frontend-dist/assets/RovingFocusItem-DnIa_lwH.js +0 -1
- package/frontend-dist/assets/SelectValue-BB0Ckbjh.js +0 -1
- package/frontend-dist/assets/TableHeader-D2GkiqRx.js +0 -1
- package/frontend-dist/assets/alert-dialog-CWjBke-O.js +0 -1
- package/frontend-dist/assets/badge-_ZHrMEpC.js +0 -3
- package/frontend-dist/assets/button-C4_mChkc.js +0 -1
- package/frontend-dist/assets/client-BWw0R36V.js +0 -12
- package/frontend-dist/assets/dialog-CUHMcTqp.js +0 -1
- package/frontend-dist/assets/index-DEl48bm9.css +0 -1
- package/frontend-dist/assets/index-UZK1BnPG.js +0 -1
- package/frontend-dist/assets/lib-Qs8xoTas.js +0 -1
- package/frontend-dist/assets/useForwardExpose-B-xauF1X.js +0 -1
- package/frontend-dist/assets/x-JBJB26JV.js +0 -1
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ Router 根据模型映射找到后端供应商 -> 转发请求 -> 自动重试
|
|
|
59
59
|
**方式一:shell alias(推荐)**
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
alias clodedev='ANTHROPIC_AUTH_TOKEN="<your-router-key>" ANTHROPIC_BASE_URL="http://127.0.0.1:
|
|
62
|
+
alias clodedev='ANTHROPIC_AUTH_TOKEN="<your-router-key>" ANTHROPIC_BASE_URL="http://127.0.0.1:9981" claude'
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
**方式二:~/.claude/settings.json**
|
|
@@ -68,7 +68,7 @@ alias clodedev='ANTHROPIC_AUTH_TOKEN="<your-router-key>" ANTHROPIC_BASE_URL="htt
|
|
|
68
68
|
{
|
|
69
69
|
"env": {
|
|
70
70
|
"ANTHROPIC_AUTH_TOKEN": "sk-router-change-me",
|
|
71
|
-
"ANTHROPIC_BASE_URL": "http://127.0.0.1:
|
|
71
|
+
"ANTHROPIC_BASE_URL": "http://127.0.0.1:9981",
|
|
72
72
|
"ANTHROPIC_MODEL": "some-model"
|
|
73
73
|
}
|
|
74
74
|
}
|
|
@@ -90,14 +90,13 @@ alias clodedev='ANTHROPIC_AUTH_TOKEN="<your-router-key>" ANTHROPIC_BASE_URL="htt
|
|
|
90
90
|
## 快速开始
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
# 访问 http://localhost:3000/admin
|
|
93
|
+
# 一行命令启动
|
|
94
|
+
npx llm-simple-router
|
|
95
|
+
# 访问 http://localhost:9981/admin
|
|
96
|
+
# 首次访问会进入 Setup 页面设置管理员密码
|
|
98
97
|
```
|
|
99
98
|
|
|
100
|
-
|
|
99
|
+
无需任何环境变量。数据默认存储在 `~/.llm-simple-router/`。
|
|
101
100
|
|
|
102
101
|
## Docker 部署
|
|
103
102
|
|
|
@@ -107,15 +106,14 @@ docker compose up -d
|
|
|
107
106
|
|
|
108
107
|
## 环境变量
|
|
109
108
|
|
|
109
|
+
所有密钥(管理员密码、加密密钥、JWT 密钥)通过首次启动的 Setup 页面设置,无需环境变量。
|
|
110
|
+
|
|
110
111
|
| 变量 | 必需 | 默认值 | 说明 |
|
|
111
112
|
|------|------|--------|------|
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `JWT_SECRET` | Yes | -- | JWT 签名密钥(64字符 hex) |
|
|
115
|
-
| `PORT` | No | `3000` | 服务端口 |
|
|
116
|
-
| `DB_PATH` | No | `./data/router.db` | SQLite 数据库路径 |
|
|
113
|
+
| `PORT` | No | `9981` | 服务端口 |
|
|
114
|
+
| `DB_PATH` | No | `~/.llm-simple-router/router.db` | SQLite 数据库路径 |
|
|
117
115
|
| `LOG_LEVEL` | No | `info` | 日志级别 |
|
|
118
|
-
| `TZ` | No |
|
|
116
|
+
| `TZ` | No | `Asia/Shanghai` | 时区设置 |
|
|
119
117
|
| `STREAM_TIMEOUT_MS` | No | `3000000` | 流式代理空闲超时(ms) |
|
|
120
118
|
| `RETRY_MAX_ATTEMPTS` | No | `3` | 最大重试次数 |
|
|
121
119
|
| `RETRY_BASE_DELAY_MS` | No | `1000` | 重试基础延迟(ms) |
|
package/dist/admin/groups.js
CHANGED
|
@@ -2,6 +2,7 @@ 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
4
|
import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_CONFLICT } from "./constants.js";
|
|
5
|
+
const MIN_FAILOVER_TARGETS = 2;
|
|
5
6
|
const CreateGroupSchema = Type.Object({
|
|
6
7
|
client_model: Type.String({ minLength: 1 }),
|
|
7
8
|
strategy: Type.String({ minLength: 1 }),
|
|
@@ -13,6 +14,10 @@ const UpdateGroupSchema = Type.Object({
|
|
|
13
14
|
rule: Type.Optional(Type.String()),
|
|
14
15
|
});
|
|
15
16
|
async function validateRule(db, strategy, ruleJson) {
|
|
17
|
+
const VALID_STRATEGIES = new Set(Object.values(STRATEGY_NAMES));
|
|
18
|
+
if (!VALID_STRATEGIES.has(strategy)) {
|
|
19
|
+
return `Unknown strategy '${strategy}'. Valid: ${[...VALID_STRATEGIES].join(", ")}`;
|
|
20
|
+
}
|
|
16
21
|
let rule;
|
|
17
22
|
try {
|
|
18
23
|
rule = JSON.parse(ruleJson);
|
|
@@ -45,6 +50,26 @@ async function validateRule(db, strategy, ruleJson) {
|
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
52
|
}
|
|
53
|
+
if (strategy === STRATEGY_NAMES.ROUND_ROBIN || strategy === STRATEGY_NAMES.RANDOM || strategy === STRATEGY_NAMES.FAILOVER) {
|
|
54
|
+
const r = rule;
|
|
55
|
+
if (!Array.isArray(r.targets) || r.targets.length === 0) {
|
|
56
|
+
return "rule.targets must be a non-empty array";
|
|
57
|
+
}
|
|
58
|
+
const minTargets = strategy === STRATEGY_NAMES.FAILOVER ? MIN_FAILOVER_TARGETS : 1;
|
|
59
|
+
if (r.targets.length < minTargets) {
|
|
60
|
+
return `strategy '${strategy}' requires at least ${minTargets} target(s)`;
|
|
61
|
+
}
|
|
62
|
+
for (let i = 0; i < r.targets.length; i++) {
|
|
63
|
+
const t = r.targets[i];
|
|
64
|
+
if (!t.backend_model || !t.provider_id) {
|
|
65
|
+
return `targets[${i}] missing backend_model or provider_id`;
|
|
66
|
+
}
|
|
67
|
+
const p = getProviderById(db, t.provider_id);
|
|
68
|
+
if (!p) {
|
|
69
|
+
return `targets[${i}] provider_id '${t.provider_id}' not found`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
48
73
|
return undefined;
|
|
49
74
|
}
|
|
50
75
|
export const adminGroupRoutes = (app, options, done) => {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { FastifyPluginCallback } from "fastify";
|
|
2
|
+
import type { RequestTracker } from "../monitor/request-tracker.js";
|
|
3
|
+
interface MonitorRoutesOptions {
|
|
4
|
+
tracker?: RequestTracker;
|
|
5
|
+
}
|
|
6
|
+
export declare const adminMonitorRoutes: FastifyPluginCallback<MonitorRoutesOptions>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const HTTP_OK = 200;
|
|
2
|
+
export const adminMonitorRoutes = (app, options, done) => {
|
|
3
|
+
const { tracker } = options;
|
|
4
|
+
if (!tracker) {
|
|
5
|
+
done();
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
app.get("/admin/api/monitor/active", async () => tracker.getActive());
|
|
9
|
+
app.get("/admin/api/monitor/recent", async () => tracker.getRecent());
|
|
10
|
+
app.get("/admin/api/monitor/stats", async () => tracker.getStats());
|
|
11
|
+
app.get("/admin/api/monitor/concurrency", async () => tracker.getConcurrency());
|
|
12
|
+
app.get("/admin/api/monitor/runtime", async () => tracker.getRuntime());
|
|
13
|
+
app.get("/admin/api/monitor/stream", (request, reply) => {
|
|
14
|
+
reply.raw.writeHead(HTTP_OK, {
|
|
15
|
+
"Content-Type": "text/event-stream",
|
|
16
|
+
"Cache-Control": "no-cache",
|
|
17
|
+
Connection: "keep-alive",
|
|
18
|
+
});
|
|
19
|
+
tracker.addClient(reply.raw);
|
|
20
|
+
request.raw.on("close", () => {
|
|
21
|
+
tracker.removeClient(reply.raw);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
done();
|
|
25
|
+
};
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { FastifyPluginCallback } from "fastify";
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
|
+
import { ProviderSemaphoreManager } from "../proxy/semaphore.js";
|
|
4
|
+
import type { RequestTracker } from "../monitor/request-tracker.js";
|
|
3
5
|
interface ProviderRoutesOptions {
|
|
4
6
|
db: Database.Database;
|
|
5
|
-
|
|
7
|
+
semaphoreManager?: ProviderSemaphoreManager;
|
|
8
|
+
tracker?: RequestTracker;
|
|
6
9
|
}
|
|
7
10
|
export declare const adminProviderRoutes: FastifyPluginCallback<ProviderRoutesOptions>;
|
|
8
11
|
export {};
|
package/dist/admin/providers.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
-
import { getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, getAllMappingGroups } from "../db/index.js";
|
|
3
|
-
import { encrypt } from "../utils/crypto.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
2
|
+
import { getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, getAllMappingGroups, PROVIDER_CONCURRENCY_DEFAULTS } from "../db/index.js";
|
|
3
|
+
import { encrypt, decrypt } from "../utils/crypto.js";
|
|
4
|
+
import { getSetting } from "../db/settings.js";
|
|
5
|
+
import { HTTP_CREATED, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_BAD_REQUEST } from "./constants.js";
|
|
6
|
+
const API_KEY_PREVIEW_MIN_LENGTH = 8;
|
|
6
7
|
const API_KEY_PREVIEW_PREFIX_LEN = 4;
|
|
8
|
+
const PROVIDER_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
7
9
|
const CreateProviderSchema = Type.Object({
|
|
8
10
|
name: Type.String({ minLength: 1 }),
|
|
9
11
|
api_type: Type.Union([Type.Literal("openai"), Type.Literal("anthropic")]),
|
|
@@ -11,6 +13,9 @@ const CreateProviderSchema = Type.Object({
|
|
|
11
13
|
api_key: Type.String({ minLength: 1 }),
|
|
12
14
|
models: Type.Optional(Type.Array(Type.String())),
|
|
13
15
|
is_active: Type.Optional(Type.Number()),
|
|
16
|
+
max_concurrency: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
17
|
+
queue_timeout_ms: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
18
|
+
max_queue_size: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
14
19
|
});
|
|
15
20
|
const UpdateProviderSchema = Type.Object({
|
|
16
21
|
name: Type.Optional(Type.String({ minLength: 1 })),
|
|
@@ -19,40 +24,59 @@ const UpdateProviderSchema = Type.Object({
|
|
|
19
24
|
api_key: Type.Optional(Type.String({ minLength: 1 })),
|
|
20
25
|
models: Type.Optional(Type.Array(Type.String())),
|
|
21
26
|
is_active: Type.Optional(Type.Number()),
|
|
27
|
+
max_concurrency: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
28
|
+
queue_timeout_ms: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
29
|
+
max_queue_size: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
22
30
|
});
|
|
23
|
-
function computeApiKeyPreview(apiKey) {
|
|
24
|
-
if (apiKey.length <= API_KEY_PREVIEW_MIN_LEN)
|
|
25
|
-
return "****";
|
|
26
|
-
return `${apiKey.slice(0, API_KEY_PREVIEW_PREFIX_LEN)}...${apiKey.slice(-API_KEY_PREVIEW_PREFIX_LEN)}`;
|
|
27
|
-
}
|
|
28
31
|
export const adminProviderRoutes = (app, options, done) => {
|
|
29
|
-
const { db,
|
|
32
|
+
const { db, semaphoreManager, tracker } = options;
|
|
30
33
|
app.get("/admin/api/providers", async (_request, reply) => {
|
|
34
|
+
const encryptionKey = getSetting(db, "encryption_key");
|
|
31
35
|
const providers = getAllProviders(db);
|
|
32
36
|
return reply.send(providers.map((s) => ({
|
|
33
37
|
id: s.id,
|
|
34
38
|
name: s.name,
|
|
35
39
|
api_type: s.api_type,
|
|
36
40
|
base_url: s.base_url,
|
|
37
|
-
|
|
41
|
+
api_key: s.api_key ? decrypt(s.api_key, encryptionKey) : "",
|
|
38
42
|
models: JSON.parse(s.models || "[]"),
|
|
39
43
|
is_active: s.is_active,
|
|
44
|
+
max_concurrency: s.max_concurrency,
|
|
45
|
+
queue_timeout_ms: s.queue_timeout_ms,
|
|
46
|
+
max_queue_size: s.max_queue_size,
|
|
47
|
+
concurrency_status: semaphoreManager?.getStatus(s.id) ?? { active: 0, queued: 0 },
|
|
40
48
|
created_at: s.created_at,
|
|
41
49
|
updated_at: s.updated_at,
|
|
42
50
|
})));
|
|
43
51
|
});
|
|
44
52
|
app.post("/admin/api/providers", { schema: { body: CreateProviderSchema } }, async (request, reply) => {
|
|
45
53
|
const body = request.body;
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
if (!PROVIDER_NAME_RE.test(body.name)) {
|
|
55
|
+
return reply.status(HTTP_BAD_REQUEST).send({ error: { message: "Provider 名称仅允许英文大小写字母、数字、横线和下划线" } });
|
|
56
|
+
}
|
|
57
|
+
const encryptedKey = encrypt(body.api_key, getSetting(db, "encryption_key"));
|
|
48
58
|
const id = createProvider(db, {
|
|
49
59
|
name: body.name,
|
|
50
60
|
api_type: body.api_type,
|
|
51
61
|
base_url: body.base_url,
|
|
52
62
|
api_key: encryptedKey,
|
|
53
|
-
api_key_preview:
|
|
63
|
+
api_key_preview: body.api_key.length > API_KEY_PREVIEW_MIN_LENGTH ? `${body.api_key.slice(0, API_KEY_PREVIEW_PREFIX_LEN)}...${body.api_key.slice(-API_KEY_PREVIEW_PREFIX_LEN)}` : "****",
|
|
54
64
|
models: JSON.stringify(body.models ?? []),
|
|
55
65
|
is_active: body.is_active ?? 1,
|
|
66
|
+
max_concurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
|
|
67
|
+
queue_timeout_ms: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
|
|
68
|
+
max_queue_size: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
|
|
69
|
+
});
|
|
70
|
+
semaphoreManager?.updateConfig(id, {
|
|
71
|
+
maxConcurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
|
|
72
|
+
queueTimeoutMs: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
|
|
73
|
+
maxQueueSize: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
|
|
74
|
+
});
|
|
75
|
+
tracker?.updateProviderConfig(id, {
|
|
76
|
+
name: body.name,
|
|
77
|
+
maxConcurrency: body.max_concurrency ?? PROVIDER_CONCURRENCY_DEFAULTS.max_concurrency,
|
|
78
|
+
queueTimeoutMs: body.queue_timeout_ms ?? PROVIDER_CONCURRENCY_DEFAULTS.queue_timeout_ms,
|
|
79
|
+
maxQueueSize: body.max_queue_size ?? PROVIDER_CONCURRENCY_DEFAULTS.max_queue_size,
|
|
56
80
|
});
|
|
57
81
|
return reply.code(HTTP_CREATED).send({ id });
|
|
58
82
|
});
|
|
@@ -63,6 +87,9 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
63
87
|
return reply.code(HTTP_NOT_FOUND).send({ error: { message: "Provider not found" } });
|
|
64
88
|
}
|
|
65
89
|
const body = request.body;
|
|
90
|
+
if (body.name !== undefined && !PROVIDER_NAME_RE.test(body.name)) {
|
|
91
|
+
return reply.status(HTTP_BAD_REQUEST).send({ error: { message: "Provider 名称仅允许英文大小写字母、数字、横线和下划线" } });
|
|
92
|
+
}
|
|
66
93
|
const fields = {};
|
|
67
94
|
if (body.name !== undefined)
|
|
68
95
|
fields.name = body.name;
|
|
@@ -74,11 +101,31 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
74
101
|
fields.is_active = body.is_active;
|
|
75
102
|
if (body.models !== undefined)
|
|
76
103
|
fields.models = JSON.stringify(body.models);
|
|
104
|
+
if (body.max_concurrency !== undefined)
|
|
105
|
+
fields.max_concurrency = body.max_concurrency;
|
|
106
|
+
if (body.queue_timeout_ms !== undefined)
|
|
107
|
+
fields.queue_timeout_ms = body.queue_timeout_ms;
|
|
108
|
+
if (body.max_queue_size !== undefined)
|
|
109
|
+
fields.max_queue_size = body.max_queue_size;
|
|
77
110
|
if (body.api_key) {
|
|
78
|
-
fields.api_key = encrypt(body.api_key,
|
|
79
|
-
fields.api_key_preview =
|
|
111
|
+
fields.api_key = encrypt(body.api_key, getSetting(db, "encryption_key"));
|
|
112
|
+
fields.api_key_preview = body.api_key.length > API_KEY_PREVIEW_MIN_LENGTH ? `${body.api_key.slice(0, API_KEY_PREVIEW_PREFIX_LEN)}...${body.api_key.slice(-API_KEY_PREVIEW_PREFIX_LEN)}` : "****";
|
|
80
113
|
}
|
|
81
114
|
updateProvider(db, id, fields);
|
|
115
|
+
const updated = getProviderById(db, id);
|
|
116
|
+
if (body.max_concurrency !== undefined || body.queue_timeout_ms !== undefined || body.max_queue_size !== undefined) {
|
|
117
|
+
semaphoreManager?.updateConfig(id, {
|
|
118
|
+
maxConcurrency: updated.max_concurrency,
|
|
119
|
+
queueTimeoutMs: updated.queue_timeout_ms,
|
|
120
|
+
maxQueueSize: updated.max_queue_size,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
tracker?.updateProviderConfig(id, {
|
|
124
|
+
name: body.name ?? existing.name,
|
|
125
|
+
maxConcurrency: updated.max_concurrency,
|
|
126
|
+
queueTimeoutMs: updated.queue_timeout_ms,
|
|
127
|
+
maxQueueSize: updated.max_queue_size,
|
|
128
|
+
});
|
|
82
129
|
return reply.send({ success: true });
|
|
83
130
|
});
|
|
84
131
|
app.delete("/admin/api/providers/:id", async (request, reply) => {
|
|
@@ -92,9 +139,13 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
92
139
|
return reply.code(HTTP_CONFLICT).send({ error: { message: `Provider is referenced by mapping group '${g.client_model}'` } });
|
|
93
140
|
}
|
|
94
141
|
}
|
|
95
|
-
catch {
|
|
142
|
+
catch {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
96
145
|
}
|
|
97
146
|
deleteProvider(db, id);
|
|
147
|
+
semaphoreManager?.remove(id);
|
|
148
|
+
tracker?.removeProviderConfig(id);
|
|
98
149
|
return reply.send({ success: true });
|
|
99
150
|
});
|
|
100
151
|
done();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { FastifyPluginCallback } from "fastify";
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
interface ProxyEnhancementOptions {
|
|
4
|
+
db: Database.Database;
|
|
5
|
+
}
|
|
6
|
+
export declare const adminProxyEnhancementRoutes: FastifyPluginCallback<ProxyEnhancementOptions>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getSetting, setSetting } from "../db/settings.js";
|
|
2
|
+
import { getSessionStates, getSessionHistory, } from "../db/session-states.js";
|
|
3
|
+
import { modelState } from "../proxy/model-state.js";
|
|
4
|
+
export const adminProxyEnhancementRoutes = (app, options, done) => {
|
|
5
|
+
const { db } = options;
|
|
6
|
+
app.get("/admin/api/proxy-enhancement", async (_req, reply) => {
|
|
7
|
+
const raw = getSetting(db, "proxy_enhancement");
|
|
8
|
+
const config = raw
|
|
9
|
+
? JSON.parse(raw)
|
|
10
|
+
: { claude_code_enabled: false };
|
|
11
|
+
return reply.send(config);
|
|
12
|
+
});
|
|
13
|
+
app.put("/admin/api/proxy-enhancement", async (req, reply) => {
|
|
14
|
+
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
|
+
const config = {
|
|
19
|
+
claude_code_enabled: body.claude_code_enabled,
|
|
20
|
+
};
|
|
21
|
+
setSetting(db, "proxy_enhancement", JSON.stringify(config));
|
|
22
|
+
return reply.send({ success: true });
|
|
23
|
+
});
|
|
24
|
+
app.get("/admin/api/session-states", async (_req, reply) => {
|
|
25
|
+
const states = getSessionStates(db);
|
|
26
|
+
return reply.send(states);
|
|
27
|
+
});
|
|
28
|
+
app.get("/admin/api/session-states/:keyId/:sessionId/history", async (req, reply) => {
|
|
29
|
+
const { keyId, sessionId } = req.params;
|
|
30
|
+
const history = getSessionHistory(db, keyId, sessionId);
|
|
31
|
+
return reply.send(history);
|
|
32
|
+
});
|
|
33
|
+
app.delete("/admin/api/session-states/:keyId/:sessionId", async (req, reply) => {
|
|
34
|
+
const { keyId, sessionId } = req.params;
|
|
35
|
+
modelState.delete(keyId, sessionId);
|
|
36
|
+
return reply.send({ success: true });
|
|
37
|
+
});
|
|
38
|
+
done();
|
|
39
|
+
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, } from "../db/index.js";
|
|
3
3
|
import { HTTP_BAD_REQUEST, HTTP_CREATED } from "./constants.js";
|
|
4
|
+
const DEFAULT_RETRY_DELAY_MS = 5000;
|
|
5
|
+
const DEFAULT_MAX_RETRIES = 10;
|
|
6
|
+
const DEFAULT_MAX_DELAY_MS = 60000;
|
|
4
7
|
const CreateRetryRuleSchema = Type.Object({
|
|
5
8
|
name: Type.String({ minLength: 1 }),
|
|
6
9
|
status_code: Type.Number({ minimum: 100, maximum: 599 }),
|
|
@@ -52,9 +55,9 @@ export const adminRetryRuleRoutes = (app, options, done) => {
|
|
|
52
55
|
body_pattern: body.body_pattern,
|
|
53
56
|
is_active: body.is_active ?? 1,
|
|
54
57
|
retry_strategy: body.retry_strategy ?? "exponential",
|
|
55
|
-
retry_delay_ms: body.retry_delay_ms ??
|
|
56
|
-
max_retries: body.max_retries ??
|
|
57
|
-
max_delay_ms: body.max_delay_ms ??
|
|
58
|
+
retry_delay_ms: body.retry_delay_ms ?? DEFAULT_RETRY_DELAY_MS,
|
|
59
|
+
max_retries: body.max_retries ?? DEFAULT_MAX_RETRIES,
|
|
60
|
+
max_delay_ms: body.max_delay_ms ?? DEFAULT_MAX_DELAY_MS,
|
|
58
61
|
});
|
|
59
62
|
refreshMatcher(matcher, db);
|
|
60
63
|
return reply.code(HTTP_CREATED).send({ id });
|
|
@@ -2,7 +2,6 @@ import { FastifyPluginCallback } from "fastify";
|
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
3
|
interface RouterKeyRoutesOptions {
|
|
4
4
|
db: Database.Database;
|
|
5
|
-
encryptionKey: string;
|
|
6
5
|
}
|
|
7
6
|
export declare const adminRouterKeyRoutes: FastifyPluginCallback<RouterKeyRoutesOptions>;
|
|
8
7
|
export {};
|
|
@@ -2,10 +2,18 @@ import { randomBytes, createHash } from "crypto";
|
|
|
2
2
|
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
|
+
import { getSetting } from "../db/settings.js";
|
|
5
6
|
const HTTP_CREATED = 201;
|
|
6
7
|
const HTTP_NOT_FOUND = 404;
|
|
7
8
|
const KEY_RANDOM_BYTES = 32;
|
|
8
9
|
const KEY_PREFIX_LENGTH = 8;
|
|
10
|
+
/** 归一化 allowed_models:null/空数组/仅含空字符串 → null(允许所有模型) */
|
|
11
|
+
function normalizeAllowedModels(val) {
|
|
12
|
+
if (!val)
|
|
13
|
+
return null;
|
|
14
|
+
const filtered = val.filter((m) => m.trim() !== "");
|
|
15
|
+
return filtered.length > 0 ? JSON.stringify(filtered) : null;
|
|
16
|
+
}
|
|
9
17
|
const CreateRouterKeySchema = Type.Object({
|
|
10
18
|
name: Type.String({ minLength: 1 }),
|
|
11
19
|
allowed_models: Type.Optional(Type.Union([Type.Array(Type.String()), Type.Null()])),
|
|
@@ -15,14 +23,15 @@ const UpdateRouterKeySchema = Type.Object({
|
|
|
15
23
|
allowed_models: Type.Optional(Type.Union([Type.Array(Type.String()), Type.Null()])),
|
|
16
24
|
is_active: Type.Optional(Type.Number()),
|
|
17
25
|
});
|
|
18
|
-
function generateRouterKey(
|
|
26
|
+
function generateRouterKey(db) {
|
|
19
27
|
const key = `sk-router-${randomBytes(KEY_RANDOM_BYTES).toString("hex")}`;
|
|
20
28
|
const hash = createHash("sha256").update(key).digest("hex");
|
|
21
29
|
const prefix = key.slice(0, KEY_PREFIX_LENGTH);
|
|
22
|
-
const encrypted = encrypt(key,
|
|
30
|
+
const encrypted = encrypt(key, getSetting(db, "encryption_key"));
|
|
23
31
|
return { key, hash, prefix, encrypted };
|
|
24
32
|
}
|
|
25
|
-
function toPublicRouterKey(rk,
|
|
33
|
+
function toPublicRouterKey(rk, db) {
|
|
34
|
+
const encryptionKey = getSetting(db, "encryption_key");
|
|
26
35
|
return {
|
|
27
36
|
id: rk.id,
|
|
28
37
|
name: rk.name,
|
|
@@ -35,15 +44,15 @@ function toPublicRouterKey(rk, encryptionKey) {
|
|
|
35
44
|
};
|
|
36
45
|
}
|
|
37
46
|
export const adminRouterKeyRoutes = (app, options, done) => {
|
|
38
|
-
const { db
|
|
47
|
+
const { db } = options;
|
|
39
48
|
app.get("/admin/api/router-keys", async (_request, reply) => {
|
|
40
49
|
const keys = getAllRouterKeys(db);
|
|
41
|
-
return reply.send(keys.map((rk) => toPublicRouterKey(rk,
|
|
50
|
+
return reply.send(keys.map((rk) => toPublicRouterKey(rk, db)));
|
|
42
51
|
});
|
|
43
52
|
app.post("/admin/api/router-keys", { schema: { body: CreateRouterKeySchema } }, async (request, reply) => {
|
|
44
53
|
const body = request.body;
|
|
45
|
-
const { key, hash, prefix, encrypted } = generateRouterKey(
|
|
46
|
-
const allowedModels =
|
|
54
|
+
const { key, hash, prefix, encrypted } = generateRouterKey(db);
|
|
55
|
+
const allowedModels = normalizeAllowedModels(body.allowed_models);
|
|
47
56
|
const id = createRouterKey(db, { name: body.name, key_hash: hash, key_prefix: prefix, key_encrypted: encrypted, allowed_models: allowedModels });
|
|
48
57
|
return reply.code(HTTP_CREATED).send({
|
|
49
58
|
id,
|
|
@@ -66,7 +75,7 @@ export const adminRouterKeyRoutes = (app, options, done) => {
|
|
|
66
75
|
if (body.name !== undefined)
|
|
67
76
|
fields.name = body.name;
|
|
68
77
|
if (body.allowed_models !== undefined)
|
|
69
|
-
fields.allowed_models =
|
|
78
|
+
fields.allowed_models = normalizeAllowedModels(body.allowed_models);
|
|
70
79
|
if (body.is_active !== undefined)
|
|
71
80
|
fields.is_active = body.is_active;
|
|
72
81
|
updateRouterKey(db, id, fields);
|
package/dist/admin/routes.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { FastifyPluginCallback } from "fastify";
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
3
|
import { RetryRuleMatcher } from "../proxy/retry-rules.js";
|
|
4
|
+
import type { RequestTracker } from "../monitor/request-tracker.js";
|
|
5
|
+
import { ProviderSemaphoreManager } from "../proxy/semaphore.js";
|
|
4
6
|
interface AdminRoutesOptions {
|
|
5
7
|
db: Database.Database;
|
|
6
|
-
adminPassword: string;
|
|
7
|
-
jwtSecret: string;
|
|
8
|
-
encryptionKey: string;
|
|
9
8
|
matcher: RetryRuleMatcher | null;
|
|
9
|
+
tracker?: RequestTracker;
|
|
10
|
+
semaphoreManager?: ProviderSemaphoreManager;
|
|
10
11
|
}
|
|
11
12
|
export declare const adminRoutes: FastifyPluginCallback<AdminRoutesOptions>;
|
|
12
13
|
export {};
|
package/dist/admin/routes.js
CHANGED
|
@@ -6,17 +6,24 @@ import { adminRetryRuleRoutes } from "./retry-rules.js";
|
|
|
6
6
|
import { adminLogRoutes } from "./logs.js";
|
|
7
7
|
import { adminStatsRoutes } from "./stats.js";
|
|
8
8
|
import { adminMetricsRoutes } from "./metrics.js";
|
|
9
|
+
import { adminProxyEnhancementRoutes } from "./proxy-enhancement.js";
|
|
9
10
|
import { adminRouterKeyRoutes } from "./router-keys.js";
|
|
11
|
+
import { adminSetupRoutes } from "./setup.js";
|
|
12
|
+
import { adminMonitorRoutes } from "./monitor.js";
|
|
10
13
|
export const adminRoutes = (app, options, done) => {
|
|
11
|
-
|
|
12
|
-
app.register(
|
|
13
|
-
app.register(
|
|
14
|
+
// Setup 路由不需要 auth
|
|
15
|
+
app.register(adminSetupRoutes, { db: options.db });
|
|
16
|
+
app.register(adminAuthPlugin, { db: options.db });
|
|
17
|
+
app.register(adminLoginRoutes, { db: options.db });
|
|
18
|
+
app.register(adminProviderRoutes, { db: options.db, semaphoreManager: options.semaphoreManager, tracker: options.tracker });
|
|
14
19
|
app.register(adminMappingRoutes, { db: options.db });
|
|
15
20
|
app.register(adminGroupRoutes, { db: options.db });
|
|
16
21
|
app.register(adminRetryRuleRoutes, { db: options.db, matcher: options.matcher });
|
|
17
22
|
app.register(adminLogRoutes, { db: options.db });
|
|
18
|
-
app.register(adminRouterKeyRoutes, { db: options.db
|
|
23
|
+
app.register(adminRouterKeyRoutes, { db: options.db });
|
|
19
24
|
app.register(adminStatsRoutes, { db: options.db });
|
|
20
25
|
app.register(adminMetricsRoutes, { db: options.db });
|
|
26
|
+
app.register(adminProxyEnhancementRoutes, { db: options.db });
|
|
27
|
+
app.register(adminMonitorRoutes, { tracker: options.tracker });
|
|
21
28
|
done();
|
|
22
29
|
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import jwt from "jsonwebtoken";
|
|
3
|
+
import { getSetting, setSetting, isInitialized } from "../db/settings.js";
|
|
4
|
+
import { hashPassword } from "../utils/password.js";
|
|
5
|
+
import { HTTP_BAD_REQUEST, HTTP_CONFLICT } from "./constants.js";
|
|
6
|
+
const CRYPTO_BYTES_LENGTH = 32;
|
|
7
|
+
const MIN_PASSWORD_LENGTH = 6;
|
|
8
|
+
export const adminSetupRoutes = (app, options, done) => {
|
|
9
|
+
const { db } = options;
|
|
10
|
+
app.get("/admin/api/setup/status", async () => {
|
|
11
|
+
return { initialized: isInitialized(db) };
|
|
12
|
+
});
|
|
13
|
+
app.post("/admin/api/setup/initialize", async (request, reply) => {
|
|
14
|
+
const { password } = request.body;
|
|
15
|
+
if (!password || password.length < MIN_PASSWORD_LENGTH) {
|
|
16
|
+
return reply.code(HTTP_BAD_REQUEST).send({ error: { message: `Password must be at least ${MIN_PASSWORD_LENGTH} characters` } });
|
|
17
|
+
}
|
|
18
|
+
// 事务中原子检查防竞态
|
|
19
|
+
const alreadyInitialized = db.transaction(() => {
|
|
20
|
+
if (isInitialized(db))
|
|
21
|
+
return true;
|
|
22
|
+
const encryptionKey = randomBytes(CRYPTO_BYTES_LENGTH).toString("hex");
|
|
23
|
+
const jwtSecret = randomBytes(CRYPTO_BYTES_LENGTH).toString("hex");
|
|
24
|
+
setSetting(db, "admin_password_hash", hashPassword(password));
|
|
25
|
+
setSetting(db, "encryption_key", encryptionKey);
|
|
26
|
+
setSetting(db, "jwt_secret", jwtSecret);
|
|
27
|
+
setSetting(db, "initialized", "true");
|
|
28
|
+
return false;
|
|
29
|
+
})();
|
|
30
|
+
if (alreadyInitialized) {
|
|
31
|
+
return reply.code(HTTP_CONFLICT).send({ error: { message: "Already initialized" } });
|
|
32
|
+
}
|
|
33
|
+
// 自动登录:签发 JWT
|
|
34
|
+
const TOKEN_EXPIRY_SECONDS = 172800; // 48 hours,与 admin-auth 保持一致
|
|
35
|
+
const secret = getSetting(db, "jwt_secret");
|
|
36
|
+
const token = jwt.sign({ role: "admin" }, secret, { expiresIn: TOKEN_EXPIRY_SECONDS });
|
|
37
|
+
reply.setCookie("admin_token", token, {
|
|
38
|
+
path: "/admin",
|
|
39
|
+
httpOnly: true,
|
|
40
|
+
secure: process.env.NODE_ENV === "production",
|
|
41
|
+
sameSite: "lax",
|
|
42
|
+
maxAge: TOKEN_EXPIRY_SECONDS,
|
|
43
|
+
});
|
|
44
|
+
return { success: true };
|
|
45
|
+
});
|
|
46
|
+
done();
|
|
47
|
+
};
|
package/dist/config.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import "dotenv/config";
|
|
2
1
|
export interface Config {
|
|
3
|
-
ADMIN_PASSWORD: string;
|
|
4
|
-
ENCRYPTION_KEY: string;
|
|
5
|
-
JWT_SECRET: string;
|
|
6
2
|
PORT: number;
|
|
7
3
|
DB_PATH: string;
|
|
8
4
|
LOG_LEVEL: string;
|
|
@@ -12,4 +8,5 @@ export interface Config {
|
|
|
12
8
|
RETRY_BASE_DELAY_MS: number;
|
|
13
9
|
}
|
|
14
10
|
export declare function resetConfig(): void;
|
|
11
|
+
export declare function getBaseConfig(): Config;
|
|
15
12
|
export declare function getConfig(): Config;
|
package/dist/config.js
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
|
-
import "
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
let cachedConfig = null;
|
|
4
|
+
function getDefaultDbPath() {
|
|
5
|
+
if (process.env.DB_PATH)
|
|
6
|
+
return process.env.DB_PATH;
|
|
7
|
+
return join(homedir(), ".llm-simple-router", "router.db");
|
|
8
|
+
}
|
|
3
9
|
export function resetConfig() {
|
|
4
10
|
cachedConfig = null;
|
|
5
11
|
}
|
|
6
|
-
export function
|
|
12
|
+
export function getBaseConfig() {
|
|
7
13
|
if (cachedConfig)
|
|
8
14
|
return cachedConfig;
|
|
9
|
-
const requiredVars = ["ADMIN_PASSWORD", "ENCRYPTION_KEY", "JWT_SECRET"];
|
|
10
|
-
for (const name of requiredVars) {
|
|
11
|
-
if (!process.env[name]) {
|
|
12
|
-
throw new Error(`Missing required environment variable: ${name}`);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
15
|
cachedConfig = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
JWT_SECRET: process.env.JWT_SECRET,
|
|
19
|
-
PORT: parseInt(process.env.PORT || "3000", 10),
|
|
20
|
-
DB_PATH: process.env.DB_PATH || "./data/router.db",
|
|
16
|
+
PORT: parseInt(process.env.PORT || "9981", 10),
|
|
17
|
+
DB_PATH: getDefaultDbPath(),
|
|
21
18
|
LOG_LEVEL: process.env.LOG_LEVEL || "info",
|
|
22
19
|
TZ: process.env.TZ || "Asia/Shanghai",
|
|
23
20
|
STREAM_TIMEOUT_MS: parseInt(process.env.STREAM_TIMEOUT_MS || "3000000", 10),
|
|
@@ -26,3 +23,6 @@ export function getConfig() {
|
|
|
26
23
|
};
|
|
27
24
|
return cachedConfig;
|
|
28
25
|
}
|
|
26
|
+
export function getConfig() {
|
|
27
|
+
return getBaseConfig();
|
|
28
|
+
}
|
package/dist/db/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
2
|
export declare function initDatabase(dbPath: string): Database.Database;
|
|
3
|
-
export { getActiveProviders, getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, } from "./providers.js";
|
|
3
|
+
export { getActiveProviders, getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
|
|
4
4
|
export type { Provider } from "./providers.js";
|
|
5
|
-
export { getModelMapping, getAllModelMappings, createModelMapping, updateModelMapping, deleteModelMapping, getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, } from "./mappings.js";
|
|
6
|
-
export type { ModelMapping, MappingGroup } from "./mappings.js";
|
|
5
|
+
export { getModelMapping, getAllModelMappings, createModelMapping, updateModelMapping, deleteModelMapping, getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getActiveProviderModels, resolveByProviderModel, } from "./mappings.js";
|
|
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
9
|
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, insertMetrics, } from "./logs.js";
|
|
@@ -14,3 +14,6 @@ export { getMetricsSummary, getMetricsTimeseries } from "./metrics.js";
|
|
|
14
14
|
export type { MetricsSummaryRow, MetricsTimeseriesRow, MetricsPeriod, MetricsMetric } from "./metrics.js";
|
|
15
15
|
export { getStats } from "./stats.js";
|
|
16
16
|
export type { Stats, StatsPeriod } from "./stats.js";
|
|
17
|
+
export { getSetting, setSetting, isInitialized } from "./settings.js";
|
|
18
|
+
export { getSessionStates, getSessionState, getSessionHistory, upsertSessionState, insertSessionHistory, deleteSessionState, } from "./session-states.js";
|
|
19
|
+
export type { SessionModelState, SessionModelHistory, UpsertSessionStateInput, InsertSessionHistoryInput } from "./session-states.js";
|