llm-simple-router 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -91
- package/dist/admin/logs.js +7 -1
- 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/routes.js +6 -0
- package/dist/admin/settings.d.ts +7 -0
- package/dist/admin/settings.js +16 -0
- package/dist/admin/usage.d.ts +7 -0
- package/dist/admin/usage.js +66 -0
- package/dist/config/recommended.d.ts +24 -0
- package/dist/config/recommended.js +30 -0
- package/dist/db/index.d.ts +4 -2
- package/dist/db/index.js +21 -4
- package/dist/db/log-cleaner.d.ts +10 -0
- package/dist/db/log-cleaner.js +42 -0
- package/dist/db/logs.d.ts +33 -8
- package/dist/db/logs.js +52 -6
- package/dist/db/metrics.d.ts +3 -3
- package/dist/db/metrics.js +50 -42
- package/dist/db/migrations/019_create_usage_windows.sql +11 -0
- package/dist/db/migrations/020_drop_log_redundancy.sql +8 -0
- package/dist/db/migrations/021_merge_metrics_columns.sql +28 -0
- package/dist/db/retry-rules.d.ts +0 -5
- package/dist/db/retry-rules.js +0 -36
- package/dist/db/settings.d.ts +2 -0
- package/dist/db/settings.js +7 -0
- 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 +20 -3
- package/dist/monitor/types.d.ts +3 -0
- package/dist/proxy/log-helpers.d.ts +0 -2
- package/dist/proxy/log-helpers.js +3 -5
- package/dist/proxy/orchestrator.d.ts +2 -0
- package/dist/proxy/orchestrator.js +1 -0
- package/dist/proxy/proxy-handler.js +29 -4
- package/dist/proxy/proxy-logging.d.ts +0 -1
- package/dist/proxy/proxy-logging.js +8 -10
- 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-3ytnac7B.js +1 -0
- package/frontend-dist/assets/CardTitle-BHZE8Rty.js +1 -0
- package/frontend-dist/assets/Checkbox-CMYgDuxw.js +1 -0
- package/frontend-dist/assets/CollapsibleTrigger-DooxvEnx.js +1 -0
- package/frontend-dist/assets/Collection-GDvpW_uY.js +3 -0
- package/frontend-dist/assets/Dashboard-BJslVTg8.js +3 -0
- package/frontend-dist/assets/DialogTitle-lj6NAA5R.js +1 -0
- package/frontend-dist/assets/Input-JApdUstN.js +1 -0
- package/frontend-dist/assets/Label-IbQFgxLe.js +1 -0
- package/frontend-dist/assets/Login-BjuVvrPV.js +1 -0
- package/frontend-dist/assets/Logs-J08HyZWA.js +1 -0
- package/frontend-dist/assets/ModelMappings-DWVmxMy6.js +1 -0
- package/frontend-dist/assets/Monitor-BTEW0evp.js +1 -0
- package/frontend-dist/assets/PopperContent-ZhhkKJo0.js +1 -0
- package/frontend-dist/assets/Providers-BqLSKXuv.js +1 -0
- package/frontend-dist/assets/ProxyEnhancement-TAHOKnxW.js +5 -0
- package/frontend-dist/assets/RetryRules-Cn6KHzgB.js +1 -0
- package/frontend-dist/assets/RouterKeys-CBgWAJ6-.js +1 -0
- package/frontend-dist/assets/SelectValue-DS4Z8y0u.js +1 -0
- package/frontend-dist/assets/Setup-QKmeMDtB.js +1 -0
- package/frontend-dist/assets/Switch-BYebebrY.js +1 -0
- package/frontend-dist/assets/TableHeader-B2A48qgy.js +1 -0
- package/frontend-dist/assets/TabsContent-BcNBY5CB.js +1 -0
- package/frontend-dist/assets/TabsTrigger-8W_mNsGI.js +1 -0
- package/frontend-dist/assets/UnifiedRequestDialog-BmEamR1L.js +3 -0
- package/frontend-dist/assets/UnifiedRequestDialog-Dk3IIDDx.css +1 -0
- package/frontend-dist/assets/VisuallyHidden-DPKPka_x.js +1 -0
- package/frontend-dist/assets/VisuallyHiddenInput-Bnglr6yR.js +1 -0
- package/frontend-dist/assets/alert-dialog-BzyDZnoE.js +1 -0
- package/frontend-dist/assets/badge-BTjuxlp4.js +1 -0
- package/frontend-dist/assets/button-BKJB3nEQ.js +12 -0
- package/frontend-dist/assets/{createLucideIcon-DGZkBjcJ.js → createLucideIcon-igIAnu_Y.js} +1 -1
- package/frontend-dist/assets/dialog-C0B-Xn-S.js +1 -0
- package/frontend-dist/assets/file-text-Ci7Mgh3F.js +1 -0
- package/frontend-dist/assets/index-BrDOp_gc.js +1 -0
- package/frontend-dist/assets/index-DMdVJThL.css +1 -0
- package/frontend-dist/assets/lib-BGW4QyKP.js +1 -0
- package/frontend-dist/assets/{ohash.D__AXeF1-B64hB831.js → ohash.D__AXeF1-CsY_LBk-.js} +1 -1
- package/frontend-dist/assets/{useClipboard-CWc1cTDo.js → useClipboard-wnGQAe3I.js} +1 -1
- package/frontend-dist/assets/useForwardExpose-bqtcPo63.js +1 -0
- package/frontend-dist/assets/useNonce-DN0Hrw3l.js +1 -0
- package/frontend-dist/assets/x-Cy_v5hrA.js +1 -0
- package/frontend-dist/index.html +8 -7
- package/package.json +1 -1
- package/frontend-dist/assets/CardContent-CTnwqTdL.js +0 -1
- package/frontend-dist/assets/CardHeader-CfUeY7tk.js +0 -1
- package/frontend-dist/assets/CardTitle-CWiDwWqd.js +0 -1
- package/frontend-dist/assets/Checkbox-BxNz70R_.js +0 -1
- package/frontend-dist/assets/CollapsibleTrigger-Uz1aGdtH.js +0 -1
- package/frontend-dist/assets/Collection-1EHC87X5.js +0 -3
- package/frontend-dist/assets/Dashboard-C3FL30UN.js +0 -3
- package/frontend-dist/assets/DialogTitle-CAOFxr83.js +0 -1
- package/frontend-dist/assets/Input-DRIid2C6.js +0 -1
- package/frontend-dist/assets/Label-UyNN2jyE.js +0 -1
- package/frontend-dist/assets/LogDetailDialog-8BT4vIlV.js +0 -3
- package/frontend-dist/assets/Login-CnzH6TdS.js +0 -1
- package/frontend-dist/assets/Logs-CbK8NB_X.js +0 -1
- package/frontend-dist/assets/ModelMappings-DeRFgsYG.js +0 -1
- package/frontend-dist/assets/Monitor-Dd80bdUn.js +0 -1
- package/frontend-dist/assets/PopperContent-B3fZao7v.js +0 -1
- package/frontend-dist/assets/Providers-B_DbV-_y.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-up1fnPzq.js +0 -5
- package/frontend-dist/assets/RetryRules-Dkuhjh0u.js +0 -1
- package/frontend-dist/assets/RouterKeys-CvMMAa4t.js +0 -1
- package/frontend-dist/assets/RovingFocusItem-X0bfqWWS.js +0 -1
- package/frontend-dist/assets/SelectValue-zO8t-tx1.js +0 -1
- package/frontend-dist/assets/Setup-ByT2ThOQ.js +0 -1
- package/frontend-dist/assets/Switch-BEMjVugO.js +0 -1
- package/frontend-dist/assets/TableHeader-DpHWSnxK.js +0 -1
- package/frontend-dist/assets/TabsTrigger-Db6RqsZc.js +0 -1
- package/frontend-dist/assets/VisuallyHidden-hs8pj8OP.js +0 -1
- package/frontend-dist/assets/VisuallyHiddenInput-1m0nNADN.js +0 -1
- package/frontend-dist/assets/alert-dialog-PP91kaO8.js +0 -1
- package/frontend-dist/assets/button-Dcc0gF5i.js +0 -1
- package/frontend-dist/assets/client-DIIo9zPK.js +0 -12
- package/frontend-dist/assets/dialog-CxSyR-fN.js +0 -1
- package/frontend-dist/assets/index-BL-LAtac.css +0 -1
- package/frontend-dist/assets/index-CvT41fGL.js +0 -1
- package/frontend-dist/assets/lib-Bl0OuBjh.js +0 -1
- package/frontend-dist/assets/useForwardExpose-AkE0lq8y.js +0 -1
- package/frontend-dist/assets/useNonce-DGyPxdjq.js +0 -1
- package/frontend-dist/assets/x-BuUpx9Fr.js +0 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { deleteLogsBefore } from "./logs.js";
|
|
2
|
+
import { getLogRetentionDays } from "./settings.js";
|
|
3
|
+
const MS_PER_DAY = 86_400_000;
|
|
4
|
+
const CLEANUP_INTERVAL_MS = 3_600_000; // 1 小时
|
|
5
|
+
/** 运行一次清理,返回删除条数 */
|
|
6
|
+
export function runLogCleanup(db) {
|
|
7
|
+
const days = getLogRetentionDays(db);
|
|
8
|
+
if (days <= 0)
|
|
9
|
+
return 0;
|
|
10
|
+
const cutoff = new Date(Date.now() - days * MS_PER_DAY).toISOString();
|
|
11
|
+
return deleteLogsBefore(db, cutoff);
|
|
12
|
+
}
|
|
13
|
+
/** 启动定时清理,返回 handle 用于停止 */
|
|
14
|
+
export function scheduleLogCleanup(db, log) {
|
|
15
|
+
let cleaning = false;
|
|
16
|
+
let timer = null;
|
|
17
|
+
const doCleanup = () => {
|
|
18
|
+
if (cleaning)
|
|
19
|
+
return;
|
|
20
|
+
cleaning = true;
|
|
21
|
+
try {
|
|
22
|
+
const deleted = runLogCleanup(db);
|
|
23
|
+
if (deleted > 0)
|
|
24
|
+
log.info(`Log cleanup: deleted ${deleted} records`);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
cleaning = false;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
// 启动时立即执行一次
|
|
31
|
+
doCleanup();
|
|
32
|
+
// 定时执行
|
|
33
|
+
timer = setInterval(doCleanup, CLEANUP_INTERVAL_MS);
|
|
34
|
+
return {
|
|
35
|
+
stop: () => {
|
|
36
|
+
if (timer) {
|
|
37
|
+
clearInterval(timer);
|
|
38
|
+
timer = null;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
package/dist/db/logs.d.ts
CHANGED
|
@@ -9,21 +9,27 @@ export interface RequestLog {
|
|
|
9
9
|
is_stream: number;
|
|
10
10
|
error_message: string | null;
|
|
11
11
|
created_at: string;
|
|
12
|
-
request_body: string | null;
|
|
13
|
-
response_body: string | null;
|
|
14
12
|
client_request: string | null;
|
|
15
13
|
upstream_request: string | null;
|
|
16
14
|
upstream_response: string | null;
|
|
17
|
-
client_response: string | null;
|
|
18
15
|
is_retry: number;
|
|
19
16
|
is_failover: number;
|
|
20
17
|
original_request_id: string | null;
|
|
21
18
|
original_model: string | null;
|
|
19
|
+
input_tokens: number | null;
|
|
20
|
+
output_tokens: number | null;
|
|
21
|
+
cache_read_tokens: number | null;
|
|
22
|
+
ttft_ms: number | null;
|
|
23
|
+
tokens_per_second: number | null;
|
|
24
|
+
stop_reason: string | null;
|
|
25
|
+
backend_model: string | null;
|
|
26
|
+
metrics_complete: number;
|
|
27
|
+
stream_text_content: string | null;
|
|
22
28
|
}
|
|
23
|
-
/** 列表查询扩展字段:JOIN
|
|
29
|
+
/** 列表查询扩展字段:JOIN providers 获得 provider_name */
|
|
24
30
|
export interface RequestLogListRow extends RequestLog {
|
|
25
|
-
backend_model: string | null;
|
|
26
31
|
provider_name: string | null;
|
|
32
|
+
child_count?: number;
|
|
27
33
|
}
|
|
28
34
|
export interface RequestLogInsert {
|
|
29
35
|
id: string;
|
|
@@ -35,12 +41,9 @@ export interface RequestLogInsert {
|
|
|
35
41
|
is_stream: number;
|
|
36
42
|
error_message: string | null;
|
|
37
43
|
created_at: string;
|
|
38
|
-
request_body?: string | null;
|
|
39
|
-
response_body?: string | null;
|
|
40
44
|
client_request?: string | null;
|
|
41
45
|
upstream_request?: string | null;
|
|
42
46
|
upstream_response?: string | null;
|
|
43
|
-
client_response?: string | null;
|
|
44
47
|
is_retry?: number;
|
|
45
48
|
is_failover?: number;
|
|
46
49
|
original_request_id?: string | null;
|
|
@@ -54,11 +57,29 @@ export declare function getRequestLogs(db: Database.Database, options: {
|
|
|
54
57
|
api_type?: string;
|
|
55
58
|
model?: string;
|
|
56
59
|
router_key_id?: string;
|
|
60
|
+
provider_id?: string;
|
|
61
|
+
start_time?: string;
|
|
62
|
+
end_time?: string;
|
|
57
63
|
}): {
|
|
58
64
|
data: RequestLogListRow[];
|
|
59
65
|
total: number;
|
|
60
66
|
};
|
|
61
67
|
export declare function getRequestLogById(db: Database.Database, id: string): RequestLog | undefined;
|
|
68
|
+
type MetricsUpdate = {
|
|
69
|
+
input_tokens?: number | null;
|
|
70
|
+
output_tokens?: number | null;
|
|
71
|
+
cache_read_tokens?: number | null;
|
|
72
|
+
ttft_ms?: number | null;
|
|
73
|
+
tokens_per_second?: number | null;
|
|
74
|
+
stop_reason?: string | null;
|
|
75
|
+
is_complete?: number;
|
|
76
|
+
};
|
|
77
|
+
/** 双写:collectTransportMetrics 写 request_metrics 的同时,更新 request_logs 的冗余列 */
|
|
78
|
+
export declare function updateLogMetrics(db: Database.Database, logId: string, m: MetricsUpdate): void;
|
|
79
|
+
/** 流式请求完成后,将 tracker 中累积的文本内容写入 request_logs */
|
|
80
|
+
export declare function updateLogStreamContent(db: Database.Database, logId: string, textContent: string): void;
|
|
81
|
+
/** 启动时回填:从 request_metrics 补齐 metrics_complete = 0 但实际有指标的行 */
|
|
82
|
+
export declare function backfillMetricsFromRequestMetrics(db: Database.Database): number;
|
|
62
83
|
export declare function deleteLogsBefore(db: Database.Database, beforeDate: string): number;
|
|
63
84
|
/** 查询某条日志的子请求(retry/failover 关联),上限 100 条 */
|
|
64
85
|
export declare function getRequestLogChildren(db: Database.Database, parentId: string): RequestLogListRow[];
|
|
@@ -72,7 +93,11 @@ export declare function getRequestLogsGrouped(db: Database.Database, options: {
|
|
|
72
93
|
api_type?: string;
|
|
73
94
|
model?: string;
|
|
74
95
|
router_key_id?: string;
|
|
96
|
+
provider_id?: string;
|
|
97
|
+
start_time?: string;
|
|
98
|
+
end_time?: string;
|
|
75
99
|
}): {
|
|
76
100
|
data: RequestLogGroupedRow[];
|
|
77
101
|
total: number;
|
|
78
102
|
};
|
|
103
|
+
export {};
|
package/dist/db/logs.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
// --- request_logs ---
|
|
2
|
-
/**
|
|
2
|
+
/** 日志列表查询共享的 SELECT 列 + JOIN 子句(metrics 已冗余到 request_logs,无需 JOIN request_metrics) */
|
|
3
3
|
const LOG_LIST_SELECT = `rl.id, rl.api_type, rl.model, rl.provider_id, rl.status_code, rl.latency_ms,
|
|
4
4
|
rl.is_stream, rl.error_message, rl.created_at, rl.is_retry, rl.is_failover, rl.original_request_id, rl.original_model,
|
|
5
5
|
CASE WHEN rl.provider_id = 'router' THEN rl.upstream_request ELSE NULL END AS upstream_request,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
rl.input_tokens, rl.output_tokens, rl.cache_read_tokens, rl.ttft_ms, rl.tokens_per_second, rl.stop_reason,
|
|
7
|
+
rl.backend_model, rl.metrics_complete,
|
|
8
|
+
COALESCE(p.name, rl.provider_id) AS provider_name`;
|
|
9
|
+
const LOG_LIST_JOIN = `LEFT JOIN providers p ON p.id = rl.provider_id`;
|
|
9
10
|
export function insertRequestLog(db, log) {
|
|
10
|
-
db.prepare(`INSERT INTO request_logs (id, api_type, model, provider_id, status_code, latency_ms,
|
|
11
|
-
|
|
11
|
+
db.prepare(`INSERT INTO request_logs (id, api_type, model, provider_id, status_code, latency_ms,
|
|
12
|
+
is_stream, error_message, created_at, client_request, upstream_request, upstream_response,
|
|
13
|
+
is_retry, is_failover, original_request_id, router_key_id, original_model)
|
|
14
|
+
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.client_request ?? null, log.upstream_request ?? null, log.upstream_response ?? null, log.is_retry ?? 0, log.is_failover ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null);
|
|
12
15
|
}
|
|
13
16
|
function buildLogWhereClause(options, baseCondition) {
|
|
14
17
|
let where = baseCondition;
|
|
@@ -25,6 +28,18 @@ function buildLogWhereClause(options, baseCondition) {
|
|
|
25
28
|
where += " AND rl.router_key_id = ?";
|
|
26
29
|
params.push(options.router_key_id);
|
|
27
30
|
}
|
|
31
|
+
if (options.provider_id) {
|
|
32
|
+
where += " AND rl.provider_id = ?";
|
|
33
|
+
params.push(options.provider_id);
|
|
34
|
+
}
|
|
35
|
+
if (options.start_time) {
|
|
36
|
+
where += " AND rl.created_at >= ?";
|
|
37
|
+
params.push(options.start_time);
|
|
38
|
+
}
|
|
39
|
+
if (options.end_time) {
|
|
40
|
+
where += " AND rl.created_at <= ?";
|
|
41
|
+
params.push(options.end_time);
|
|
42
|
+
}
|
|
28
43
|
return { where, params };
|
|
29
44
|
}
|
|
30
45
|
export function getRequestLogs(db, options) {
|
|
@@ -42,6 +57,37 @@ export function getRequestLogs(db, options) {
|
|
|
42
57
|
export function getRequestLogById(db, id) {
|
|
43
58
|
return db.prepare("SELECT * FROM request_logs WHERE id = ?").get(id);
|
|
44
59
|
}
|
|
60
|
+
/** 双写:collectTransportMetrics 写 request_metrics 的同时,更新 request_logs 的冗余列 */
|
|
61
|
+
export function updateLogMetrics(db, logId, m) {
|
|
62
|
+
db.prepare(`UPDATE request_logs SET
|
|
63
|
+
input_tokens = ?, output_tokens = ?, cache_read_tokens = ?,
|
|
64
|
+
ttft_ms = ?, tokens_per_second = ?, stop_reason = ?,
|
|
65
|
+
backend_model = (SELECT backend_model FROM request_metrics WHERE request_log_id = ?),
|
|
66
|
+
metrics_complete = ?
|
|
67
|
+
WHERE id = ?`).run(m.input_tokens ?? null, m.output_tokens ?? null, m.cache_read_tokens ?? null, m.ttft_ms ?? null, m.tokens_per_second ?? null, m.stop_reason ?? null, logId, m.is_complete ?? 1, logId);
|
|
68
|
+
}
|
|
69
|
+
/** 流式请求完成后,将 tracker 中累积的文本内容写入 request_logs */
|
|
70
|
+
export function updateLogStreamContent(db, logId, textContent) {
|
|
71
|
+
db.prepare("UPDATE request_logs SET stream_text_content = ? WHERE id = ?").run(textContent, logId);
|
|
72
|
+
}
|
|
73
|
+
/** 启动时回填:从 request_metrics 补齐 metrics_complete = 0 但实际有指标的行 */
|
|
74
|
+
export function backfillMetricsFromRequestMetrics(db) {
|
|
75
|
+
return db.prepare(`
|
|
76
|
+
UPDATE request_logs
|
|
77
|
+
SET
|
|
78
|
+
input_tokens = rm.input_tokens,
|
|
79
|
+
output_tokens = rm.output_tokens,
|
|
80
|
+
cache_read_tokens = rm.cache_read_tokens,
|
|
81
|
+
ttft_ms = rm.ttft_ms,
|
|
82
|
+
tokens_per_second = rm.tokens_per_second,
|
|
83
|
+
stop_reason = rm.stop_reason,
|
|
84
|
+
backend_model = rm.backend_model,
|
|
85
|
+
metrics_complete = rm.is_complete
|
|
86
|
+
FROM request_metrics rm
|
|
87
|
+
WHERE rm.request_log_id = request_logs.id
|
|
88
|
+
AND request_logs.metrics_complete = 0
|
|
89
|
+
`).run().changes;
|
|
90
|
+
}
|
|
45
91
|
export function deleteLogsBefore(db, beforeDate) {
|
|
46
92
|
return db.prepare("DELETE FROM request_logs WHERE created_at < ?").run(beforeDate).changes;
|
|
47
93
|
}
|
package/dist/db/metrics.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
4
|
export interface MetricsRow {
|
|
5
5
|
id: string;
|
|
@@ -48,10 +48,10 @@ export interface MetricsSummaryRow {
|
|
|
48
48
|
total_cache_hit_tokens: number;
|
|
49
49
|
cache_hit_rate: number | null;
|
|
50
50
|
}
|
|
51
|
-
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[];
|
|
52
52
|
export interface MetricsTimeseriesRow {
|
|
53
53
|
time_bucket: string;
|
|
54
54
|
avg_value: number | null;
|
|
55
55
|
count: number;
|
|
56
56
|
}
|
|
57
|
-
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
|
@@ -7,6 +7,7 @@ export function insertMetrics(db, m) {
|
|
|
7
7
|
}
|
|
8
8
|
const PERIOD_OFFSET = {
|
|
9
9
|
"1h": "-1 hours",
|
|
10
|
+
"5h": "-5 hours",
|
|
10
11
|
"6h": "-6 hours",
|
|
11
12
|
"24h": "-1 day",
|
|
12
13
|
"7d": "-7 days",
|
|
@@ -14,6 +15,7 @@ const PERIOD_OFFSET = {
|
|
|
14
15
|
};
|
|
15
16
|
const BUCKET_SECONDS = {
|
|
16
17
|
"1h": 60,
|
|
18
|
+
"5h": 300,
|
|
17
19
|
"6h": 300,
|
|
18
20
|
"24h": 900,
|
|
19
21
|
"7d": 3600,
|
|
@@ -21,10 +23,39 @@ const BUCKET_SECONDS = {
|
|
|
21
23
|
};
|
|
22
24
|
// unix epoch 秒转毫秒的乘数
|
|
23
25
|
const MS_PER_SEC = 1000;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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"];
|
|
28
59
|
if (providerId) {
|
|
29
60
|
conditions.push("rm.provider_id = ?");
|
|
30
61
|
params.push(providerId);
|
|
@@ -33,48 +64,23 @@ export function getMetricsSummary(db, period, providerId, backendModel, routerKe
|
|
|
33
64
|
conditions.push("rm.backend_model = ?");
|
|
34
65
|
params.push(backendModel);
|
|
35
66
|
}
|
|
36
|
-
const joins = "LEFT JOIN providers p ON p.id = rm.provider_id";
|
|
37
67
|
if (routerKeyId) {
|
|
68
|
+
joins.push("LEFT JOIN request_logs rl ON rl.id = rm.request_log_id");
|
|
38
69
|
conditions.push("rl.router_key_id = ?");
|
|
39
70
|
params.push(routerKeyId);
|
|
40
|
-
return db.prepare(`
|
|
41
|
-
SELECT
|
|
42
|
-
rm.provider_id, COALESCE(p.name, rm.provider_id) AS provider_name, rm.backend_model,
|
|
43
|
-
COUNT(*) AS request_count, AVG(rm.ttft_ms) AS avg_ttft_ms, NULL AS p50_ttft_ms, NULL AS p95_ttft_ms,
|
|
44
|
-
AVG(rm.tokens_per_second) AS avg_tps,
|
|
45
|
-
COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens, COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens,
|
|
46
|
-
COALESCE(SUM(rm.cache_read_tokens), 0) AS total_cache_hit_tokens,
|
|
47
|
-
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
|
|
48
|
-
FROM request_metrics rm
|
|
49
|
-
LEFT JOIN providers p ON p.id = rm.provider_id
|
|
50
|
-
LEFT JOIN request_logs rl ON rl.id = rm.request_log_id
|
|
51
|
-
WHERE ${conditions.join(" AND ")}
|
|
52
|
-
GROUP BY rm.provider_id, rm.backend_model ORDER BY request_count DESC
|
|
53
|
-
`).all(...params);
|
|
54
71
|
}
|
|
55
|
-
const where = conditions.join(" AND ");
|
|
56
72
|
return db.prepare(`
|
|
57
73
|
SELECT
|
|
58
|
-
rm.provider_id,
|
|
59
|
-
|
|
60
|
-
rm.backend_model,
|
|
61
|
-
COUNT(*) AS request_count,
|
|
62
|
-
AVG(rm.ttft_ms) AS avg_ttft_ms,
|
|
63
|
-
NULL AS p50_ttft_ms,
|
|
64
|
-
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,
|
|
65
76
|
AVG(rm.tokens_per_second) AS avg_tps,
|
|
66
|
-
COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens,
|
|
67
|
-
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,
|
|
68
78
|
COALESCE(SUM(rm.cache_read_tokens), 0) AS total_cache_hit_tokens,
|
|
69
|
-
CASE WHEN SUM(rm.input_tokens) > 0
|
|
70
|
-
THEN SUM(rm.cache_read_tokens) * 1.0 / SUM(rm.input_tokens)
|
|
71
|
-
ELSE NULL
|
|
72
|
-
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
|
|
73
80
|
FROM request_metrics rm
|
|
74
|
-
${joins}
|
|
75
|
-
WHERE ${
|
|
76
|
-
GROUP BY rm.provider_id, rm.backend_model
|
|
77
|
-
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
|
|
78
84
|
`).all(...params);
|
|
79
85
|
}
|
|
80
86
|
const METRIC_EXPR = {
|
|
@@ -87,11 +93,13 @@ const METRIC_EXPR = {
|
|
|
87
93
|
output_tokens: "SUM(rm.output_tokens)",
|
|
88
94
|
cache_hit_tokens: "SUM(rm.cache_read_tokens)",
|
|
89
95
|
};
|
|
90
|
-
export function getMetricsTimeseries(db, period, metric, providerId, backendModel, routerKeyId) {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
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];
|
|
95
103
|
if (providerId) {
|
|
96
104
|
conditions.push("rm.provider_id = ?");
|
|
97
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);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- 019_drop_log_redundancy.sql
|
|
2
|
+
-- 删除冗余大文本字段:request_body = client_request.body, response_body = upstream_response.body, client_response ≈ upstream_response
|
|
3
|
+
ALTER TABLE request_logs DROP COLUMN request_body;
|
|
4
|
+
ALTER TABLE request_logs DROP COLUMN response_body;
|
|
5
|
+
ALTER TABLE request_logs DROP COLUMN client_response;
|
|
6
|
+
|
|
7
|
+
-- 日志自动清理保留天数(默认 3 天,0 = 不自动清理)
|
|
8
|
+
INSERT OR IGNORE INTO settings (key, value) VALUES ('log_retention_days', '3');
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- V1 双写:将 request_metrics 列冗余到 request_logs,消除日志查询的 JOIN
|
|
2
|
+
-- request_metrics 表保留不动,聚合查询仍查它
|
|
3
|
+
|
|
4
|
+
ALTER TABLE request_logs ADD COLUMN input_tokens INTEGER;
|
|
5
|
+
ALTER TABLE request_logs ADD COLUMN output_tokens INTEGER;
|
|
6
|
+
ALTER TABLE request_logs ADD COLUMN cache_read_tokens INTEGER;
|
|
7
|
+
ALTER TABLE request_logs ADD COLUMN ttft_ms INTEGER;
|
|
8
|
+
ALTER TABLE request_logs ADD COLUMN tokens_per_second REAL;
|
|
9
|
+
ALTER TABLE request_logs ADD COLUMN stop_reason TEXT;
|
|
10
|
+
ALTER TABLE request_logs ADD COLUMN backend_model TEXT;
|
|
11
|
+
ALTER TABLE request_logs ADD COLUMN metrics_complete INTEGER NOT NULL DEFAULT 0;
|
|
12
|
+
|
|
13
|
+
-- 流式请求的累积文本内容(从 tracker.appendStreamChunk 中提取的纯文本)
|
|
14
|
+
ALTER TABLE request_logs ADD COLUMN stream_text_content TEXT;
|
|
15
|
+
|
|
16
|
+
-- 回填历史数据:把已有的 request_metrics 写入 request_logs 新列
|
|
17
|
+
UPDATE request_logs
|
|
18
|
+
SET
|
|
19
|
+
input_tokens = rm.input_tokens,
|
|
20
|
+
output_tokens = rm.output_tokens,
|
|
21
|
+
cache_read_tokens = rm.cache_read_tokens,
|
|
22
|
+
ttft_ms = rm.ttft_ms,
|
|
23
|
+
tokens_per_second = rm.tokens_per_second,
|
|
24
|
+
stop_reason = rm.stop_reason,
|
|
25
|
+
backend_model = rm.backend_model,
|
|
26
|
+
metrics_complete = rm.is_complete
|
|
27
|
+
FROM request_metrics rm
|
|
28
|
+
WHERE rm.request_log_id = request_logs.id;
|
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
|
-
* 已存在的规则不会被重复插入或覆盖。
|
|
31
|
-
*/
|
|
32
|
-
export declare function seedDefaultRules(db: Database.Database): void;
|
package/dist/db/retry-rules.js
CHANGED
|
@@ -27,39 +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_RETRY_FIELDS = {
|
|
32
|
-
is_active: 1,
|
|
33
|
-
retry_strategy: "exponential",
|
|
34
|
-
retry_delay_ms: DEFAULT_RETRY_DELAY_MS,
|
|
35
|
-
max_retries: DEFAULT_MAX_RETRIES,
|
|
36
|
-
max_delay_ms: DEFAULT_MAX_DELAY_MS,
|
|
37
|
-
};
|
|
38
|
-
const DEFAULT_RULES = [
|
|
39
|
-
{ name: "429 Too Many Requests", status_code: 429, body_pattern: ".*", ...DEFAULT_RETRY_FIELDS },
|
|
40
|
-
{ name: "503 Service Unavailable", status_code: 503, body_pattern: ".*", ...DEFAULT_RETRY_FIELDS },
|
|
41
|
-
{ name: 'ZAI 网络错误 (code 1234)', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*"code"\\s*:\\s*"1234"', ...DEFAULT_RETRY_FIELDS },
|
|
42
|
-
{ name: 'ZAI 临时不可用', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*请稍后重试', ...DEFAULT_RETRY_FIELDS },
|
|
43
|
-
{ name: 'ZAI 操作失败 (code 500)', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*"code"\\s*:\\s*"500"', ...DEFAULT_RETRY_FIELDS },
|
|
44
|
-
{ name: 'ZAI 速率限制 (HTTP 200, code 1302)', status_code: 200, body_pattern: '"error".*"code"\\s*:\\s*"1302"', ...DEFAULT_RETRY_FIELDS },
|
|
45
|
-
{ name: 'ZAI SSE 错误 (HTTP 200, code 500)', status_code: 200, body_pattern: '"error".*"code"\\s*:\\s*"500"', ...DEFAULT_RETRY_FIELDS },
|
|
46
|
-
{ name: 'ZAI SSE 错误 (HTTP 200, code 1234)', status_code: 200, body_pattern: '"error".*"code"\\s*:\\s*"1234"', ...DEFAULT_RETRY_FIELDS },
|
|
47
|
-
];
|
|
48
|
-
/**
|
|
49
|
-
* 启动时按名称查重插入默认重试规则。
|
|
50
|
-
* 已存在的规则不会被重复插入或覆盖。
|
|
51
|
-
*/
|
|
52
|
-
export function seedDefaultRules(db) {
|
|
53
|
-
const names = DEFAULT_RULES.map(r => r.name);
|
|
54
|
-
const placeholders = names.map(() => '?').join(',');
|
|
55
|
-
const existing = db.prepare(`SELECT name FROM retry_rules WHERE name IN (${placeholders})`).all(...names);
|
|
56
|
-
const existingSet = new Set(existing.map(r => r.name));
|
|
57
|
-
const stmt = 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)
|
|
58
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
59
|
-
const now = new Date().toISOString();
|
|
60
|
-
for (const rule of DEFAULT_RULES) {
|
|
61
|
-
if (!existingSet.has(rule.name)) {
|
|
62
|
-
stmt.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);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
package/dist/db/settings.d.ts
CHANGED
|
@@ -2,3 +2,5 @@ import Database from "better-sqlite3";
|
|
|
2
2
|
export declare function getSetting(db: Database.Database, key: string): string | null;
|
|
3
3
|
export declare function setSetting(db: Database.Database, key: string, value: string): void;
|
|
4
4
|
export declare function isInitialized(db: Database.Database): boolean;
|
|
5
|
+
export declare function getLogRetentionDays(db: Database.Database): number;
|
|
6
|
+
export declare function setLogRetentionDays(db: Database.Database, days: number): void;
|
package/dist/db/settings.js
CHANGED
|
@@ -8,3 +8,10 @@ export function setSetting(db, key, value) {
|
|
|
8
8
|
export function isInitialized(db) {
|
|
9
9
|
return getSetting(db, "initialized") === "true";
|
|
10
10
|
}
|
|
11
|
+
export function getLogRetentionDays(db) {
|
|
12
|
+
const val = getSetting(db, "log_retention_days");
|
|
13
|
+
return val ? parseInt(val, 10) : 3;
|
|
14
|
+
}
|
|
15
|
+
export function setLogRetentionDays(db, days) {
|
|
16
|
+
setSetting(db, "log_retention_days", String(days));
|
|
17
|
+
}
|
|
@@ -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
|
@@ -21,7 +21,8 @@ function getProxyApiType(url) {
|
|
|
21
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
22
22
|
const __dirname = path.dirname(__filename);
|
|
23
23
|
import { getConfig } from "./config.js";
|
|
24
|
-
import { initDatabase,
|
|
24
|
+
import { initDatabase, getAllProviders, backfillMetricsFromRequestMetrics } from "./db/index.js";
|
|
25
|
+
import { loadRecommendedConfig } from "./config/recommended.js";
|
|
25
26
|
import { authMiddleware } from "./middleware/auth.js";
|
|
26
27
|
import { openaiProxy } from "./proxy/openai.js";
|
|
27
28
|
import { anthropicProxy } from "./proxy/anthropic.js";
|
|
@@ -30,16 +31,20 @@ import { RetryRuleMatcher } from "./proxy/retry-rules.js";
|
|
|
30
31
|
import { ProviderSemaphoreManager } from "./proxy/semaphore.js";
|
|
31
32
|
import { RequestTracker } from "./monitor/request-tracker.js";
|
|
32
33
|
import { modelState } from "./proxy/model-state.js";
|
|
34
|
+
import { UsageWindowTracker } from "./proxy/usage-window-tracker.js";
|
|
35
|
+
import { scheduleLogCleanup } from "./db/log-cleaner.js";
|
|
33
36
|
import fastifyStatic from "@fastify/static";
|
|
34
37
|
export async function buildApp(options) {
|
|
35
38
|
const config = options?.config ?? getBaseConfig();
|
|
36
39
|
// 允许外部传入已初始化的 DB(测试用),否则自行创建
|
|
37
40
|
let db;
|
|
41
|
+
let shouldBackfill = false;
|
|
38
42
|
if (options?.db) {
|
|
39
43
|
db = options.db;
|
|
40
44
|
}
|
|
41
45
|
else {
|
|
42
46
|
db = initDatabase(config.DB_PATH);
|
|
47
|
+
shouldBackfill = true;
|
|
43
48
|
}
|
|
44
49
|
const isDev = process.env.NODE_ENV !== "production";
|
|
45
50
|
const app = Fastify({
|
|
@@ -100,8 +105,14 @@ export async function buildApp(options) {
|
|
|
100
105
|
}
|
|
101
106
|
return reply.code(status).send({ error: { message: fastifyError.message } });
|
|
102
107
|
});
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
loadRecommendedConfig();
|
|
109
|
+
// 启动时回填:补齐回退老版本期间缺失的 metrics 冗余列
|
|
110
|
+
if (shouldBackfill) {
|
|
111
|
+
const backfilled = backfillMetricsFromRequestMetrics(db);
|
|
112
|
+
if (backfilled > 0) {
|
|
113
|
+
app.log.info({ backfilled }, "Backfilled metrics from request_metrics");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
105
116
|
// 注入 DB 到 modelState 单例,启用会话级持久化
|
|
106
117
|
modelState.init(db);
|
|
107
118
|
const matcher = new RetryRuleMatcher();
|
|
@@ -109,6 +120,9 @@ export async function buildApp(options) {
|
|
|
109
120
|
const semaphoreManager = new ProviderSemaphoreManager();
|
|
110
121
|
const tracker = new RequestTracker({ semaphoreManager, logger: app.log });
|
|
111
122
|
tracker.startPushInterval();
|
|
123
|
+
// 5h 用量窗口追踪器,启动时自动补齐缺失窗口
|
|
124
|
+
const usageWindowTracker = new UsageWindowTracker(db);
|
|
125
|
+
usageWindowTracker.reconcileOnStartup();
|
|
112
126
|
// 从 DB 读取已有 provider 的并发配置,初始化信号量管理器和 tracker
|
|
113
127
|
const allProviders = getAllProviders(db);
|
|
114
128
|
for (const p of allProviders) {
|
|
@@ -169,10 +183,13 @@ export async function buildApp(options) {
|
|
|
169
183
|
app.get("/health", async () => {
|
|
170
184
|
return { status: "ok" };
|
|
171
185
|
});
|
|
186
|
+
const logCleanup = scheduleLogCleanup(db, app.log);
|
|
172
187
|
return {
|
|
173
188
|
app,
|
|
174
189
|
db,
|
|
190
|
+
usageWindowTracker,
|
|
175
191
|
close: async () => {
|
|
192
|
+
logCleanup.stop();
|
|
176
193
|
tracker.stopPushInterval();
|
|
177
194
|
await app.close();
|
|
178
195
|
db.close();
|
package/dist/monitor/types.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface ActiveRequest {
|
|
|
27
27
|
streamMetrics?: StreamMetricsSnapshot;
|
|
28
28
|
streamContent?: StreamContentSnapshot;
|
|
29
29
|
clientIp?: string;
|
|
30
|
+
sessionId?: string;
|
|
30
31
|
completedAt?: number;
|
|
31
32
|
}
|
|
32
33
|
export interface AttemptSnapshot {
|
|
@@ -38,7 +39,9 @@ export interface AttemptSnapshot {
|
|
|
38
39
|
export interface StreamMetricsSnapshot {
|
|
39
40
|
inputTokens: number | null;
|
|
40
41
|
outputTokens: number | null;
|
|
42
|
+
cacheReadTokens: number | null;
|
|
41
43
|
ttftMs: number | null;
|
|
44
|
+
tokensPerSecond: number | null;
|
|
42
45
|
stopReason: string | null;
|
|
43
46
|
isComplete: boolean;
|
|
44
47
|
}
|