llm-simple-router 0.3.7 → 0.4.1
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 +144 -75
- package/dist/admin/constants.d.ts +1 -8
- package/dist/admin/constants.js +2 -8
- package/dist/admin/logs.js +25 -4
- package/dist/admin/metrics.js +7 -3
- package/dist/admin/recommended.d.ts +7 -0
- package/dist/admin/recommended.js +25 -0
- package/dist/admin/router-keys.js +1 -2
- package/dist/admin/routes.js +4 -0
- package/dist/admin/usage.d.ts +7 -0
- package/dist/admin/usage.js +66 -0
- package/dist/cli.js +0 -0
- package/dist/config/recommended.d.ts +24 -0
- package/dist/config/recommended.js +30 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.js +9 -0
- package/dist/db/index.d.ts +7 -5
- package/dist/db/index.js +4 -3
- package/dist/db/logs.d.ts +24 -33
- package/dist/db/logs.js +52 -17
- package/dist/db/metrics.d.ts +36 -3
- package/dist/db/metrics.js +57 -42
- package/dist/db/migrations/018_add_failover_field.sql +2 -0
- package/dist/db/migrations/019_create_usage_windows.sql +11 -0
- package/dist/db/retry-rules.d.ts +0 -5
- package/dist/db/retry-rules.js +0 -23
- package/dist/db/usage-windows.d.ts +19 -0
- package/dist/db/usage-windows.js +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +11 -8
- package/dist/monitor/request-tracker.d.ts +6 -0
- package/dist/monitor/request-tracker.js +23 -54
- package/dist/monitor/stream-extractor.d.ts +11 -0
- package/dist/monitor/stream-extractor.js +51 -0
- package/dist/proxy/anthropic.js +19 -32
- package/dist/proxy/log-helpers.d.ts +11 -4
- package/dist/proxy/log-helpers.js +5 -3
- package/dist/proxy/openai.js +18 -34
- package/dist/proxy/orchestrator.d.ts +52 -0
- package/dist/proxy/orchestrator.js +100 -0
- package/dist/proxy/proxy-core.d.ts +14 -26
- package/dist/proxy/proxy-core.js +40 -337
- package/dist/proxy/proxy-handler.d.ts +18 -0
- package/dist/proxy/proxy-handler.js +223 -0
- package/dist/proxy/proxy-logging.d.ts +28 -0
- package/dist/proxy/proxy-logging.js +122 -0
- package/dist/proxy/resilience.d.ts +63 -0
- package/dist/proxy/resilience.js +188 -0
- package/dist/proxy/scope.d.ts +18 -0
- package/dist/proxy/scope.js +37 -0
- package/dist/proxy/semaphore.d.ts +9 -2
- package/dist/proxy/semaphore.js +34 -7
- package/dist/proxy/stream-proxy.d.ts +7 -0
- package/dist/proxy/stream-proxy.js +263 -0
- package/dist/proxy/{upstream-call.d.ts → transport.d.ts} +25 -18
- package/dist/proxy/transport.js +128 -0
- package/dist/proxy/types.d.ts +58 -0
- package/dist/proxy/types.js +30 -0
- package/dist/proxy/usage-window-tracker.d.ts +11 -0
- package/dist/proxy/usage-window-tracker.js +75 -0
- package/dist/utils/datetime.d.ts +4 -0
- package/dist/utils/datetime.js +10 -0
- package/frontend-dist/assets/CardContent-fmM_iiuR.js +1 -0
- package/frontend-dist/assets/CardHeader-BzzFzZ1B.js +1 -0
- package/frontend-dist/assets/CardTitle-09d7O-11.js +1 -0
- package/frontend-dist/assets/Checkbox-DH8iqXQd.js +1 -0
- package/frontend-dist/assets/CollapsibleTrigger-DCRRORrU.js +1 -0
- package/frontend-dist/assets/Collection-DY9-Yue9.js +3 -0
- package/frontend-dist/assets/Dashboard-BEzoZuSm.js +3 -0
- package/frontend-dist/assets/DialogTitle-BeMGJzYO.js +1 -0
- package/frontend-dist/assets/Input-BhvZ-Up7.js +1 -0
- package/frontend-dist/assets/Label-DjtouWZ7.js +1 -0
- package/frontend-dist/assets/LogDetailDialog-BjRsy_FR.js +3 -0
- package/frontend-dist/assets/Login-hOCPB-34.js +1 -0
- package/frontend-dist/assets/Logs-C5c3BJsg.js +1 -0
- package/frontend-dist/assets/ModelMappings-CDjxwyyz.js +1 -0
- package/frontend-dist/assets/Monitor-CPAvIREG.js +1 -0
- package/frontend-dist/assets/PopperContent-CHNw_qb6.js +1 -0
- package/frontend-dist/assets/Providers-C9ZAqHxO.js +1 -0
- package/frontend-dist/assets/ProxyEnhancement-Ct5WbiB7.js +5 -0
- package/frontend-dist/assets/RetryRules-CbgyrP6w.js +1 -0
- package/frontend-dist/assets/RouterKeys-zmqgFEKp.js +1 -0
- package/frontend-dist/assets/SelectValue-CP4Sh7LP.js +1 -0
- package/frontend-dist/assets/Setup-BXDEPt4o.js +1 -0
- package/frontend-dist/assets/Switch-DF6awXqs.js +1 -0
- package/frontend-dist/assets/TableHeader-BKE_yVML.js +1 -0
- package/frontend-dist/assets/TabsTrigger-D8R7lxaI.js +1 -0
- package/frontend-dist/assets/TooltipTrigger-BjQXeFem.js +1 -0
- package/frontend-dist/assets/VisuallyHidden-B_NnkONE.js +1 -0
- package/frontend-dist/assets/VisuallyHiddenInput-cjeTgyDe.js +1 -0
- package/frontend-dist/assets/alert-dialog-BoGRIC1Q.js +1 -0
- package/frontend-dist/assets/badge-DIO8W_W9.js +1 -0
- package/frontend-dist/assets/button-qxGNBunr.js +12 -0
- package/frontend-dist/assets/{createLucideIcon-CCmQ9QKM.js → createLucideIcon-jHUFhqKn.js} +1 -1
- package/frontend-dist/assets/dialog-D8pIXeSs.js +1 -0
- package/frontend-dist/assets/format-CPdJtjZ5.js +1 -0
- package/frontend-dist/assets/index-C_disqMY.js +1 -0
- package/frontend-dist/assets/index-DDp6SHfg.css +1 -0
- package/frontend-dist/assets/lib-DjpgwSRA.js +1 -0
- package/frontend-dist/assets/{ohash.D__AXeF1-p4vp6Svt.js → ohash.D__AXeF1-nmJ7gFbh.js} +1 -1
- package/frontend-dist/assets/{useClipboard-DO-38TXr.js → useClipboard-CmLp2YGk.js} +1 -1
- package/frontend-dist/assets/useForwardExpose-awoGXQkg.js +1 -0
- package/frontend-dist/assets/useNonce-_2e-GL-A.js +1 -0
- package/frontend-dist/assets/x-B0G-wIAB.js +1 -0
- package/frontend-dist/index.html +7 -7
- package/package.json +1 -1
- package/dist/admin/services.d.ts +0 -7
- package/dist/admin/services.js +0 -63
- package/dist/proxy/retry.d.ts +0 -43
- package/dist/proxy/retry.js +0 -121
- package/dist/proxy/upstream-call.js +0 -208
- package/frontend-dist/assets/CardContent-CucI6u41.js +0 -1
- package/frontend-dist/assets/CardHeader-d-DYsWxe.js +0 -1
- package/frontend-dist/assets/CardTitle-CIDEQkWB.js +0 -1
- package/frontend-dist/assets/Checkbox-CybCw3zS.js +0 -1
- package/frontend-dist/assets/CollapsibleTrigger-BFNhb19_.js +0 -1
- package/frontend-dist/assets/Collection-DUBb4r6h.js +0 -3
- package/frontend-dist/assets/Dashboard-DLB6iqH1.js +0 -3
- package/frontend-dist/assets/DialogTitle-Dq-5o7nJ.js +0 -1
- package/frontend-dist/assets/Input-HN3Il0-c.js +0 -1
- package/frontend-dist/assets/Label-CXAeFn-r.js +0 -1
- package/frontend-dist/assets/LogResponseViewer-CyBzv02a.js +0 -3
- package/frontend-dist/assets/Login-Br3qsdxf.js +0 -1
- package/frontend-dist/assets/Logs-Cu_IftdS.js +0 -1
- package/frontend-dist/assets/ModelMappings-DXC0sNH5.js +0 -1
- package/frontend-dist/assets/Monitor-CKlid1sC.js +0 -1
- package/frontend-dist/assets/PopperContent-CnZejY31.js +0 -1
- package/frontend-dist/assets/Providers-8CHhW4uH.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-CkYeXwgH.js +0 -5
- package/frontend-dist/assets/RetryRules-Csb7u9W4.js +0 -1
- package/frontend-dist/assets/RouterKeys-C6YIufmj.js +0 -1
- package/frontend-dist/assets/RovingFocusItem-B7ZIkplZ.js +0 -1
- package/frontend-dist/assets/SelectValue-B32pgmTJ.js +0 -1
- package/frontend-dist/assets/Setup-Df9IQo2x.js +0 -1
- package/frontend-dist/assets/Switch-CLeo7H6d.js +0 -1
- package/frontend-dist/assets/TableHeader-BpscAtT3.js +0 -1
- package/frontend-dist/assets/TabsTrigger-DErAbTuM.js +0 -1
- package/frontend-dist/assets/VisuallyHidden-CJBR3YB3.js +0 -1
- package/frontend-dist/assets/VisuallyHiddenInput-Cy0VuE1l.js +0 -1
- package/frontend-dist/assets/alert-dialog-BAR1JRmT.js +0 -1
- package/frontend-dist/assets/button-D54q76GQ.js +0 -1
- package/frontend-dist/assets/client-Mb8fy_bC.js +0 -12
- package/frontend-dist/assets/dialog-DSH5k5Kj.js +0 -1
- package/frontend-dist/assets/index-BQBtSfem.js +0 -1
- package/frontend-dist/assets/index-H-lnTkMr.css +0 -1
- package/frontend-dist/assets/lib-BgOqOzXI.js +0 -1
- package/frontend-dist/assets/useForwardExpose-CzQFheaD.js +0 -1
- package/frontend-dist/assets/useNonce-CU-NirfM.js +0 -1
- package/frontend-dist/assets/x-DEJ1xpi5.js +0 -1
package/dist/db/index.js
CHANGED
|
@@ -38,10 +38,11 @@ export function initDatabase(dbPath) {
|
|
|
38
38
|
// --- Re-export from per-table modules ---
|
|
39
39
|
export { getActiveProviders, getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
|
|
40
40
|
export { getModelMapping, getAllModelMappings, createModelMapping, updateModelMapping, deleteModelMapping, getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getActiveProviderModels, resolveByProviderModel, } from "./mappings.js";
|
|
41
|
-
export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule,
|
|
42
|
-
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore,
|
|
41
|
+
export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
|
|
42
|
+
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, } from "./logs.js";
|
|
43
43
|
export { getRouterKeyByHash, getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "./router-keys.js";
|
|
44
|
-
export { getMetricsSummary, getMetricsTimeseries } from "./metrics.js";
|
|
44
|
+
export { getMetricsSummary, getMetricsTimeseries, insertMetrics } from "./metrics.js";
|
|
45
45
|
export { getStats } from "./stats.js";
|
|
46
46
|
export { getSetting, setSetting, isInitialized } from "./settings.js";
|
|
47
47
|
export { getSessionStates, getSessionState, getSessionHistory, upsertSessionState, insertSessionHistory, deleteSessionState, } from "./session-states.js";
|
|
48
|
+
export { insertWindow, getLatestWindow, getWindowsInRange, getWindowUsage, } from "./usage-windows.js";
|
package/dist/db/logs.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface RequestLog {
|
|
|
16
16
|
upstream_response: string | null;
|
|
17
17
|
client_response: string | null;
|
|
18
18
|
is_retry: number;
|
|
19
|
+
is_failover: number;
|
|
19
20
|
original_request_id: string | null;
|
|
20
21
|
original_model: string | null;
|
|
21
22
|
}
|
|
@@ -24,38 +25,6 @@ export interface RequestLogListRow extends RequestLog {
|
|
|
24
25
|
backend_model: string | null;
|
|
25
26
|
provider_name: string | null;
|
|
26
27
|
}
|
|
27
|
-
export interface MetricsRow {
|
|
28
|
-
id: string;
|
|
29
|
-
request_log_id: string;
|
|
30
|
-
provider_id: string;
|
|
31
|
-
backend_model: string;
|
|
32
|
-
api_type: string;
|
|
33
|
-
input_tokens: number | null;
|
|
34
|
-
output_tokens: number | null;
|
|
35
|
-
cache_creation_tokens: number | null;
|
|
36
|
-
cache_read_tokens: number | null;
|
|
37
|
-
ttft_ms: number | null;
|
|
38
|
-
total_duration_ms: number | null;
|
|
39
|
-
tokens_per_second: number | null;
|
|
40
|
-
stop_reason: string | null;
|
|
41
|
-
is_complete: number;
|
|
42
|
-
created_at: string;
|
|
43
|
-
}
|
|
44
|
-
export type MetricsInsert = {
|
|
45
|
-
request_log_id: string;
|
|
46
|
-
provider_id: string;
|
|
47
|
-
backend_model: string;
|
|
48
|
-
api_type: string;
|
|
49
|
-
input_tokens?: number | null;
|
|
50
|
-
output_tokens?: number | null;
|
|
51
|
-
cache_creation_tokens?: number | null;
|
|
52
|
-
cache_read_tokens?: number | null;
|
|
53
|
-
ttft_ms?: number | null;
|
|
54
|
-
total_duration_ms?: number | null;
|
|
55
|
-
tokens_per_second?: number | null;
|
|
56
|
-
stop_reason?: string | null;
|
|
57
|
-
is_complete?: number;
|
|
58
|
-
};
|
|
59
28
|
export interface RequestLogInsert {
|
|
60
29
|
id: string;
|
|
61
30
|
api_type: string;
|
|
@@ -73,6 +42,7 @@ export interface RequestLogInsert {
|
|
|
73
42
|
upstream_response?: string | null;
|
|
74
43
|
client_response?: string | null;
|
|
75
44
|
is_retry?: number;
|
|
45
|
+
is_failover?: number;
|
|
76
46
|
original_request_id?: string | null;
|
|
77
47
|
router_key_id?: string | null;
|
|
78
48
|
original_model?: string | null;
|
|
@@ -84,10 +54,31 @@ export declare function getRequestLogs(db: Database.Database, options: {
|
|
|
84
54
|
api_type?: string;
|
|
85
55
|
model?: string;
|
|
86
56
|
router_key_id?: string;
|
|
57
|
+
provider_id?: string;
|
|
58
|
+
start_time?: string;
|
|
59
|
+
end_time?: string;
|
|
87
60
|
}): {
|
|
88
61
|
data: RequestLogListRow[];
|
|
89
62
|
total: number;
|
|
90
63
|
};
|
|
91
64
|
export declare function getRequestLogById(db: Database.Database, id: string): RequestLog | undefined;
|
|
92
65
|
export declare function deleteLogsBefore(db: Database.Database, beforeDate: string): number;
|
|
93
|
-
|
|
66
|
+
/** 查询某条日志的子请求(retry/failover 关联),上限 100 条 */
|
|
67
|
+
export declare function getRequestLogChildren(db: Database.Database, parentId: string): RequestLogListRow[];
|
|
68
|
+
export interface RequestLogGroupedRow extends RequestLogListRow {
|
|
69
|
+
child_count: number;
|
|
70
|
+
}
|
|
71
|
+
/** 只返回根请求(original_request_id IS NULL),附带子请求数量 */
|
|
72
|
+
export declare function getRequestLogsGrouped(db: Database.Database, options: {
|
|
73
|
+
page: number;
|
|
74
|
+
limit: number;
|
|
75
|
+
api_type?: string;
|
|
76
|
+
model?: string;
|
|
77
|
+
router_key_id?: string;
|
|
78
|
+
provider_id?: string;
|
|
79
|
+
start_time?: string;
|
|
80
|
+
end_time?: string;
|
|
81
|
+
}): {
|
|
82
|
+
data: RequestLogGroupedRow[];
|
|
83
|
+
total: number;
|
|
84
|
+
};
|
package/dist/db/logs.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
// --- request_logs ---
|
|
2
|
+
/** 三处日志列表查询共享的 SELECT 列 + JOIN 子句 */
|
|
3
|
+
const LOG_LIST_SELECT = `rl.id, rl.api_type, rl.model, rl.provider_id, rl.status_code, rl.latency_ms,
|
|
4
|
+
rl.is_stream, rl.error_message, rl.created_at, rl.is_retry, rl.is_failover, rl.original_request_id, rl.original_model,
|
|
5
|
+
CASE WHEN rl.provider_id = 'router' THEN rl.upstream_request ELSE NULL END AS upstream_request,
|
|
6
|
+
rm.backend_model, COALESCE(p.name, rl.provider_id) AS provider_name`;
|
|
7
|
+
const LOG_LIST_JOIN = `LEFT JOIN request_metrics rm ON rm.request_log_id = rl.id
|
|
8
|
+
LEFT JOIN providers p ON p.id = rl.provider_id`;
|
|
2
9
|
export function insertRequestLog(db, log) {
|
|
3
|
-
db.prepare(`INSERT INTO request_logs (id, api_type, model, provider_id, status_code, latency_ms, is_stream, error_message, created_at, request_body, response_body, client_request, upstream_request, upstream_response, client_response, is_retry, original_request_id, router_key_id, original_model)
|
|
4
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(log.id, log.api_type, log.model, log.provider_id, log.status_code, log.latency_ms, log.is_stream, log.error_message, log.created_at, log.request_body ?? null, log.response_body ?? null, log.client_request ?? null, log.upstream_request ?? null, log.upstream_response ?? null, log.client_response ?? null, log.is_retry ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null);
|
|
10
|
+
db.prepare(`INSERT INTO request_logs (id, api_type, model, provider_id, status_code, latency_ms, is_stream, error_message, created_at, request_body, response_body, client_request, upstream_request, upstream_response, client_response, is_retry, is_failover, original_request_id, router_key_id, original_model)
|
|
11
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(log.id, log.api_type, log.model, log.provider_id, log.status_code, log.latency_ms, log.is_stream, log.error_message, log.created_at, log.request_body ?? null, log.response_body ?? null, log.client_request ?? null, log.upstream_request ?? null, log.upstream_response ?? null, log.client_response ?? null, log.is_retry ?? 0, log.is_failover ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null);
|
|
5
12
|
}
|
|
6
|
-
|
|
7
|
-
let where =
|
|
13
|
+
function buildLogWhereClause(options, baseCondition) {
|
|
14
|
+
let where = baseCondition;
|
|
8
15
|
const params = [];
|
|
9
16
|
if (options.api_type) {
|
|
10
17
|
where += " AND rl.api_type = ?";
|
|
@@ -18,16 +25,28 @@ export function getRequestLogs(db, options) {
|
|
|
18
25
|
where += " AND rl.router_key_id = ?";
|
|
19
26
|
params.push(options.router_key_id);
|
|
20
27
|
}
|
|
28
|
+
if (options.provider_id) {
|
|
29
|
+
where += " AND rl.provider_id = ?";
|
|
30
|
+
params.push(options.provider_id);
|
|
31
|
+
}
|
|
32
|
+
if (options.start_time) {
|
|
33
|
+
where += " AND rl.created_at >= ?";
|
|
34
|
+
params.push(options.start_time);
|
|
35
|
+
}
|
|
36
|
+
if (options.end_time) {
|
|
37
|
+
where += " AND rl.created_at <= ?";
|
|
38
|
+
params.push(options.end_time);
|
|
39
|
+
}
|
|
40
|
+
return { where, params };
|
|
41
|
+
}
|
|
42
|
+
export function getRequestLogs(db, options) {
|
|
43
|
+
const { where, params } = buildLogWhereClause(options, "1=1");
|
|
21
44
|
const total = db.prepare(`SELECT COUNT(*) as count FROM request_logs rl WHERE ${where}`).get(...params).count;
|
|
22
45
|
const offset = (options.page - 1) * options.limit;
|
|
23
46
|
const data = db
|
|
24
|
-
.prepare(`SELECT
|
|
25
|
-
rl.is_stream, rl.error_message, rl.created_at, rl.is_retry, rl.original_request_id, rl.original_model,
|
|
26
|
-
CASE WHEN rl.provider_id = 'router' THEN rl.upstream_request ELSE NULL END AS upstream_request,
|
|
27
|
-
rm.backend_model, COALESCE(p.name, rl.provider_id) AS provider_name
|
|
47
|
+
.prepare(`SELECT ${LOG_LIST_SELECT}
|
|
28
48
|
FROM request_logs rl
|
|
29
|
-
|
|
30
|
-
LEFT JOIN providers p ON p.id = rl.provider_id
|
|
49
|
+
${LOG_LIST_JOIN}
|
|
31
50
|
WHERE ${where} ORDER BY rl.created_at DESC LIMIT ? OFFSET ?`)
|
|
32
51
|
.all(...params, options.limit, offset);
|
|
33
52
|
return { data, total };
|
|
@@ -38,10 +57,26 @@ export function getRequestLogById(db, id) {
|
|
|
38
57
|
export function deleteLogsBefore(db, beforeDate) {
|
|
39
58
|
return db.prepare("DELETE FROM request_logs WHERE created_at < ?").run(beforeDate).changes;
|
|
40
59
|
}
|
|
41
|
-
|
|
42
|
-
export function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
/** 查询某条日志的子请求(retry/failover 关联),上限 100 条 */
|
|
61
|
+
export function getRequestLogChildren(db, parentId) {
|
|
62
|
+
return db.prepare(`SELECT ${LOG_LIST_SELECT}
|
|
63
|
+
FROM request_logs rl
|
|
64
|
+
${LOG_LIST_JOIN}
|
|
65
|
+
WHERE rl.original_request_id = ?
|
|
66
|
+
ORDER BY rl.created_at ASC
|
|
67
|
+
LIMIT 100`).all(parentId);
|
|
68
|
+
}
|
|
69
|
+
/** 只返回根请求(original_request_id IS NULL),附带子请求数量 */
|
|
70
|
+
export function getRequestLogsGrouped(db, options) {
|
|
71
|
+
const { where, params } = buildLogWhereClause(options, "rl.original_request_id IS NULL");
|
|
72
|
+
const total = db.prepare(`SELECT COUNT(*) as count FROM request_logs rl WHERE ${where}`).get(...params).count;
|
|
73
|
+
const offset = (options.page - 1) * options.limit;
|
|
74
|
+
const data = db
|
|
75
|
+
.prepare(`SELECT ${LOG_LIST_SELECT},
|
|
76
|
+
(SELECT COUNT(*) FROM request_logs c WHERE c.original_request_id = rl.id) AS child_count
|
|
77
|
+
FROM request_logs rl
|
|
78
|
+
${LOG_LIST_JOIN}
|
|
79
|
+
WHERE ${where} ORDER BY rl.created_at DESC LIMIT ? OFFSET ?`)
|
|
80
|
+
.all(...params, options.limit, offset);
|
|
81
|
+
return { data, total };
|
|
47
82
|
}
|
package/dist/db/metrics.d.ts
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
|
-
export type MetricsPeriod = "1h" | "6h" | "24h" | "7d" | "30d";
|
|
2
|
+
export type MetricsPeriod = "1h" | "5h" | "6h" | "24h" | "7d" | "30d";
|
|
3
3
|
export type MetricsMetric = "ttft" | "tps" | "tokens" | "cache_rate" | "request_count" | "input_tokens" | "output_tokens" | "cache_hit_tokens";
|
|
4
|
+
export interface MetricsRow {
|
|
5
|
+
id: string;
|
|
6
|
+
request_log_id: string;
|
|
7
|
+
provider_id: string;
|
|
8
|
+
backend_model: string;
|
|
9
|
+
api_type: string;
|
|
10
|
+
input_tokens: number | null;
|
|
11
|
+
output_tokens: number | null;
|
|
12
|
+
cache_creation_tokens: number | null;
|
|
13
|
+
cache_read_tokens: number | null;
|
|
14
|
+
ttft_ms: number | null;
|
|
15
|
+
total_duration_ms: number | null;
|
|
16
|
+
tokens_per_second: number | null;
|
|
17
|
+
stop_reason: string | null;
|
|
18
|
+
is_complete: number;
|
|
19
|
+
created_at: string;
|
|
20
|
+
}
|
|
21
|
+
export type MetricsInsert = {
|
|
22
|
+
request_log_id: string;
|
|
23
|
+
provider_id: string;
|
|
24
|
+
backend_model: string;
|
|
25
|
+
api_type: string;
|
|
26
|
+
input_tokens?: number | null;
|
|
27
|
+
output_tokens?: number | null;
|
|
28
|
+
cache_creation_tokens?: number | null;
|
|
29
|
+
cache_read_tokens?: number | null;
|
|
30
|
+
ttft_ms?: number | null;
|
|
31
|
+
total_duration_ms?: number | null;
|
|
32
|
+
tokens_per_second?: number | null;
|
|
33
|
+
stop_reason?: string | null;
|
|
34
|
+
is_complete?: number;
|
|
35
|
+
};
|
|
36
|
+
export declare function insertMetrics(db: Database.Database, m: MetricsInsert): string;
|
|
4
37
|
export interface MetricsSummaryRow {
|
|
5
38
|
provider_id: string;
|
|
6
39
|
provider_name: string;
|
|
@@ -15,10 +48,10 @@ export interface MetricsSummaryRow {
|
|
|
15
48
|
total_cache_hit_tokens: number;
|
|
16
49
|
cache_hit_rate: number | null;
|
|
17
50
|
}
|
|
18
|
-
export declare function getMetricsSummary(db: Database.Database, period: MetricsPeriod, providerId?: string, backendModel?: string, routerKeyId?: string): MetricsSummaryRow[];
|
|
51
|
+
export declare function getMetricsSummary(db: Database.Database, period: MetricsPeriod, providerId?: string, backendModel?: string, routerKeyId?: string, startTime?: string, endTime?: string): MetricsSummaryRow[];
|
|
19
52
|
export interface MetricsTimeseriesRow {
|
|
20
53
|
time_bucket: string;
|
|
21
54
|
avg_value: number | null;
|
|
22
55
|
count: number;
|
|
23
56
|
}
|
|
24
|
-
export declare function getMetricsTimeseries(db: Database.Database, period: MetricsPeriod, metric: MetricsMetric, providerId?: string, backendModel?: string, routerKeyId?: string): MetricsTimeseriesRow[];
|
|
57
|
+
export declare function getMetricsTimeseries(db: Database.Database, period: MetricsPeriod, metric: MetricsMetric, providerId?: string, backendModel?: string, routerKeyId?: string, startTime?: string, endTime?: string): MetricsTimeseriesRow[];
|
package/dist/db/metrics.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
export function insertMetrics(db, m) {
|
|
3
|
+
const id = randomUUID();
|
|
4
|
+
db.prepare(`INSERT INTO request_metrics (id, request_log_id, provider_id, backend_model, api_type, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, ttft_ms, total_duration_ms, tokens_per_second, stop_reason, is_complete)
|
|
5
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, m.request_log_id, m.provider_id, m.backend_model, m.api_type, m.input_tokens ?? null, m.output_tokens ?? null, m.cache_creation_tokens ?? null, m.cache_read_tokens ?? null, m.ttft_ms ?? null, m.total_duration_ms ?? null, m.tokens_per_second ?? null, m.stop_reason ?? null, m.is_complete ?? 1);
|
|
6
|
+
return id;
|
|
7
|
+
}
|
|
1
8
|
const PERIOD_OFFSET = {
|
|
2
9
|
"1h": "-1 hours",
|
|
10
|
+
"5h": "-5 hours",
|
|
3
11
|
"6h": "-6 hours",
|
|
4
12
|
"24h": "-1 day",
|
|
5
13
|
"7d": "-7 days",
|
|
@@ -7,6 +15,7 @@ const PERIOD_OFFSET = {
|
|
|
7
15
|
};
|
|
8
16
|
const BUCKET_SECONDS = {
|
|
9
17
|
"1h": 60,
|
|
18
|
+
"5h": 300,
|
|
10
19
|
"6h": 300,
|
|
11
20
|
"24h": 900,
|
|
12
21
|
"7d": 3600,
|
|
@@ -14,10 +23,39 @@ const BUCKET_SECONDS = {
|
|
|
14
23
|
};
|
|
15
24
|
// unix epoch 秒转毫秒的乘数
|
|
16
25
|
const MS_PER_SEC = 1000;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
// 时间跨度(秒)→ 桶大小(秒)的阶梯映射,与 BUCKET_SECONDS 保持对齐
|
|
27
|
+
const BUCKET_THRESHOLDS = [
|
|
28
|
+
{ maxSec: 3600, bucketSec: 60 }, // ≤1h: 1min
|
|
29
|
+
{ maxSec: 21600, bucketSec: 300 }, // ≤6h: 5min
|
|
30
|
+
{ maxSec: 86400, bucketSec: 900 }, // ≤1d: 15min
|
|
31
|
+
{ maxSec: 604800, bucketSec: 3600 }, // ≤7d: 1h
|
|
32
|
+
];
|
|
33
|
+
const FALLBACK_BUCKET_SEC = 14400; // >7d: 4h
|
|
34
|
+
function calculateBucketSeconds(startTime, endTime) {
|
|
35
|
+
const ms = new Date(endTime).getTime() - new Date(startTime).getTime();
|
|
36
|
+
const sec = ms / MS_PER_SEC;
|
|
37
|
+
const match = BUCKET_THRESHOLDS.find((t) => sec <= t.maxSec);
|
|
38
|
+
return match ? match.bucketSec : FALLBACK_BUCKET_SEC;
|
|
39
|
+
}
|
|
40
|
+
function buildTimeCondition(period, startTime, endTime) {
|
|
41
|
+
if (startTime && endTime) {
|
|
42
|
+
// request_metrics.created_at 用 datetime('now') 格式 (YYYY-MM-DD HH:MM:SS),
|
|
43
|
+
// 前端传入 ISO 8601,需要转换格式以匹配字符串比较
|
|
44
|
+
return {
|
|
45
|
+
timeWhere: "rm.created_at >= datetime(?) AND rm.created_at < datetime(?)",
|
|
46
|
+
timeParams: [startTime, endTime],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
timeWhere: "rm.created_at >= datetime('now', ?)",
|
|
51
|
+
timeParams: [PERIOD_OFFSET[period]],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function getMetricsSummary(db, period, providerId, backendModel, routerKeyId, startTime, endTime) {
|
|
55
|
+
const { timeWhere, timeParams } = buildTimeCondition(period, startTime, endTime);
|
|
56
|
+
const conditions = ["rm.is_complete = 1", timeWhere];
|
|
57
|
+
const params = [...timeParams];
|
|
58
|
+
const joins = ["LEFT JOIN providers p ON p.id = rm.provider_id"];
|
|
21
59
|
if (providerId) {
|
|
22
60
|
conditions.push("rm.provider_id = ?");
|
|
23
61
|
params.push(providerId);
|
|
@@ -26,48 +64,23 @@ export function getMetricsSummary(db, period, providerId, backendModel, routerKe
|
|
|
26
64
|
conditions.push("rm.backend_model = ?");
|
|
27
65
|
params.push(backendModel);
|
|
28
66
|
}
|
|
29
|
-
const joins = "LEFT JOIN providers p ON p.id = rm.provider_id";
|
|
30
67
|
if (routerKeyId) {
|
|
68
|
+
joins.push("LEFT JOIN request_logs rl ON rl.id = rm.request_log_id");
|
|
31
69
|
conditions.push("rl.router_key_id = ?");
|
|
32
70
|
params.push(routerKeyId);
|
|
33
|
-
return db.prepare(`
|
|
34
|
-
SELECT
|
|
35
|
-
rm.provider_id, COALESCE(p.name, rm.provider_id) AS provider_name, rm.backend_model,
|
|
36
|
-
COUNT(*) AS request_count, AVG(rm.ttft_ms) AS avg_ttft_ms, NULL AS p50_ttft_ms, NULL AS p95_ttft_ms,
|
|
37
|
-
AVG(rm.tokens_per_second) AS avg_tps,
|
|
38
|
-
COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens, COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens,
|
|
39
|
-
COALESCE(SUM(rm.cache_read_tokens), 0) AS total_cache_hit_tokens,
|
|
40
|
-
CASE WHEN SUM(rm.input_tokens) > 0 THEN SUM(rm.cache_read_tokens) * 1.0 / SUM(rm.input_tokens) ELSE NULL END AS cache_hit_rate
|
|
41
|
-
FROM request_metrics rm
|
|
42
|
-
LEFT JOIN providers p ON p.id = rm.provider_id
|
|
43
|
-
LEFT JOIN request_logs rl ON rl.id = rm.request_log_id
|
|
44
|
-
WHERE ${conditions.join(" AND ")}
|
|
45
|
-
GROUP BY rm.provider_id, rm.backend_model ORDER BY request_count DESC
|
|
46
|
-
`).all(...params);
|
|
47
71
|
}
|
|
48
|
-
const where = conditions.join(" AND ");
|
|
49
72
|
return db.prepare(`
|
|
50
73
|
SELECT
|
|
51
|
-
rm.provider_id,
|
|
52
|
-
|
|
53
|
-
rm.backend_model,
|
|
54
|
-
COUNT(*) AS request_count,
|
|
55
|
-
AVG(rm.ttft_ms) AS avg_ttft_ms,
|
|
56
|
-
NULL AS p50_ttft_ms,
|
|
57
|
-
NULL AS p95_ttft_ms,
|
|
74
|
+
rm.provider_id, COALESCE(p.name, rm.provider_id) AS provider_name, rm.backend_model,
|
|
75
|
+
COUNT(*) AS request_count, AVG(rm.ttft_ms) AS avg_ttft_ms, NULL AS p50_ttft_ms, NULL AS p95_ttft_ms,
|
|
58
76
|
AVG(rm.tokens_per_second) AS avg_tps,
|
|
59
|
-
COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens,
|
|
60
|
-
COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens,
|
|
77
|
+
COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens, COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens,
|
|
61
78
|
COALESCE(SUM(rm.cache_read_tokens), 0) AS total_cache_hit_tokens,
|
|
62
|
-
CASE WHEN SUM(rm.input_tokens) > 0
|
|
63
|
-
THEN SUM(rm.cache_read_tokens) * 1.0 / SUM(rm.input_tokens)
|
|
64
|
-
ELSE NULL
|
|
65
|
-
END AS cache_hit_rate
|
|
79
|
+
CASE WHEN SUM(rm.input_tokens) > 0 THEN SUM(rm.cache_read_tokens) * 1.0 / SUM(rm.input_tokens) ELSE NULL END AS cache_hit_rate
|
|
66
80
|
FROM request_metrics rm
|
|
67
|
-
${joins}
|
|
68
|
-
WHERE ${
|
|
69
|
-
GROUP BY rm.provider_id, rm.backend_model
|
|
70
|
-
ORDER BY request_count DESC
|
|
81
|
+
${joins.join(" ")}
|
|
82
|
+
WHERE ${conditions.join(" AND ")}
|
|
83
|
+
GROUP BY rm.provider_id, rm.backend_model ORDER BY request_count DESC
|
|
71
84
|
`).all(...params);
|
|
72
85
|
}
|
|
73
86
|
const METRIC_EXPR = {
|
|
@@ -80,11 +93,13 @@ const METRIC_EXPR = {
|
|
|
80
93
|
output_tokens: "SUM(rm.output_tokens)",
|
|
81
94
|
cache_hit_tokens: "SUM(rm.cache_read_tokens)",
|
|
82
95
|
};
|
|
83
|
-
export function getMetricsTimeseries(db, period, metric, providerId, backendModel, routerKeyId) {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
96
|
+
export function getMetricsTimeseries(db, period, metric, providerId, backendModel, routerKeyId, startTime, endTime) {
|
|
97
|
+
const bucketSec = (startTime && endTime)
|
|
98
|
+
? calculateBucketSeconds(startTime, endTime)
|
|
99
|
+
: BUCKET_SECONDS[period];
|
|
100
|
+
const { timeWhere, timeParams } = buildTimeCondition(period, startTime, endTime);
|
|
101
|
+
const conditions = ["rm.is_complete = 1", timeWhere];
|
|
102
|
+
const params = [...timeParams];
|
|
88
103
|
if (providerId) {
|
|
89
104
|
conditions.push("rm.provider_id = ?");
|
|
90
105
|
params.push(providerId);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS usage_windows (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
router_key_id TEXT,
|
|
4
|
+
start_time TEXT NOT NULL,
|
|
5
|
+
end_time TEXT NOT NULL,
|
|
6
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
CREATE INDEX IF NOT EXISTS idx_usage_windows_start ON usage_windows(start_time);
|
|
10
|
+
CREATE INDEX IF NOT EXISTS idx_usage_windows_router_key ON usage_windows(router_key_id);
|
|
11
|
+
CREATE INDEX IF NOT EXISTS idx_usage_windows_end_time ON usage_windows(end_time);
|
package/dist/db/retry-rules.d.ts
CHANGED
|
@@ -25,8 +25,3 @@ export declare function createRetryRule(db: Database.Database, rule: {
|
|
|
25
25
|
}): string;
|
|
26
26
|
export declare function updateRetryRule(db: Database.Database, id: string, fields: Partial<Pick<RetryRule, "name" | "status_code" | "body_pattern" | "is_active" | "retry_strategy" | "retry_delay_ms" | "max_retries" | "max_delay_ms">>): void;
|
|
27
27
|
export declare function deleteRetryRule(db: Database.Database, id: string): void;
|
|
28
|
-
/**
|
|
29
|
-
* 首次启动时(表为空)插入默认重试规则。
|
|
30
|
-
* 429/503 为通用 HTTP 重试;其余为 ZAI middleware 特定 400 响应。
|
|
31
|
-
*/
|
|
32
|
-
export declare function seedDefaultRules(db: Database.Database): void;
|
package/dist/db/retry-rules.js
CHANGED
|
@@ -27,26 +27,3 @@ export function updateRetryRule(db, id, fields) {
|
|
|
27
27
|
export function deleteRetryRule(db, id) {
|
|
28
28
|
deleteById(db, "retry_rules", id);
|
|
29
29
|
}
|
|
30
|
-
// ---------- Default seed rules ----------
|
|
31
|
-
const DEFAULT_RULES = [
|
|
32
|
-
{ name: "429 Too Many Requests", status_code: 429, body_pattern: ".*", is_active: 1, retry_strategy: "exponential", retry_delay_ms: 5000, max_retries: 10, max_delay_ms: 60000 },
|
|
33
|
-
{ name: "503 Service Unavailable", status_code: 503, body_pattern: ".*", is_active: 1, retry_strategy: "exponential", retry_delay_ms: 5000, max_retries: 10, max_delay_ms: 60000 },
|
|
34
|
-
{ name: 'ZAI 网络错误 (code 1234)', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*"code"\\s*:\\s*"1234"', is_active: 1, retry_strategy: "exponential", retry_delay_ms: 5000, max_retries: 10, max_delay_ms: 60000 },
|
|
35
|
-
{ name: 'ZAI 临时不可用', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*请稍后重试', is_active: 1, retry_strategy: "exponential", retry_delay_ms: 5000, max_retries: 10, max_delay_ms: 60000 },
|
|
36
|
-
{ name: 'ZAI 操作失败 (code 500)', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*"code"\\s*:\\s*"500"', is_active: 1, retry_strategy: "exponential", retry_delay_ms: 5000, max_retries: 10, max_delay_ms: 60000 },
|
|
37
|
-
];
|
|
38
|
-
/**
|
|
39
|
-
* 首次启动时(表为空)插入默认重试规则。
|
|
40
|
-
* 429/503 为通用 HTTP 重试;其余为 ZAI middleware 特定 400 响应。
|
|
41
|
-
*/
|
|
42
|
-
export function seedDefaultRules(db) {
|
|
43
|
-
const count = db.prepare("SELECT COUNT(*) as c FROM retry_rules").get().c;
|
|
44
|
-
if (count > 0)
|
|
45
|
-
return;
|
|
46
|
-
const now = new Date().toISOString();
|
|
47
|
-
const insert = db.prepare(`INSERT INTO retry_rules (id, name, status_code, body_pattern, is_active, created_at, retry_strategy, retry_delay_ms, max_retries, max_delay_ms)
|
|
48
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
49
|
-
for (const rule of DEFAULT_RULES) {
|
|
50
|
-
insert.run(randomUUID(), rule.name, rule.status_code, rule.body_pattern, rule.is_active, now, rule.retry_strategy, rule.retry_delay_ms, rule.max_retries, rule.max_delay_ms);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
export interface UsageWindow {
|
|
3
|
+
id: string;
|
|
4
|
+
router_key_id: string | null;
|
|
5
|
+
start_time: string;
|
|
6
|
+
end_time: string;
|
|
7
|
+
created_at: string;
|
|
8
|
+
}
|
|
9
|
+
export interface WindowUsage {
|
|
10
|
+
request_count: number;
|
|
11
|
+
total_input_tokens: number;
|
|
12
|
+
total_output_tokens: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function insertWindow(db: Database.Database, w: Omit<UsageWindow, "created_at">): string;
|
|
15
|
+
export declare function getLatestWindow(db: Database.Database, routerKeyId?: string): UsageWindow | null;
|
|
16
|
+
/** 返回与 [start, end) 区间有重叠的窗口 */
|
|
17
|
+
export declare function getWindowsInRange(db: Database.Database, start: string, end: string, routerKeyId?: string): UsageWindow[];
|
|
18
|
+
/** 聚合指定时间窗口内的请求计数和 token 用量 */
|
|
19
|
+
export declare function getWindowUsage(db: Database.Database, startTime: string, endTime: string, routerKeyId?: string): WindowUsage;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
export function insertWindow(db, w) {
|
|
3
|
+
const id = w.id || randomUUID();
|
|
4
|
+
db.prepare("INSERT INTO usage_windows (id, router_key_id, start_time, end_time) VALUES (?, ?, ?, ?)").run(id, w.router_key_id ?? null, w.start_time, w.end_time);
|
|
5
|
+
return id;
|
|
6
|
+
}
|
|
7
|
+
export function getLatestWindow(db, routerKeyId) {
|
|
8
|
+
const sql = routerKeyId
|
|
9
|
+
? "SELECT * FROM usage_windows WHERE router_key_id = ? ORDER BY start_time DESC LIMIT 1"
|
|
10
|
+
: "SELECT * FROM usage_windows ORDER BY start_time DESC LIMIT 1";
|
|
11
|
+
const params = routerKeyId ? [routerKeyId] : [];
|
|
12
|
+
return db.prepare(sql).get(...params) ?? null;
|
|
13
|
+
}
|
|
14
|
+
/** 返回与 [start, end) 区间有重叠的窗口 */
|
|
15
|
+
export function getWindowsInRange(db, start, end, routerKeyId) {
|
|
16
|
+
if (routerKeyId) {
|
|
17
|
+
return db.prepare("SELECT * FROM usage_windows WHERE start_time < ? AND end_time > ? AND router_key_id = ? ORDER BY start_time ASC").all(end, start, routerKeyId);
|
|
18
|
+
}
|
|
19
|
+
return db.prepare("SELECT * FROM usage_windows WHERE start_time < ? AND end_time > ? ORDER BY start_time ASC").all(end, start);
|
|
20
|
+
}
|
|
21
|
+
/** 聚合指定时间窗口内的请求计数和 token 用量 */
|
|
22
|
+
export function getWindowUsage(db, startTime, endTime, routerKeyId) {
|
|
23
|
+
const baseSql = `
|
|
24
|
+
SELECT
|
|
25
|
+
COUNT(*) AS request_count,
|
|
26
|
+
COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens,
|
|
27
|
+
COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens
|
|
28
|
+
FROM request_metrics rm
|
|
29
|
+
JOIN request_logs rl ON rl.id = rm.request_log_id
|
|
30
|
+
WHERE rm.is_complete = 1
|
|
31
|
+
AND rm.created_at >= datetime(?)
|
|
32
|
+
AND rm.created_at < datetime(?)`;
|
|
33
|
+
if (routerKeyId) {
|
|
34
|
+
return db.prepare(`${baseSql} AND rl.router_key_id = ?`).get(startTime, endTime, routerKeyId);
|
|
35
|
+
}
|
|
36
|
+
return db.prepare(baseSql).get(startTime, endTime);
|
|
37
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { FastifyInstance } from "fastify";
|
|
3
3
|
import { Config } from "./config.js";
|
|
4
|
+
import { UsageWindowTracker } from "./proxy/usage-window-tracker.js";
|
|
4
5
|
import Database from "better-sqlite3";
|
|
5
6
|
export interface AppOptions {
|
|
6
7
|
config?: Config;
|
|
@@ -9,6 +10,7 @@ export interface AppOptions {
|
|
|
9
10
|
export declare function buildApp(options?: AppOptions): Promise<{
|
|
10
11
|
app: FastifyInstance;
|
|
11
12
|
db: Database.Database;
|
|
13
|
+
usageWindowTracker: UsageWindowTracker;
|
|
12
14
|
close: () => Promise<void>;
|
|
13
15
|
}>;
|
|
14
16
|
export declare function main(): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,7 @@ import { existsSync } from "node:fs";
|
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
6
|
import Fastify from "fastify";
|
|
7
7
|
import { insertRequestLog } from "./db/logs.js";
|
|
8
|
-
|
|
9
|
-
const HTTP_INTERNAL_ERROR = 500;
|
|
10
|
-
const HTTP_BAD_REQUEST = 400;
|
|
8
|
+
import { HTTP_NOT_FOUND, HTTP_INTERNAL_ERROR, HTTP_BAD_REQUEST } from "./constants.js";
|
|
11
9
|
const PROVIDER_DEFAULT_QUEUE_TIMEOUT_MS = 5000;
|
|
12
10
|
const PROVIDER_DEFAULT_MAX_QUEUE_SIZE = 100;
|
|
13
11
|
// 代理路由路径 → api_type,用于在全局 hook/errorHandler 中识别代理请求
|
|
@@ -23,7 +21,8 @@ function getProxyApiType(url) {
|
|
|
23
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
22
|
const __dirname = path.dirname(__filename);
|
|
25
23
|
import { getConfig } from "./config.js";
|
|
26
|
-
import { initDatabase,
|
|
24
|
+
import { initDatabase, getAllProviders } from "./db/index.js";
|
|
25
|
+
import { loadRecommendedConfig } from "./config/recommended.js";
|
|
27
26
|
import { authMiddleware } from "./middleware/auth.js";
|
|
28
27
|
import { openaiProxy } from "./proxy/openai.js";
|
|
29
28
|
import { anthropicProxy } from "./proxy/anthropic.js";
|
|
@@ -32,6 +31,7 @@ import { RetryRuleMatcher } from "./proxy/retry-rules.js";
|
|
|
32
31
|
import { ProviderSemaphoreManager } from "./proxy/semaphore.js";
|
|
33
32
|
import { RequestTracker } from "./monitor/request-tracker.js";
|
|
34
33
|
import { modelState } from "./proxy/model-state.js";
|
|
34
|
+
import { UsageWindowTracker } from "./proxy/usage-window-tracker.js";
|
|
35
35
|
import fastifyStatic from "@fastify/static";
|
|
36
36
|
export async function buildApp(options) {
|
|
37
37
|
const config = options?.config ?? getBaseConfig();
|
|
@@ -102,15 +102,17 @@ export async function buildApp(options) {
|
|
|
102
102
|
}
|
|
103
103
|
return reply.code(status).send({ error: { message: fastifyError.message } });
|
|
104
104
|
});
|
|
105
|
-
|
|
106
|
-
seedDefaultRules(db);
|
|
105
|
+
loadRecommendedConfig();
|
|
107
106
|
// 注入 DB 到 modelState 单例,启用会话级持久化
|
|
108
107
|
modelState.init(db);
|
|
109
108
|
const matcher = new RetryRuleMatcher();
|
|
110
109
|
matcher.load(db);
|
|
111
110
|
const semaphoreManager = new ProviderSemaphoreManager();
|
|
112
|
-
const tracker = new RequestTracker({ semaphoreManager });
|
|
111
|
+
const tracker = new RequestTracker({ semaphoreManager, logger: app.log });
|
|
113
112
|
tracker.startPushInterval();
|
|
113
|
+
// 5h 用量窗口追踪器,启动时自动补齐缺失窗口
|
|
114
|
+
const usageWindowTracker = new UsageWindowTracker(db);
|
|
115
|
+
usageWindowTracker.reconcileOnStartup();
|
|
114
116
|
// 从 DB 读取已有 provider 的并发配置,初始化信号量管理器和 tracker
|
|
115
117
|
const allProviders = getAllProviders(db);
|
|
116
118
|
for (const p of allProviders) {
|
|
@@ -162,7 +164,7 @@ export async function buildApp(options) {
|
|
|
162
164
|
!request.url.startsWith("/admin/api")) {
|
|
163
165
|
return reply.sendFile("index.html");
|
|
164
166
|
}
|
|
165
|
-
reply.code(HTTP_NOT_FOUND).send({ error: "Not Found" });
|
|
167
|
+
reply.code(HTTP_NOT_FOUND).send({ error: { message: "Not Found" } });
|
|
166
168
|
});
|
|
167
169
|
}
|
|
168
170
|
else {
|
|
@@ -174,6 +176,7 @@ export async function buildApp(options) {
|
|
|
174
176
|
return {
|
|
175
177
|
app,
|
|
176
178
|
db,
|
|
179
|
+
usageWindowTracker,
|
|
177
180
|
close: async () => {
|
|
178
181
|
tracker.stopPushInterval();
|
|
179
182
|
await app.close();
|
|
@@ -3,10 +3,15 @@ import { StatsAggregator } from "./stats-aggregator.js";
|
|
|
3
3
|
import { RuntimeCollector } from "./runtime-collector.js";
|
|
4
4
|
import type { ProviderSemaphoreManager } from "../proxy/semaphore.js";
|
|
5
5
|
import type { ActiveRequest, ProviderConcurrencySnapshot, RuntimeMetrics, StatsSnapshot } from "./types.js";
|
|
6
|
+
export interface TrackerLogger {
|
|
7
|
+
debug(obj: Record<string, unknown>, msg: string): void;
|
|
8
|
+
warn(obj: Record<string, unknown>, msg: string): void;
|
|
9
|
+
}
|
|
6
10
|
export declare class RequestTracker {
|
|
7
11
|
private activeMap;
|
|
8
12
|
private recentCompleted;
|
|
9
13
|
private clients;
|
|
14
|
+
private logger?;
|
|
10
15
|
private providerConfigCache;
|
|
11
16
|
private pushTimer;
|
|
12
17
|
private tickCount;
|
|
@@ -17,6 +22,7 @@ export declare class RequestTracker {
|
|
|
17
22
|
constructor(deps?: {
|
|
18
23
|
semaphoreManager?: ProviderSemaphoreManager;
|
|
19
24
|
runtimeCollector?: RuntimeCollector;
|
|
25
|
+
logger?: TrackerLogger;
|
|
20
26
|
});
|
|
21
27
|
start(req: ActiveRequest): void;
|
|
22
28
|
update(id: string, patch: Partial<ActiveRequest>): void;
|