llm-simple-router 0.5.5 → 0.6.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/dist/admin/groups.js +17 -68
- package/dist/admin/logs.js +2 -0
- package/dist/admin/mappings.js +17 -13
- package/dist/admin/metrics.js +7 -6
- package/dist/admin/providers.js +22 -70
- package/dist/admin/routes.js +2 -0
- package/dist/admin/schedules.d.ts +7 -0
- package/dist/admin/schedules.js +210 -0
- package/dist/admin/stats.js +5 -3
- package/dist/admin/usage.js +40 -19
- package/dist/db/index.d.ts +4 -2
- package/dist/db/index.js +32 -2
- package/dist/db/logs.d.ts +1 -0
- package/dist/db/logs.js +13 -4
- package/dist/db/mappings.d.ts +0 -20
- package/dist/db/mappings.js +17 -34
- package/dist/db/metrics.d.ts +12 -1
- package/dist/db/metrics.js +29 -33
- package/dist/db/migrations/011_create_mapping_groups.sql +8 -4
- package/dist/db/migrations/024_add_mapping_groups_is_active.sql +3 -0
- package/dist/db/migrations/026_create_schedules_simplify_mappings.sql +64 -0
- package/dist/db/migrations/027_metrics_independent.sql +54 -0
- package/dist/db/migrations/028_ensure_strategy_column.sql +11 -0
- package/dist/db/migrations/029_convert_old_rule_format.sql +7 -0
- package/dist/db/migrations/030_add_input_tokens_estimated.sql +6 -0
- package/dist/db/migrations/031_add_tps_breakdown.sql +13 -0
- package/dist/db/migrations/032_add_non_thinking_tps.sql +3 -0
- package/dist/db/schedules.d.ts +31 -0
- package/dist/db/schedules.js +40 -0
- package/dist/db/stats.d.ts +3 -2
- package/dist/db/stats.js +16 -7
- package/dist/db/usage-windows.d.ts +5 -4
- package/dist/db/usage-windows.js +48 -20
- package/dist/metrics/metrics-extractor.d.ts +20 -0
- package/dist/metrics/metrics-extractor.js +105 -13
- package/dist/monitor/request-tracker.d.ts +5 -0
- package/dist/monitor/request-tracker.js +33 -0
- package/dist/monitor/types.d.ts +8 -0
- package/dist/proxy/mapping-resolver.d.ts +2 -2
- package/dist/proxy/mapping-resolver.js +144 -27
- package/dist/proxy/orchestrator.d.ts +3 -1
- package/dist/proxy/orchestrator.js +1 -1
- package/dist/proxy/overflow.js +1 -16
- package/dist/proxy/patch/deepseek/patch-orphan-tool-results.js +0 -2
- package/dist/proxy/proxy-handler.js +22 -11
- package/dist/proxy/proxy-logging.d.ts +1 -1
- package/dist/proxy/proxy-logging.js +17 -4
- package/dist/proxy/resilience.js +14 -1
- package/dist/proxy/scope.d.ts +2 -1
- package/dist/proxy/scope.js +2 -2
- package/dist/proxy/semaphore.d.ts +5 -1
- package/dist/proxy/semaphore.js +4 -2
- package/dist/proxy/strategy/types.d.ts +10 -8
- package/dist/proxy/strategy/types.js +1 -6
- package/dist/proxy/stream-proxy.js +25 -1
- package/dist/proxy/transport-fn.js +10 -0
- package/dist/proxy/usage-window-tracker.d.ts +5 -3
- package/dist/proxy/usage-window-tracker.js +21 -22
- package/dist/utils/time-range.d.ts +1 -1
- package/dist/utils/time-range.js +13 -7
- package/dist/utils/token-counter.d.ts +7 -0
- package/dist/utils/token-counter.js +81 -0
- package/frontend-dist/assets/{CardContent-CVofwD9T.js → CardContent-BtAcFNMy.js} +1 -1
- package/frontend-dist/assets/{CardTitle-MLH-EpHz.js → CardTitle-Bmwf1S5Y.js} +1 -1
- package/frontend-dist/assets/CascadingModelSelect-CicfrqcY.js +1 -0
- package/frontend-dist/assets/Checkbox-B1o39YuC.js +1 -0
- package/frontend-dist/assets/{CollapsibleTrigger-CbiBtEE8.js → CollapsibleTrigger-2jySTCeh.js} +1 -1
- package/frontend-dist/assets/{Collection-D9NeXDOI.js → Collection-ChUVejsh.js} +1 -1
- package/frontend-dist/assets/Dashboard-DkJauxYu.js +3 -0
- package/frontend-dist/assets/{DialogTitle-CTcpSq_w.js → DialogTitle-D0erB-Fr.js} +1 -1
- package/frontend-dist/assets/{Input-DDF6744B.js → Input-BDbKynVD.js} +1 -1
- package/frontend-dist/assets/{Label-_cw0EkS1.js → Label-CrHq5hrg.js} +1 -1
- package/frontend-dist/assets/{Login-Cca1HpLA.js → Login-D2YdqYnu.js} +1 -1
- package/frontend-dist/assets/Logs-DgeOPIkd.js +1 -0
- package/frontend-dist/assets/ModelMappings-De_UjiND.js +1 -0
- package/frontend-dist/assets/Monitor-BgRMReMF.js +1 -0
- package/frontend-dist/assets/PopoverTrigger-BVsxIE2L.js +1 -0
- package/frontend-dist/assets/PopperContent-B23SzU9H.js +1 -0
- package/frontend-dist/assets/Providers-DQypvsEg.js +1 -0
- package/frontend-dist/assets/ProxyEnhancement-Cijb2FID.js +5 -0
- package/frontend-dist/assets/RetryRules-CSseSPoO.js +1 -0
- package/frontend-dist/assets/{RouterKeys-DH7nUWDa.js → RouterKeys-ccwqoMCX.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-DyNl4G42.js → RovingFocusItem-rwA4uA9N.js} +1 -1
- package/frontend-dist/assets/Schedules-8YYNjLNo.js +1 -0
- package/frontend-dist/assets/SelectValue-TvIOOalu.js +1 -0
- package/frontend-dist/assets/{Settings-BmG999ol.js → Settings-D1WDm5lQ.js} +1 -1
- package/frontend-dist/assets/{Setup-D1xUsqUD.js → Setup-Bw-RIF9G.js} +1 -1
- package/frontend-dist/assets/{Switch-DrDIdQ_V.js → Switch-D9wFEsMF.js} +1 -1
- package/frontend-dist/assets/{TableHeader-BNPqkg9S.js → TableHeader-HOR173Xk.js} +1 -1
- package/frontend-dist/assets/{TabsTrigger-D-fGrdG3.js → TabsTrigger-BOsmgFYE.js} +1 -1
- package/frontend-dist/assets/{Teleport-BYzsRHC6.js → Teleport-BGbwtNTD.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-C3v2O7fX.js → TooltipTrigger-DPzNY2Sp.js} +1 -1
- package/frontend-dist/assets/UnifiedRequestDialog-BjEigSaR.css +1 -0
- package/frontend-dist/assets/UnifiedRequestDialog-CevmD2P2.js +3 -0
- package/frontend-dist/assets/VisuallyHidden-ChauvWtH.js +1 -0
- package/frontend-dist/assets/{VisuallyHiddenInput-Bf9hgsd7.js → VisuallyHiddenInput-BcDuL0V8.js} +1 -1
- package/frontend-dist/assets/{alert-dialog-Bn1IauFR.js → alert-dialog-DgtxmV7t.js} +1 -1
- package/frontend-dist/assets/arrow-down-BuK6B6yc.js +1 -0
- package/frontend-dist/assets/{badge-dyL-tG0V.js → badge-NUBqZBxu.js} +1 -1
- package/frontend-dist/assets/{button-B6f3Nfab.js → button-BLX8zWc1.js} +2 -2
- package/frontend-dist/assets/check-CsZv9cnK.js +1 -0
- package/frontend-dist/assets/constants-D_0jiLjw.js +1 -0
- package/frontend-dist/assets/copy-BMWzukd1.js +1 -0
- package/frontend-dist/assets/{dialog-1FvPG-71.js → dialog-Dsvgfiw-.js} +1 -1
- package/frontend-dist/assets/{file-text-BFyk5DT1.js → file-text-CqJ33eWr.js} +1 -1
- package/frontend-dist/assets/{format-K3VR67cG.js → format-Dln15Luw.js} +1 -1
- package/frontend-dist/assets/index-Bqo2qo88.js +1 -0
- package/frontend-dist/assets/index-_Icfkt3I.css +1 -0
- package/frontend-dist/assets/{lib-C_27TgFv.js → lib-DQotd1d8.js} +1 -1
- package/frontend-dist/assets/loader-circle-CnEL8ILi.js +1 -0
- package/frontend-dist/assets/ohash.D__AXeF1-D5e5Wyzx.js +1 -0
- package/frontend-dist/assets/{useClipboard-DCoGBvxy.js → useClipboard-BDAhyrgL.js} +1 -1
- package/frontend-dist/assets/useFocusGuards-CHAbXhQp.js +1 -0
- package/frontend-dist/assets/useFormControl-CCVkIi3o.js +1 -0
- package/frontend-dist/assets/{useLogRetention-BGcJntJ-.js → useLogRetention-X-CkHhJ7.js} +1 -1
- package/frontend-dist/assets/useNonce-DyF1ycZV.js +1 -0
- package/frontend-dist/assets/x-DMAovOe-.js +1 -0
- package/frontend-dist/index.html +22 -20
- package/package.json +2 -2
- package/dist/proxy/strategy/failover.d.ts +0 -4
- package/dist/proxy/strategy/failover.js +0 -8
- package/dist/proxy/strategy/random.d.ts +0 -4
- package/dist/proxy/strategy/random.js +0 -11
- package/dist/proxy/strategy/round-robin.d.ts +0 -5
- package/dist/proxy/strategy/round-robin.js +0 -16
- package/dist/proxy/strategy/scheduled.d.ts +0 -4
- package/dist/proxy/strategy/scheduled.js +0 -55
- package/dist/proxy/strategy/targets-rule.d.ts +0 -8
- package/dist/proxy/strategy/targets-rule.js +0 -19
- package/frontend-dist/assets/Checkbox-KiNnjeB_.js +0 -1
- package/frontend-dist/assets/Dashboard-CuBkXd58.js +0 -3
- package/frontend-dist/assets/Logs-CUIPRZEO.js +0 -1
- package/frontend-dist/assets/ModelMappings-DEswiX1e.js +0 -1
- package/frontend-dist/assets/Monitor-BXpf9sjE.js +0 -1
- package/frontend-dist/assets/PopoverTrigger-DIOWDISK.js +0 -1
- package/frontend-dist/assets/PopperContent-DIEU7Y6E.js +0 -1
- package/frontend-dist/assets/Providers-CvYtuvfg.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-BxTbZGYB.js +0 -5
- package/frontend-dist/assets/RetryRules-COK28WDb.js +0 -1
- package/frontend-dist/assets/SelectValue-DyjOpyaF.js +0 -1
- package/frontend-dist/assets/UnifiedRequestDialog-B2nt8nLl.css +0 -1
- package/frontend-dist/assets/UnifiedRequestDialog-DJOIbAUb.js +0 -3
- package/frontend-dist/assets/VisuallyHidden-Bz0LPzC7.js +0 -1
- package/frontend-dist/assets/chevron-down-DKTxr1tk.js +0 -1
- package/frontend-dist/assets/circle-question-mark-WJzB-je7.js +0 -1
- package/frontend-dist/assets/index-B2SjbR82.js +0 -1
- package/frontend-dist/assets/index-uA-D1xVT.css +0 -1
- package/frontend-dist/assets/loader-circle-DEqD4BxX.js +0 -1
- package/frontend-dist/assets/ohash.D__AXeF1-BcwqVPVj.js +0 -1
- package/frontend-dist/assets/useNonce-Ccld5giw.js +0 -1
- package/frontend-dist/assets/x-iwmjHBXS.js +0 -1
package/dist/admin/groups.js
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getProviderById, getMappingGroupById, } from "../db/index.js";
|
|
3
|
-
import { STRATEGY_NAMES } from "../proxy/strategy/types.js";
|
|
4
3
|
import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_CONFLICT, HTTP_NOT_FOUND } from "./constants.js";
|
|
5
4
|
import { API_CODE, apiError } from "./api-response.js";
|
|
6
|
-
const MIN_FAILOVER_TARGETS = 2;
|
|
7
5
|
const CreateGroupSchema = Type.Object({
|
|
8
6
|
client_model: Type.String({ minLength: 1 }),
|
|
9
|
-
strategy: Type.String({ minLength: 1 }),
|
|
10
7
|
rule: Type.String(),
|
|
11
8
|
});
|
|
12
9
|
const UpdateGroupSchema = Type.Object({
|
|
13
10
|
client_model: Type.Optional(Type.String({ minLength: 1 })),
|
|
14
|
-
strategy: Type.Optional(Type.String({ minLength: 1 })),
|
|
15
11
|
rule: Type.Optional(Type.String()),
|
|
16
12
|
});
|
|
17
13
|
function validateOverflow(db, target, label) {
|
|
@@ -31,11 +27,7 @@ function validateOverflow(db, target, label) {
|
|
|
31
27
|
}
|
|
32
28
|
return undefined;
|
|
33
29
|
}
|
|
34
|
-
|
|
35
|
-
const VALID_STRATEGIES = new Set(Object.values(STRATEGY_NAMES));
|
|
36
|
-
if (!VALID_STRATEGIES.has(strategy)) {
|
|
37
|
-
return `Unknown strategy '${strategy}'. Valid: ${[...VALID_STRATEGIES].join(", ")}`;
|
|
38
|
-
}
|
|
30
|
+
function validateRule(db, ruleJson) {
|
|
39
31
|
let rule;
|
|
40
32
|
try {
|
|
41
33
|
rule = JSON.parse(ruleJson);
|
|
@@ -43,59 +35,24 @@ async function validateRule(db, strategy, ruleJson) {
|
|
|
43
35
|
catch {
|
|
44
36
|
return "Invalid rule JSON";
|
|
45
37
|
}
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
if (typeof rule !== "object" || rule === null)
|
|
39
|
+
return "Invalid rule";
|
|
40
|
+
const r = rule;
|
|
41
|
+
if (!Array.isArray(r.targets) || r.targets.length === 0) {
|
|
42
|
+
return "rule.targets must be a non-empty array";
|
|
43
|
+
}
|
|
44
|
+
for (let i = 0; i < r.targets.length; i++) {
|
|
45
|
+
const t = r.targets[i];
|
|
46
|
+
if (!t.backend_model || !t.provider_id) {
|
|
47
|
+
return `targets[${i}] missing backend_model or provider_id`;
|
|
50
48
|
}
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
53
|
-
return `provider_id '${
|
|
49
|
+
const p = getProviderById(db, t.provider_id);
|
|
50
|
+
if (!p) {
|
|
51
|
+
return `targets[${i}] provider_id '${t.provider_id}' not found`;
|
|
54
52
|
}
|
|
55
|
-
const overflowErr = validateOverflow(db,
|
|
53
|
+
const overflowErr = validateOverflow(db, t, `targets[${i}]`);
|
|
56
54
|
if (overflowErr)
|
|
57
55
|
return overflowErr;
|
|
58
|
-
if (r.windows !== undefined && !Array.isArray(r.windows)) {
|
|
59
|
-
return "rule.windows must be an array";
|
|
60
|
-
}
|
|
61
|
-
if (Array.isArray(r.windows)) {
|
|
62
|
-
for (let i = 0; i < r.windows.length; i++) {
|
|
63
|
-
const w = r.windows[i];
|
|
64
|
-
if (!w.start || !w.end || !w.target || !w.target.backend_model || !w.target.provider_id) {
|
|
65
|
-
return `window[${i}] missing start/end/target.backend_model/target.provider_id`;
|
|
66
|
-
}
|
|
67
|
-
const p = getProviderById(db, w.target.provider_id);
|
|
68
|
-
if (!p) {
|
|
69
|
-
return `window[${i}] provider_id '${w.target.provider_id}' not found`;
|
|
70
|
-
}
|
|
71
|
-
const wOverflowErr = validateOverflow(db, w.target, `window[${i}]`);
|
|
72
|
-
if (wOverflowErr)
|
|
73
|
-
return wOverflowErr;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
if (strategy === STRATEGY_NAMES.ROUND_ROBIN || strategy === STRATEGY_NAMES.RANDOM || strategy === STRATEGY_NAMES.FAILOVER) {
|
|
78
|
-
const r = rule;
|
|
79
|
-
if (!Array.isArray(r.targets) || r.targets.length === 0) {
|
|
80
|
-
return "rule.targets must be a non-empty array";
|
|
81
|
-
}
|
|
82
|
-
const minTargets = strategy === STRATEGY_NAMES.FAILOVER ? MIN_FAILOVER_TARGETS : 1;
|
|
83
|
-
if (r.targets.length < minTargets) {
|
|
84
|
-
return `strategy '${strategy}' requires at least ${minTargets} target(s)`;
|
|
85
|
-
}
|
|
86
|
-
for (let i = 0; i < r.targets.length; i++) {
|
|
87
|
-
const t = r.targets[i];
|
|
88
|
-
if (!t.backend_model || !t.provider_id) {
|
|
89
|
-
return `targets[${i}] missing backend_model or provider_id`;
|
|
90
|
-
}
|
|
91
|
-
const p = getProviderById(db, t.provider_id);
|
|
92
|
-
if (!p) {
|
|
93
|
-
return `targets[${i}] provider_id '${t.provider_id}' not found`;
|
|
94
|
-
}
|
|
95
|
-
const overflowErr = validateOverflow(db, t, `targets[${i}]`);
|
|
96
|
-
if (overflowErr)
|
|
97
|
-
return overflowErr;
|
|
98
|
-
}
|
|
99
56
|
}
|
|
100
57
|
return undefined;
|
|
101
58
|
}
|
|
@@ -107,14 +64,13 @@ export const adminGroupRoutes = (app, options, done) => {
|
|
|
107
64
|
});
|
|
108
65
|
app.post("/admin/api/mapping-groups", { schema: { body: CreateGroupSchema } }, async (request, reply) => {
|
|
109
66
|
const body = request.body;
|
|
110
|
-
const validationError =
|
|
67
|
+
const validationError = validateRule(db, body.rule);
|
|
111
68
|
if (validationError) {
|
|
112
69
|
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, validationError));
|
|
113
70
|
}
|
|
114
71
|
try {
|
|
115
72
|
const id = createMappingGroup(db, {
|
|
116
73
|
client_model: body.client_model,
|
|
117
|
-
strategy: body.strategy,
|
|
118
74
|
rule: body.rule,
|
|
119
75
|
});
|
|
120
76
|
return reply.code(HTTP_CREATED).send({ id });
|
|
@@ -132,13 +88,10 @@ export const adminGroupRoutes = (app, options, done) => {
|
|
|
132
88
|
const fields = {};
|
|
133
89
|
if (body.client_model !== undefined)
|
|
134
90
|
fields.client_model = body.client_model;
|
|
135
|
-
if (body.strategy !== undefined)
|
|
136
|
-
fields.strategy = body.strategy;
|
|
137
91
|
if (body.rule !== undefined)
|
|
138
92
|
fields.rule = body.rule;
|
|
139
|
-
const strategy = body.strategy ?? findGroupStrategy(db, id);
|
|
140
93
|
const ruleJson = body.rule ?? findGroupRule(db, id);
|
|
141
|
-
const validationError =
|
|
94
|
+
const validationError = validateRule(db, ruleJson);
|
|
142
95
|
if (validationError) {
|
|
143
96
|
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, validationError));
|
|
144
97
|
}
|
|
@@ -172,10 +125,6 @@ export const adminGroupRoutes = (app, options, done) => {
|
|
|
172
125
|
});
|
|
173
126
|
done();
|
|
174
127
|
};
|
|
175
|
-
function findGroupStrategy(db, id) {
|
|
176
|
-
const g = getMappingGroupById(db, id);
|
|
177
|
-
return g?.strategy ?? STRATEGY_NAMES.SCHEDULED;
|
|
178
|
-
}
|
|
179
128
|
function findGroupRule(db, id) {
|
|
180
129
|
const g = getMappingGroupById(db, id);
|
|
181
130
|
return g?.rule ?? "{}";
|
package/dist/admin/logs.js
CHANGED
|
@@ -11,6 +11,7 @@ const LogQuerySchema = Type.Object({
|
|
|
11
11
|
provider_id: Type.Optional(Type.String()),
|
|
12
12
|
start_time: Type.Optional(Type.String()),
|
|
13
13
|
end_time: Type.Optional(Type.String()),
|
|
14
|
+
status_code: Type.Optional(Type.String()),
|
|
14
15
|
view: Type.Optional(Type.Literal("grouped")),
|
|
15
16
|
});
|
|
16
17
|
const DeleteLogsBeforeSchema = Type.Object({
|
|
@@ -33,6 +34,7 @@ export const adminLogRoutes = (app, options, done) => {
|
|
|
33
34
|
provider_id: query.provider_id || undefined,
|
|
34
35
|
start_time: query.start_time || undefined,
|
|
35
36
|
end_time: query.end_time || undefined,
|
|
37
|
+
status_code: query.status_code || undefined,
|
|
36
38
|
};
|
|
37
39
|
const result = view === "grouped"
|
|
38
40
|
? getRequestLogsGrouped(db, listOptions)
|
package/dist/admin/mappings.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getProviderById, getMappingGroupById, getMappingGroup, } from "../db/index.js";
|
|
3
|
-
import { STRATEGY_NAMES } from "../proxy/strategy/types.js";
|
|
4
3
|
import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND, HTTP_CONFLICT } from "./constants.js";
|
|
5
4
|
import { API_CODE, apiError } from "./api-response.js";
|
|
6
5
|
const CreateMappingSchema = Type.Object({
|
|
@@ -21,14 +20,14 @@ function toLegacy(group) {
|
|
|
21
20
|
catch {
|
|
22
21
|
return null;
|
|
23
22
|
}
|
|
24
|
-
const
|
|
25
|
-
if (!
|
|
23
|
+
const targets = rule?.targets;
|
|
24
|
+
if (!targets || targets.length === 0)
|
|
26
25
|
return null;
|
|
27
26
|
return {
|
|
28
27
|
id: group.id,
|
|
29
28
|
client_model: group.client_model,
|
|
30
|
-
backend_model:
|
|
31
|
-
provider_id:
|
|
29
|
+
backend_model: targets[0].backend_model ?? "",
|
|
30
|
+
provider_id: targets[0].provider_id ?? "",
|
|
32
31
|
is_active: 1,
|
|
33
32
|
created_at: group.created_at,
|
|
34
33
|
};
|
|
@@ -52,10 +51,8 @@ export const adminMappingRoutes = (app, options, done) => {
|
|
|
52
51
|
try {
|
|
53
52
|
const id = createMappingGroup(db, {
|
|
54
53
|
client_model: body.client_model,
|
|
55
|
-
strategy: STRATEGY_NAMES.SCHEDULED,
|
|
56
54
|
rule: JSON.stringify({
|
|
57
|
-
|
|
58
|
-
windows: [],
|
|
55
|
+
targets: [{ backend_model: body.backend_model, provider_id: body.provider_id }],
|
|
59
56
|
}),
|
|
60
57
|
});
|
|
61
58
|
return reply.code(HTTP_CREATED).send({ id });
|
|
@@ -79,19 +76,26 @@ export const adminMappingRoutes = (app, options, done) => {
|
|
|
79
76
|
rule = JSON.parse(group.rule);
|
|
80
77
|
}
|
|
81
78
|
catch {
|
|
82
|
-
rule = {
|
|
79
|
+
rule = { targets: [{}] };
|
|
83
80
|
}
|
|
84
|
-
const
|
|
81
|
+
const targets = rule.targets || [];
|
|
82
|
+
const firstTarget = { ...(targets[0] || {}) };
|
|
85
83
|
if (body.backend_model !== undefined)
|
|
86
|
-
|
|
84
|
+
firstTarget.backend_model = body.backend_model;
|
|
87
85
|
if (body.provider_id !== undefined) {
|
|
88
86
|
const provider = getProviderById(db, body.provider_id);
|
|
89
87
|
if (!provider) {
|
|
90
88
|
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.NOT_FOUND, "provider_id not found"));
|
|
91
89
|
}
|
|
92
|
-
|
|
90
|
+
firstTarget.provider_id = body.provider_id;
|
|
93
91
|
}
|
|
94
|
-
|
|
92
|
+
if (targets.length > 0) {
|
|
93
|
+
targets[0] = firstTarget;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
targets.push(firstTarget);
|
|
97
|
+
}
|
|
98
|
+
rule.targets = targets;
|
|
95
99
|
const fields = {
|
|
96
100
|
rule: JSON.stringify(rule),
|
|
97
101
|
};
|
package/dist/admin/metrics.js
CHANGED
|
@@ -10,8 +10,9 @@ const DashboardPeriodEnum = Type.Union([
|
|
|
10
10
|
]);
|
|
11
11
|
const PeriodEnum = Type.Union([LegacyPeriodEnum, DashboardPeriodEnum]);
|
|
12
12
|
const MetricEnum = Type.Union([
|
|
13
|
-
Type.Literal("ttft"), Type.Literal("tps"), Type.Literal("
|
|
14
|
-
Type.Literal("
|
|
13
|
+
Type.Literal("ttft"), Type.Literal("tps"), Type.Literal("text_tps"),
|
|
14
|
+
Type.Literal("thinking_tps"), Type.Literal("tool_use_tps"), Type.Literal("total_tps"),
|
|
15
|
+
Type.Literal("tokens"), Type.Literal("cache_rate"), Type.Literal("request_count"),
|
|
15
16
|
Type.Literal("input_tokens"), Type.Literal("output_tokens"),
|
|
16
17
|
Type.Literal("cache_hit_tokens"),
|
|
17
18
|
]);
|
|
@@ -33,13 +34,13 @@ const TimeseriesQuerySchema = Type.Object({
|
|
|
33
34
|
end_time: Type.Optional(Type.String()),
|
|
34
35
|
});
|
|
35
36
|
const DASHBOARD_PERIODS = new Set(["window", "weekly", "monthly"]);
|
|
36
|
-
function resolveMetricsTime(query, db, routerKeyId) {
|
|
37
|
+
function resolveMetricsTime(query, db, routerKeyId, providerId) {
|
|
37
38
|
if (query.start_time && query.end_time) {
|
|
38
39
|
return { startTime: query.start_time, endTime: query.end_time, legacyPeriod: "30d" };
|
|
39
40
|
}
|
|
40
41
|
const period = query.period ?? "weekly";
|
|
41
42
|
if (DASHBOARD_PERIODS.has(period)) {
|
|
42
|
-
const range = resolveTimeRange(period, db, routerKeyId);
|
|
43
|
+
const range = resolveTimeRange(period, db, routerKeyId, providerId);
|
|
43
44
|
return { startTime: range.startTime, endTime: range.endTime, legacyPeriod: "5h" };
|
|
44
45
|
}
|
|
45
46
|
return { legacyPeriod: period };
|
|
@@ -48,14 +49,14 @@ export const adminMetricsRoutes = (app, options, done) => {
|
|
|
48
49
|
const { db } = options;
|
|
49
50
|
app.get("/admin/api/metrics/summary", { schema: { querystring: SummaryQuerySchema } }, async (request, reply) => {
|
|
50
51
|
const query = request.query;
|
|
51
|
-
const { startTime, endTime, legacyPeriod } = resolveMetricsTime(query, db, query.router_key_id);
|
|
52
|
+
const { startTime, endTime, legacyPeriod } = resolveMetricsTime(query, db, query.router_key_id, query.provider_id);
|
|
52
53
|
const summary = getMetricsSummary(db, legacyPeriod, query.provider_id, query.backend_model, query.router_key_id, startTime, endTime);
|
|
53
54
|
return reply.send(summary);
|
|
54
55
|
});
|
|
55
56
|
app.get("/admin/api/metrics/timeseries", { schema: { querystring: TimeseriesQuerySchema } }, async (request, reply) => {
|
|
56
57
|
const query = request.query;
|
|
57
58
|
const metric = query.metric;
|
|
58
|
-
const { startTime, endTime, legacyPeriod } = resolveMetricsTime(query, db, query.router_key_id);
|
|
59
|
+
const { startTime, endTime, legacyPeriod } = resolveMetricsTime(query, db, query.router_key_id, query.provider_id);
|
|
59
60
|
const timeseries = getMetricsTimeseries(db, legacyPeriod, metric, query.provider_id, query.backend_model, query.router_key_id, startTime, endTime);
|
|
60
61
|
return reply.send(timeseries);
|
|
61
62
|
});
|
package/dist/admin/providers.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
-
import { getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, getAllMappingGroups,
|
|
2
|
+
import { getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, getAllMappingGroups, updateMappingGroup, 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 { HTTP_CREATED, HTTP_NOT_FOUND, HTTP_CONFLICT, HTTP_BAD_REQUEST } from "./constants.js";
|
|
@@ -22,55 +22,29 @@ function cascadeProviderDisable(db, providerId) {
|
|
|
22
22
|
}
|
|
23
23
|
let modified = false;
|
|
24
24
|
let shouldDisable = false;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
// 归一化旧格式 { default, windows } → { targets }(向后兼容 migration 026 前数据)
|
|
26
|
+
// eslint-disable-next-line taste/no-deprecated-rule-format
|
|
27
|
+
if (!Array.isArray(rule.targets) && typeof rule.default === "object" && rule.default !== null) {
|
|
28
|
+
// eslint-disable-next-line taste/no-deprecated-rule-format
|
|
29
|
+
rule.targets = [rule.default];
|
|
30
|
+
}
|
|
31
|
+
const targets = rule.targets;
|
|
32
|
+
if (Array.isArray(targets)) {
|
|
33
|
+
const filtered = targets.filter((t) => {
|
|
34
|
+
if (t.provider_id === providerId) {
|
|
30
35
|
modified = true;
|
|
36
|
+
return false;
|
|
31
37
|
}
|
|
32
|
-
if (
|
|
33
|
-
delete
|
|
34
|
-
delete
|
|
38
|
+
if (t.overflow_provider_id === providerId) {
|
|
39
|
+
delete t.overflow_provider_id;
|
|
40
|
+
delete t.overflow_model;
|
|
35
41
|
modified = true;
|
|
36
42
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
modified = true;
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
if (w.target?.overflow_provider_id === providerId) {
|
|
46
|
-
delete w.target.overflow_provider_id;
|
|
47
|
-
delete w.target.overflow_model;
|
|
48
|
-
modified = true;
|
|
49
|
-
}
|
|
50
|
-
return true;
|
|
51
|
-
});
|
|
52
|
-
rule.windows = filtered;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
const targets = rule.targets;
|
|
57
|
-
if (Array.isArray(targets)) {
|
|
58
|
-
const filtered = targets.filter((t) => {
|
|
59
|
-
if (t.provider_id === providerId) {
|
|
60
|
-
modified = true;
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
if (t.overflow_provider_id === providerId) {
|
|
64
|
-
delete t.overflow_provider_id;
|
|
65
|
-
delete t.overflow_model;
|
|
66
|
-
modified = true;
|
|
67
|
-
}
|
|
68
|
-
return true;
|
|
69
|
-
});
|
|
70
|
-
rule.targets = filtered;
|
|
71
|
-
if (filtered.length === 0 && modified) {
|
|
72
|
-
shouldDisable = true;
|
|
73
|
-
}
|
|
43
|
+
return true;
|
|
44
|
+
});
|
|
45
|
+
rule.targets = filtered;
|
|
46
|
+
if (filtered.length === 0 && modified) {
|
|
47
|
+
shouldDisable = true;
|
|
74
48
|
}
|
|
75
49
|
}
|
|
76
50
|
if (modified) {
|
|
@@ -256,22 +230,6 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
256
230
|
const refs = [];
|
|
257
231
|
try {
|
|
258
232
|
const rule = JSON.parse(g.rule);
|
|
259
|
-
if (rule.default?.provider_id === id) {
|
|
260
|
-
refs.push(`默认模型 (${rule.default.backend_model})`);
|
|
261
|
-
}
|
|
262
|
-
if (rule.default?.overflow_provider_id === id) {
|
|
263
|
-
refs.push(`默认溢出模型 (${rule.default.overflow_model || "-"})`);
|
|
264
|
-
}
|
|
265
|
-
if (Array.isArray(rule.windows)) {
|
|
266
|
-
for (const w of rule.windows) {
|
|
267
|
-
if (w.target?.provider_id === id) {
|
|
268
|
-
refs.push(`时间窗口 ${w.start}-${w.end} (${w.target.backend_model})`);
|
|
269
|
-
}
|
|
270
|
-
if (w.target?.overflow_provider_id === id) {
|
|
271
|
-
refs.push(`时间窗口 ${w.start}-${w.end} 溢出 (${w.target.overflow_model || "-"})`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
233
|
if (Array.isArray(rule.targets)) {
|
|
276
234
|
for (let i = 0; i < rule.targets.length; i++) {
|
|
277
235
|
const t = rule.targets[i];
|
|
@@ -291,12 +249,6 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
291
249
|
references.push(`映射分组「${g.client_model}」: ${ref}`);
|
|
292
250
|
}
|
|
293
251
|
}
|
|
294
|
-
const mappings = getAllModelMappings(db);
|
|
295
|
-
for (const m of mappings) {
|
|
296
|
-
if (m.provider_id === id) {
|
|
297
|
-
references.push(`旧版映射「${m.client_model}」→ ${m.backend_model}`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
252
|
return reply.send({ references });
|
|
301
253
|
});
|
|
302
254
|
app.delete("/admin/api/providers/:id", async (request, reply) => {
|
|
@@ -309,8 +261,8 @@ export const adminProviderRoutes = (app, options, done) => {
|
|
|
309
261
|
for (const g of groups) {
|
|
310
262
|
try {
|
|
311
263
|
const rule = JSON.parse(g.rule);
|
|
312
|
-
const targets =
|
|
313
|
-
if (targets.some((t) => t
|
|
264
|
+
const targets = Array.isArray(rule.targets) ? rule.targets : [];
|
|
265
|
+
if (targets.some((t) => t?.provider_id === id)) {
|
|
314
266
|
return reply.code(HTTP_CONFLICT).send(apiError(API_CODE.CONFLICT_REFERENCED, `Provider is referenced by mapping group '${g.client_model}'`));
|
|
315
267
|
}
|
|
316
268
|
}
|
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 { adminScheduleRoutes } from "./schedules.js";
|
|
18
19
|
export const adminRoutes = (app, options, done) => {
|
|
19
20
|
// Setup 路由不需要 auth
|
|
20
21
|
app.register(adminSetupRoutes, { db: options.db });
|
|
@@ -23,6 +24,7 @@ export const adminRoutes = (app, options, done) => {
|
|
|
23
24
|
app.register(adminProviderRoutes, { db: options.db, semaphoreManager: options.semaphoreManager, tracker: options.tracker });
|
|
24
25
|
app.register(adminMappingRoutes, { db: options.db });
|
|
25
26
|
app.register(adminGroupRoutes, { db: options.db });
|
|
27
|
+
app.register(adminScheduleRoutes, { db: options.db });
|
|
26
28
|
app.register(adminRetryRuleRoutes, { db: options.db, matcher: options.matcher });
|
|
27
29
|
app.register(adminLogRoutes, { db: options.db });
|
|
28
30
|
app.register(adminRouterKeyRoutes, { db: options.db });
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { getSchedulesByGroup, getAllSchedules, getScheduleById, createSchedule, updateSchedule, deleteSchedule, } from "../db/index.js";
|
|
3
|
+
import { getMappingGroupById, getProviderById } from "../db/index.js";
|
|
4
|
+
import { HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND } from "./constants.js";
|
|
5
|
+
import { API_CODE, apiError } from "./api-response.js";
|
|
6
|
+
const CreateScheduleSchema = Type.Object({
|
|
7
|
+
mapping_group_id: Type.String({ minLength: 1 }),
|
|
8
|
+
name: Type.String({ minLength: 1 }),
|
|
9
|
+
week: Type.String(),
|
|
10
|
+
start_hour: Type.Number({ minimum: 0, maximum: 23 }),
|
|
11
|
+
end_hour: Type.Number({ minimum: 1, maximum: 24 }),
|
|
12
|
+
mapping_rule: Type.String(),
|
|
13
|
+
concurrency_rule: Type.Optional(Type.String()),
|
|
14
|
+
});
|
|
15
|
+
const UpdateScheduleSchema = Type.Object({
|
|
16
|
+
name: Type.Optional(Type.String({ minLength: 1 })),
|
|
17
|
+
enabled: Type.Optional(Type.Number()),
|
|
18
|
+
week: Type.Optional(Type.String()),
|
|
19
|
+
start_hour: Type.Optional(Type.Number({ minimum: 0, maximum: 23 })),
|
|
20
|
+
end_hour: Type.Optional(Type.Number({ minimum: 1, maximum: 24 })),
|
|
21
|
+
mapping_rule: Type.Optional(Type.String()),
|
|
22
|
+
concurrency_rule: Type.Optional(Type.String()),
|
|
23
|
+
});
|
|
24
|
+
function validateMappingRule(db, ruleJson) {
|
|
25
|
+
let rule;
|
|
26
|
+
try {
|
|
27
|
+
rule = JSON.parse(ruleJson);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "Invalid mapping_rule JSON";
|
|
31
|
+
}
|
|
32
|
+
if (typeof rule !== "object" || rule === null)
|
|
33
|
+
return "Invalid mapping_rule";
|
|
34
|
+
const r = rule;
|
|
35
|
+
if (!Array.isArray(r.targets) || r.targets.length === 0) {
|
|
36
|
+
return "mapping_rule.targets must be a non-empty array";
|
|
37
|
+
}
|
|
38
|
+
for (let i = 0; i < r.targets.length; i++) {
|
|
39
|
+
const t = r.targets[i];
|
|
40
|
+
if (!t.backend_model || !t.provider_id) {
|
|
41
|
+
return `targets[${i}] missing backend_model or provider_id`;
|
|
42
|
+
}
|
|
43
|
+
const p = getProviderById(db, t.provider_id);
|
|
44
|
+
if (!p)
|
|
45
|
+
return `targets[${i}] provider_id '${t.provider_id}' not found`;
|
|
46
|
+
const hasOverflowProvider = !!t.overflow_provider_id;
|
|
47
|
+
const hasOverflowModel = !!t.overflow_model;
|
|
48
|
+
if (hasOverflowProvider && !hasOverflowModel) {
|
|
49
|
+
return `targets[${i}]: overflow_provider_id requires overflow_model`;
|
|
50
|
+
}
|
|
51
|
+
if (hasOverflowModel && !hasOverflowProvider) {
|
|
52
|
+
return `targets[${i}]: overflow_model requires overflow_provider_id`;
|
|
53
|
+
}
|
|
54
|
+
if (hasOverflowProvider) {
|
|
55
|
+
const op = getProviderById(db, t.overflow_provider_id);
|
|
56
|
+
if (!op)
|
|
57
|
+
return `targets[${i}]: overflow_provider_id '${t.overflow_provider_id}' not found`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/** 解析 week JSON 为数字数组,失败返回 null */
|
|
63
|
+
function parseWeekSafe(weekJson) {
|
|
64
|
+
try {
|
|
65
|
+
const arr = JSON.parse(weekJson);
|
|
66
|
+
if (!Array.isArray(arr) || !arr.every((d) => typeof d === "number" && d >= 0 && d <= 6))
|
|
67
|
+
return null;
|
|
68
|
+
return arr;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** 检查同组 schedules 是否与 [startHour, endHour) 时段重叠(按星期交集判断) */
|
|
75
|
+
function checkOverlap(db, groupId, excludeId, weekDays, startHour, endHour) {
|
|
76
|
+
const existing = getSchedulesByGroup(db, groupId);
|
|
77
|
+
for (const s of existing) {
|
|
78
|
+
if (excludeId && s.id === excludeId)
|
|
79
|
+
continue;
|
|
80
|
+
const sWeek = parseWeekSafe(s.week);
|
|
81
|
+
if (!sWeek)
|
|
82
|
+
continue;
|
|
83
|
+
// 星期有交集 AND 时段有交集 才算重叠
|
|
84
|
+
const weekOverlap = weekDays.some(d => sWeek.includes(d));
|
|
85
|
+
const timeOverlap = startHour < s.end_hour && endHour > s.start_hour;
|
|
86
|
+
if (weekOverlap && timeOverlap) {
|
|
87
|
+
const days = weekDays.map(d => ["周日", "周一", "周二", "周三", "周四", "周五", "周六"][d]).join("、");
|
|
88
|
+
return `时段与「${s.name}」重叠 (${days} ${formatHour(startHour)}-${formatHour(endHour)} vs ${formatHour(s.start_hour)}-${formatHour(s.end_hour)})`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
function formatHour(h) {
|
|
94
|
+
return String(h).padStart(2, "0") + ":00";
|
|
95
|
+
}
|
|
96
|
+
export const adminScheduleRoutes = (app, options, done) => {
|
|
97
|
+
const { db } = options;
|
|
98
|
+
app.get("/admin/api/schedules", async (_request, reply) => {
|
|
99
|
+
const schedules = getAllSchedules(db);
|
|
100
|
+
return reply.send(schedules);
|
|
101
|
+
});
|
|
102
|
+
app.get("/admin/api/schedules/group/:groupId", async (request, reply) => {
|
|
103
|
+
const { groupId } = request.params;
|
|
104
|
+
const group = getMappingGroupById(db, groupId);
|
|
105
|
+
if (!group)
|
|
106
|
+
return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Mapping group not found"));
|
|
107
|
+
const schedules = getSchedulesByGroup(db, groupId);
|
|
108
|
+
return reply.send(schedules);
|
|
109
|
+
});
|
|
110
|
+
app.post("/admin/api/schedules", { schema: { body: CreateScheduleSchema } }, async (request, reply) => {
|
|
111
|
+
const body = request.body;
|
|
112
|
+
const group = getMappingGroupById(db, body.mapping_group_id);
|
|
113
|
+
if (!group)
|
|
114
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "mapping_group_id not found"));
|
|
115
|
+
const ruleErr = validateMappingRule(db, body.mapping_rule);
|
|
116
|
+
if (ruleErr)
|
|
117
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, ruleErr));
|
|
118
|
+
if (body.concurrency_rule) {
|
|
119
|
+
try {
|
|
120
|
+
JSON.parse(body.concurrency_rule);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "Invalid concurrency_rule JSON"));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (body.start_hour >= body.end_hour) {
|
|
127
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "start_hour must be < end_hour"));
|
|
128
|
+
}
|
|
129
|
+
const weekDays = parseWeekSafe(body.week);
|
|
130
|
+
if (!weekDays) {
|
|
131
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "week must be array of 0-6"));
|
|
132
|
+
}
|
|
133
|
+
const overlapErr = checkOverlap(db, body.mapping_group_id, undefined, weekDays, body.start_hour, body.end_hour);
|
|
134
|
+
if (overlapErr) {
|
|
135
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, overlapErr));
|
|
136
|
+
}
|
|
137
|
+
const id = createSchedule(db, {
|
|
138
|
+
mapping_group_id: body.mapping_group_id,
|
|
139
|
+
name: body.name,
|
|
140
|
+
week: body.week,
|
|
141
|
+
start_hour: body.start_hour,
|
|
142
|
+
end_hour: body.end_hour,
|
|
143
|
+
mapping_rule: body.mapping_rule,
|
|
144
|
+
concurrency_rule: body.concurrency_rule,
|
|
145
|
+
});
|
|
146
|
+
return reply.code(HTTP_CREATED).send({ id });
|
|
147
|
+
});
|
|
148
|
+
app.put("/admin/api/schedules/:id", { schema: { body: UpdateScheduleSchema } }, async (request, reply) => {
|
|
149
|
+
const { id } = request.params;
|
|
150
|
+
const body = request.body;
|
|
151
|
+
const existing = getScheduleById(db, id);
|
|
152
|
+
if (!existing)
|
|
153
|
+
return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Schedule not found"));
|
|
154
|
+
const mappingRule = body.mapping_rule ?? existing.mapping_rule;
|
|
155
|
+
const ruleErr = validateMappingRule(db, mappingRule);
|
|
156
|
+
if (ruleErr)
|
|
157
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, ruleErr));
|
|
158
|
+
if (body.concurrency_rule) {
|
|
159
|
+
try {
|
|
160
|
+
JSON.parse(body.concurrency_rule);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "Invalid concurrency_rule JSON"));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const startH = body.start_hour ?? existing.start_hour;
|
|
167
|
+
const endH = body.end_hour ?? existing.end_hour;
|
|
168
|
+
if (startH >= endH) {
|
|
169
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "start_hour must be < end_hour"));
|
|
170
|
+
}
|
|
171
|
+
// 时段重叠校验(仅在 week 或 start/end 有变更时)
|
|
172
|
+
if (body.week !== undefined || body.start_hour !== undefined || body.end_hour !== undefined) {
|
|
173
|
+
const weekDays = parseWeekSafe(body.week ?? existing.week);
|
|
174
|
+
if (!weekDays) {
|
|
175
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, "week must be array of 0-6"));
|
|
176
|
+
}
|
|
177
|
+
const overlapErr = checkOverlap(db, existing.mapping_group_id, id, weekDays, startH, endH);
|
|
178
|
+
if (overlapErr) {
|
|
179
|
+
return reply.code(HTTP_BAD_REQUEST).send(apiError(API_CODE.BAD_REQUEST, overlapErr));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const fields = {};
|
|
183
|
+
const UPDATE_FIELDS = ["name", "enabled", "week", "start_hour", "end_hour", "mapping_rule", "concurrency_rule"];
|
|
184
|
+
const bodyObj = body;
|
|
185
|
+
for (const key of UPDATE_FIELDS) {
|
|
186
|
+
if (bodyObj[key] !== undefined)
|
|
187
|
+
fields[key] = bodyObj[key];
|
|
188
|
+
}
|
|
189
|
+
updateSchedule(db, id, fields);
|
|
190
|
+
return reply.send({ success: true });
|
|
191
|
+
});
|
|
192
|
+
app.delete("/admin/api/schedules/:id", async (request, reply) => {
|
|
193
|
+
const { id } = request.params;
|
|
194
|
+
const existing = getScheduleById(db, id);
|
|
195
|
+
if (!existing)
|
|
196
|
+
return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Schedule not found"));
|
|
197
|
+
deleteSchedule(db, id);
|
|
198
|
+
return reply.send({ success: true });
|
|
199
|
+
});
|
|
200
|
+
app.post("/admin/api/schedules/:id/toggle", async (request, reply) => {
|
|
201
|
+
const { id } = request.params;
|
|
202
|
+
const existing = getScheduleById(db, id);
|
|
203
|
+
if (!existing)
|
|
204
|
+
return reply.code(HTTP_NOT_FOUND).send(apiError(API_CODE.NOT_FOUND, "Schedule not found"));
|
|
205
|
+
const newEnabled = existing.enabled ? 0 : 1;
|
|
206
|
+
updateSchedule(db, id, { enabled: newEnabled });
|
|
207
|
+
return reply.send({ success: true, enabled: newEnabled });
|
|
208
|
+
});
|
|
209
|
+
done();
|
|
210
|
+
};
|
package/dist/admin/stats.js
CHANGED
|
@@ -10,6 +10,8 @@ const StatsQuerySchema = Type.Object({
|
|
|
10
10
|
start_time: Type.Optional(Type.String()),
|
|
11
11
|
end_time: Type.Optional(Type.String()),
|
|
12
12
|
router_key_id: Type.Optional(Type.String()),
|
|
13
|
+
provider_id: Type.Optional(Type.String()),
|
|
14
|
+
backend_model: Type.Optional(Type.String()),
|
|
13
15
|
});
|
|
14
16
|
export const adminStatsRoutes = (app, options, done) => {
|
|
15
17
|
app.get("/admin/api/stats", { schema: { querystring: StatsQuerySchema } }, async (request, reply) => {
|
|
@@ -21,12 +23,12 @@ export const adminStatsRoutes = (app, options, done) => {
|
|
|
21
23
|
endTime = query.end_time;
|
|
22
24
|
}
|
|
23
25
|
else {
|
|
24
|
-
const range = resolveTimeRange((query.period ?? "weekly"), options.db, query.router_key_id);
|
|
26
|
+
const range = resolveTimeRange((query.period ?? "weekly"), options.db, query.router_key_id, query.provider_id);
|
|
25
27
|
startTime = range.startTime;
|
|
26
28
|
endTime = range.endTime;
|
|
27
29
|
}
|
|
28
|
-
const stats = getStats(options.db, startTime, endTime, query.router_key_id);
|
|
29
|
-
return reply.send(stats);
|
|
30
|
+
const stats = getStats(options.db, startTime, endTime, query.router_key_id, query.provider_id, query.backend_model);
|
|
31
|
+
return reply.send({ ...stats, startTime, endTime });
|
|
30
32
|
});
|
|
31
33
|
done();
|
|
32
34
|
};
|