llm-simple-router 0.4.1 → 0.4.3
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/logs.js +2 -2
- package/dist/admin/routes.js +2 -0
- package/dist/admin/settings.d.ts +7 -0
- package/dist/admin/settings.js +16 -0
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +19 -3
- package/dist/db/log-cleaner.d.ts +10 -0
- package/dist/db/log-cleaner.js +42 -0
- package/dist/db/logs.d.ts +27 -8
- package/dist/db/logs.js +40 -6
- 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/settings.d.ts +2 -0
- package/dist/db/settings.js +7 -0
- package/dist/index.js +13 -1
- 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/frontend-dist/assets/{CardContent-fmM_iiuR.js → CardContent-BIgWZo6N.js} +1 -1
- package/frontend-dist/assets/CardTitle-BVVp36Pq.js +1 -0
- package/frontend-dist/assets/Checkbox-CnvHc21k.js +1 -0
- package/frontend-dist/assets/CollapsibleTrigger-DG8JZQfm.js +1 -0
- package/frontend-dist/assets/Collection-B4yfvPwd.js +3 -0
- package/frontend-dist/assets/Dashboard-D9ZMKiZw.js +3 -0
- package/frontend-dist/assets/DialogTitle-C7Wl7fWN.js +1 -0
- package/frontend-dist/assets/{Input-BhvZ-Up7.js → Input-BA5UO-Ab.js} +1 -1
- package/frontend-dist/assets/Label-CmG21jDR.js +1 -0
- package/frontend-dist/assets/Login-Dqwj0s5l.js +1 -0
- package/frontend-dist/assets/Logs-CGzGzcDa.js +1 -0
- package/frontend-dist/assets/ModelMappings-qxaFWRsq.js +1 -0
- package/frontend-dist/assets/Monitor-Zy3O9UQA.js +1 -0
- package/frontend-dist/assets/{PopperContent-CHNw_qb6.js → PopperContent-B48BBgdW.js} +1 -1
- package/frontend-dist/assets/{Providers-C9ZAqHxO.js → Providers-DRMSjTlb.js} +1 -1
- package/frontend-dist/assets/ProxyEnhancement-CtlDQ_oK.js +5 -0
- package/frontend-dist/assets/RetryRules-CxbZt6dv.js +1 -0
- package/frontend-dist/assets/RouterKeys-drAASJqH.js +1 -0
- package/frontend-dist/assets/SelectValue-Ct22fmR7.js +1 -0
- package/frontend-dist/assets/Setup-CfscyFFo.js +1 -0
- package/frontend-dist/assets/Switch-ROnHu48E.js +1 -0
- package/frontend-dist/assets/TableHeader-R3y9M8qo.js +1 -0
- package/frontend-dist/assets/TabsContent-CtT2wglf.js +1 -0
- package/frontend-dist/assets/TabsTrigger-D2S77VwS.js +1 -0
- package/frontend-dist/assets/UnifiedRequestDialog-BhGWzOYY.js +3 -0
- package/frontend-dist/assets/UnifiedRequestDialog-CotaDyW7.css +1 -0
- package/frontend-dist/assets/VisuallyHidden-ppNQPfSR.js +1 -0
- package/frontend-dist/assets/{VisuallyHiddenInput-cjeTgyDe.js → VisuallyHiddenInput-DOdhvOHf.js} +1 -1
- package/frontend-dist/assets/{alert-dialog-BoGRIC1Q.js → alert-dialog-BbI_a1Xz.js} +1 -1
- package/frontend-dist/assets/{badge-DIO8W_W9.js → badge-CcgNasyU.js} +1 -1
- package/frontend-dist/assets/{button-qxGNBunr.js → button-BkhL20Qz.js} +2 -2
- package/frontend-dist/assets/{createLucideIcon-jHUFhqKn.js → createLucideIcon-D2A5NyBH.js} +1 -1
- package/frontend-dist/assets/dialog-BeetdYQi.js +1 -0
- package/frontend-dist/assets/file-text-D-Vekfc6.js +1 -0
- package/frontend-dist/assets/index-Bgqc9Lca.js +1 -0
- package/frontend-dist/assets/index-hzTjKE_2.css +1 -0
- package/frontend-dist/assets/lib-BAWhmkfz.js +1 -0
- package/frontend-dist/assets/{ohash.D__AXeF1-nmJ7gFbh.js → ohash.D__AXeF1-B5oxGXRD.js} +1 -1
- package/frontend-dist/assets/{useClipboard-CmLp2YGk.js → useClipboard-D3mHD2V6.js} +1 -1
- package/frontend-dist/assets/{useForwardExpose-awoGXQkg.js → useForwardExpose-Pf45y-q1.js} +1 -1
- package/frontend-dist/assets/useNonce-Dfbnp6Co.js +1 -0
- package/frontend-dist/assets/x-CrCHzImd.js +1 -0
- package/frontend-dist/index.html +8 -7
- package/package.json +1 -1
- package/frontend-dist/assets/CardHeader-BzzFzZ1B.js +0 -1
- package/frontend-dist/assets/CardTitle-09d7O-11.js +0 -1
- package/frontend-dist/assets/Checkbox-DH8iqXQd.js +0 -1
- package/frontend-dist/assets/CollapsibleTrigger-DCRRORrU.js +0 -1
- package/frontend-dist/assets/Collection-DY9-Yue9.js +0 -3
- package/frontend-dist/assets/Dashboard-BEzoZuSm.js +0 -3
- package/frontend-dist/assets/DialogTitle-BeMGJzYO.js +0 -1
- package/frontend-dist/assets/Label-DjtouWZ7.js +0 -1
- package/frontend-dist/assets/LogDetailDialog-BjRsy_FR.js +0 -3
- package/frontend-dist/assets/Login-hOCPB-34.js +0 -1
- package/frontend-dist/assets/Logs-C5c3BJsg.js +0 -1
- package/frontend-dist/assets/ModelMappings-CDjxwyyz.js +0 -1
- package/frontend-dist/assets/Monitor-CPAvIREG.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-Ct5WbiB7.js +0 -5
- package/frontend-dist/assets/RetryRules-CbgyrP6w.js +0 -1
- package/frontend-dist/assets/RouterKeys-zmqgFEKp.js +0 -1
- package/frontend-dist/assets/SelectValue-CP4Sh7LP.js +0 -1
- package/frontend-dist/assets/Setup-BXDEPt4o.js +0 -1
- package/frontend-dist/assets/Switch-DF6awXqs.js +0 -1
- package/frontend-dist/assets/TableHeader-BKE_yVML.js +0 -1
- package/frontend-dist/assets/TabsTrigger-D8R7lxaI.js +0 -1
- package/frontend-dist/assets/TooltipTrigger-BjQXeFem.js +0 -1
- package/frontend-dist/assets/VisuallyHidden-B_NnkONE.js +0 -1
- package/frontend-dist/assets/dialog-D8pIXeSs.js +0 -1
- package/frontend-dist/assets/index-C_disqMY.js +0 -1
- package/frontend-dist/assets/index-DDp6SHfg.css +0 -1
- package/frontend-dist/assets/lib-DjpgwSRA.js +0 -1
- package/frontend-dist/assets/useNonce-_2e-GL-A.js +0 -1
- package/frontend-dist/assets/x-B0G-wIAB.js +0 -1
package/dist/admin/logs.js
CHANGED
|
@@ -44,7 +44,7 @@ export const adminLogRoutes = (app, options, done) => {
|
|
|
44
44
|
if (!log) {
|
|
45
45
|
return reply.code(HTTP_NOT_FOUND).send({ error: { message: "Log not found" } });
|
|
46
46
|
}
|
|
47
|
-
return reply.send(
|
|
47
|
+
return reply.send(log);
|
|
48
48
|
});
|
|
49
49
|
app.get("/admin/api/logs/:id/children", async (request, reply) => {
|
|
50
50
|
const params = request.params;
|
|
@@ -53,7 +53,7 @@ export const adminLogRoutes = (app, options, done) => {
|
|
|
53
53
|
return reply.code(HTTP_NOT_FOUND).send({ error: { message: "Log not found" } });
|
|
54
54
|
}
|
|
55
55
|
const rows = getRequestLogChildren(db, params.id);
|
|
56
|
-
return reply.send(
|
|
56
|
+
return reply.send(rows);
|
|
57
57
|
});
|
|
58
58
|
app.delete("/admin/api/logs/before", { schema: { body: DeleteLogsBeforeSchema } }, async (request, reply) => {
|
|
59
59
|
const body = request.body;
|
package/dist/admin/routes.js
CHANGED
|
@@ -10,6 +10,7 @@ import { adminProxyEnhancementRoutes } from "./proxy-enhancement.js";
|
|
|
10
10
|
import { adminRouterKeyRoutes } from "./router-keys.js";
|
|
11
11
|
import { adminSetupRoutes } from "./setup.js";
|
|
12
12
|
import { adminMonitorRoutes } from "./monitor.js";
|
|
13
|
+
import { adminSettingsRoutes } from "./settings.js";
|
|
13
14
|
import { adminRecommendedRoutes } from "./recommended.js";
|
|
14
15
|
import { adminUsageRoutes } from "./usage.js";
|
|
15
16
|
export const adminRoutes = (app, options, done) => {
|
|
@@ -27,6 +28,7 @@ export const adminRoutes = (app, options, done) => {
|
|
|
27
28
|
app.register(adminMetricsRoutes, { db: options.db });
|
|
28
29
|
app.register(adminProxyEnhancementRoutes, { db: options.db });
|
|
29
30
|
app.register(adminMonitorRoutes, { tracker: options.tracker });
|
|
31
|
+
app.register(adminSettingsRoutes, { db: options.db });
|
|
30
32
|
app.register(adminRecommendedRoutes, { db: options.db });
|
|
31
33
|
app.register(adminUsageRoutes, { db: options.db });
|
|
32
34
|
done();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getLogRetentionDays, setLogRetentionDays } from "../db/settings.js";
|
|
2
|
+
export const adminSettingsRoutes = (app, options, done) => {
|
|
3
|
+
const { db } = options;
|
|
4
|
+
app.get("/admin/api/settings/log-retention", async () => {
|
|
5
|
+
return { days: getLogRetentionDays(db) };
|
|
6
|
+
});
|
|
7
|
+
app.put("/admin/api/settings/log-retention", async (request) => {
|
|
8
|
+
const { days } = request.body;
|
|
9
|
+
if (!Number.isInteger(days) || days < 0 || days > 90) {
|
|
10
|
+
throw { statusCode: 400, message: "days must be integer 0-90" };
|
|
11
|
+
}
|
|
12
|
+
setLogRetentionDays(db, days);
|
|
13
|
+
return { days };
|
|
14
|
+
});
|
|
15
|
+
done();
|
|
16
|
+
};
|
package/dist/db/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { getModelMapping, getAllModelMappings, createModelMapping, updateModelMa
|
|
|
6
6
|
export type { ModelMapping, MappingGroup, ProviderModelEntry } from "./mappings.js";
|
|
7
7
|
export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
|
|
8
8
|
export type { RetryRule } from "./retry-rules.js";
|
|
9
|
-
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, } from "./logs.js";
|
|
9
|
+
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, updateLogMetrics, updateLogStreamContent, backfillMetricsFromRequestMetrics, } from "./logs.js";
|
|
10
10
|
export type { RequestLog, RequestLogGroupedRow, RequestLogListRow } from "./logs.js";
|
|
11
11
|
export { getRouterKeyByHash, getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "./router-keys.js";
|
|
12
12
|
export type { RouterKey } from "./router-keys.js";
|
package/dist/db/index.js
CHANGED
|
@@ -5,11 +5,17 @@ import { fileURLToPath } from "url";
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = dirname(__filename);
|
|
7
7
|
const MIGRATIONS_DIR = join(__dirname, "migrations");
|
|
8
|
+
const MIGRATION_RENAMES = {
|
|
9
|
+
"019_drop_log_redundancy.sql": "020_drop_log_redundancy.sql",
|
|
10
|
+
"020_merge_metrics_columns.sql": "021_merge_metrics_columns.sql",
|
|
11
|
+
};
|
|
8
12
|
export function initDatabase(dbPath) {
|
|
9
13
|
if (dbPath !== ":memory:") {
|
|
10
14
|
mkdirSync(dirname(dbPath), { recursive: true });
|
|
11
15
|
}
|
|
12
16
|
const db = new Database(dbPath);
|
|
17
|
+
db.pragma("journal_mode = WAL");
|
|
18
|
+
db.pragma("foreign_keys = ON");
|
|
13
19
|
db.exec(`
|
|
14
20
|
CREATE TABLE IF NOT EXISTS migrations (
|
|
15
21
|
name TEXT PRIMARY KEY,
|
|
@@ -17,6 +23,14 @@ export function initDatabase(dbPath) {
|
|
|
17
23
|
);
|
|
18
24
|
`);
|
|
19
25
|
const applied = new Set(db.prepare("SELECT name FROM migrations").all().map((r) => r.name));
|
|
26
|
+
// 将已应用的旧文件名更新为新文件名,避免重命名后重复执行
|
|
27
|
+
for (const [oldName, newName] of Object.entries(MIGRATION_RENAMES)) {
|
|
28
|
+
if (applied.has(oldName) && !applied.has(newName)) {
|
|
29
|
+
db.prepare("UPDATE migrations SET name = ? WHERE name = ?").run(newName, oldName);
|
|
30
|
+
applied.delete(oldName);
|
|
31
|
+
applied.add(newName);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
20
34
|
const files = readdirSync(MIGRATIONS_DIR)
|
|
21
35
|
.filter((f) => f.endsWith(".sql"))
|
|
22
36
|
.sort();
|
|
@@ -25,13 +39,15 @@ export function initDatabase(dbPath) {
|
|
|
25
39
|
continue;
|
|
26
40
|
try {
|
|
27
41
|
const sql = readFileSync(join(MIGRATIONS_DIR, file), "utf-8");
|
|
28
|
-
db.
|
|
42
|
+
db.transaction(() => {
|
|
43
|
+
db.exec(sql);
|
|
44
|
+
db.prepare("INSERT INTO migrations (name, applied_at) VALUES (?, ?)").run(file, new Date().toISOString());
|
|
45
|
+
})();
|
|
29
46
|
}
|
|
30
47
|
catch (err) {
|
|
31
48
|
console.error(`Failed to apply migration ${file}:`, err);
|
|
32
49
|
throw err;
|
|
33
50
|
}
|
|
34
|
-
db.prepare("INSERT INTO migrations (name, applied_at) VALUES (?, ?)").run(file, new Date().toISOString());
|
|
35
51
|
}
|
|
36
52
|
return db;
|
|
37
53
|
}
|
|
@@ -39,7 +55,7 @@ export function initDatabase(dbPath) {
|
|
|
39
55
|
export { getActiveProviders, getAllProviders, getProviderById, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
|
|
40
56
|
export { getModelMapping, getAllModelMappings, createModelMapping, updateModelMapping, deleteModelMapping, getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getActiveProviderModels, resolveByProviderModel, } from "./mappings.js";
|
|
41
57
|
export { getActiveRetryRules, getAllRetryRules, createRetryRule, updateRetryRule, deleteRetryRule, } from "./retry-rules.js";
|
|
42
|
-
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, } from "./logs.js";
|
|
58
|
+
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, updateLogMetrics, updateLogStreamContent, backfillMetricsFromRequestMetrics, } from "./logs.js";
|
|
43
59
|
export { getRouterKeyByHash, getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "./router-keys.js";
|
|
44
60
|
export { getMetricsSummary, getMetricsTimeseries, insertMetrics } from "./metrics.js";
|
|
45
61
|
export { getStats } from "./stats.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
export interface LogCleanupHandle {
|
|
3
|
+
stop: () => void;
|
|
4
|
+
}
|
|
5
|
+
/** 运行一次清理,返回删除条数 */
|
|
6
|
+
export declare function runLogCleanup(db: Database.Database): number;
|
|
7
|
+
/** 启动定时清理,返回 handle 用于停止 */
|
|
8
|
+
export declare function scheduleLogCleanup(db: Database.Database, log: {
|
|
9
|
+
info: (msg: string) => void;
|
|
10
|
+
}): LogCleanupHandle;
|
|
@@ -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;
|
|
@@ -62,6 +65,21 @@ export declare function getRequestLogs(db: Database.Database, options: {
|
|
|
62
65
|
total: number;
|
|
63
66
|
};
|
|
64
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;
|
|
65
83
|
export declare function deleteLogsBefore(db: Database.Database, beforeDate: string): number;
|
|
66
84
|
/** 查询某条日志的子请求(retry/failover 关联),上限 100 条 */
|
|
67
85
|
export declare function getRequestLogChildren(db: Database.Database, parentId: string): RequestLogListRow[];
|
|
@@ -82,3 +100,4 @@ export declare function getRequestLogsGrouped(db: Database.Database, options: {
|
|
|
82
100
|
data: RequestLogGroupedRow[];
|
|
83
101
|
total: number;
|
|
84
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;
|
|
@@ -54,6 +57,37 @@ export function getRequestLogs(db, options) {
|
|
|
54
57
|
export function getRequestLogById(db, id) {
|
|
55
58
|
return db.prepare("SELECT * FROM request_logs WHERE id = ?").get(id);
|
|
56
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
|
+
}
|
|
57
91
|
export function deleteLogsBefore(db, beforeDate) {
|
|
58
92
|
return db.prepare("DELETE FROM request_logs WHERE created_at < ?").run(beforeDate).changes;
|
|
59
93
|
}
|
|
@@ -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/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
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ 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, getAllProviders } from "./db/index.js";
|
|
24
|
+
import { initDatabase, getAllProviders, backfillMetricsFromRequestMetrics } from "./db/index.js";
|
|
25
25
|
import { loadRecommendedConfig } from "./config/recommended.js";
|
|
26
26
|
import { authMiddleware } from "./middleware/auth.js";
|
|
27
27
|
import { openaiProxy } from "./proxy/openai.js";
|
|
@@ -32,16 +32,19 @@ import { ProviderSemaphoreManager } from "./proxy/semaphore.js";
|
|
|
32
32
|
import { RequestTracker } from "./monitor/request-tracker.js";
|
|
33
33
|
import { modelState } from "./proxy/model-state.js";
|
|
34
34
|
import { UsageWindowTracker } from "./proxy/usage-window-tracker.js";
|
|
35
|
+
import { scheduleLogCleanup } from "./db/log-cleaner.js";
|
|
35
36
|
import fastifyStatic from "@fastify/static";
|
|
36
37
|
export async function buildApp(options) {
|
|
37
38
|
const config = options?.config ?? getBaseConfig();
|
|
38
39
|
// 允许外部传入已初始化的 DB(测试用),否则自行创建
|
|
39
40
|
let db;
|
|
41
|
+
let shouldBackfill = false;
|
|
40
42
|
if (options?.db) {
|
|
41
43
|
db = options.db;
|
|
42
44
|
}
|
|
43
45
|
else {
|
|
44
46
|
db = initDatabase(config.DB_PATH);
|
|
47
|
+
shouldBackfill = true;
|
|
45
48
|
}
|
|
46
49
|
const isDev = process.env.NODE_ENV !== "production";
|
|
47
50
|
const app = Fastify({
|
|
@@ -103,6 +106,13 @@ export async function buildApp(options) {
|
|
|
103
106
|
return reply.code(status).send({ error: { message: fastifyError.message } });
|
|
104
107
|
});
|
|
105
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
|
+
}
|
|
106
116
|
// 注入 DB 到 modelState 单例,启用会话级持久化
|
|
107
117
|
modelState.init(db);
|
|
108
118
|
const matcher = new RetryRuleMatcher();
|
|
@@ -173,11 +183,13 @@ export async function buildApp(options) {
|
|
|
173
183
|
app.get("/health", async () => {
|
|
174
184
|
return { status: "ok" };
|
|
175
185
|
});
|
|
186
|
+
const logCleanup = scheduleLogCleanup(db, app.log);
|
|
176
187
|
return {
|
|
177
188
|
app,
|
|
178
189
|
db,
|
|
179
190
|
usageWindowTracker,
|
|
180
191
|
close: async () => {
|
|
192
|
+
logCleanup.stop();
|
|
181
193
|
tracker.stopPushInterval();
|
|
182
194
|
await app.close();
|
|
183
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
|
}
|
|
@@ -17,13 +17,11 @@ export interface RequestLogParams extends LogRetryMeta {
|
|
|
17
17
|
provider: Provider;
|
|
18
18
|
isStream: boolean;
|
|
19
19
|
startTime: number;
|
|
20
|
-
reqBody: string;
|
|
21
20
|
clientReq: string;
|
|
22
21
|
upstreamReq: string;
|
|
23
22
|
status: number;
|
|
24
23
|
respBody: string | null;
|
|
25
24
|
upHdrs: Record<string, string>;
|
|
26
|
-
cliHdrs: Record<string, string>;
|
|
27
25
|
routerKeyId?: string | null;
|
|
28
26
|
originalModel?: string | null;
|
|
29
27
|
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { insertRequestLog } from "../db/index.js";
|
|
2
2
|
/** 插入成功请求日志,供 openai/anthropic 插件共享 */
|
|
3
3
|
export function insertSuccessLog(db, params) {
|
|
4
|
-
const { id: logId, apiType, model, provider, isStream, startTime,
|
|
4
|
+
const { id: logId, apiType, model, provider, isStream, startTime, clientReq, upstreamReq, status, respBody, upHdrs, isRetry = false, isFailover = false, originalRequestId = null, routerKeyId = null, originalModel = null } = params;
|
|
5
5
|
insertRequestLog(db, {
|
|
6
6
|
id: logId, api_type: apiType, model, provider_id: provider.id,
|
|
7
7
|
status_code: status, latency_ms: Date.now() - startTime,
|
|
8
8
|
is_stream: isStream ? 1 : 0, error_message: null,
|
|
9
|
-
created_at: new Date().toISOString(),
|
|
10
|
-
|
|
9
|
+
created_at: new Date().toISOString(),
|
|
10
|
+
client_request: clientReq, upstream_request: upstreamReq,
|
|
11
11
|
upstream_response: JSON.stringify({ statusCode: status, headers: upHdrs, body: respBody }),
|
|
12
|
-
client_response: JSON.stringify({ statusCode: status, headers: cliHdrs, body: respBody }),
|
|
13
12
|
is_retry: isRetry ? 1 : 0, is_failover: isFailover ? 1 : 0, original_request_id: originalRequestId,
|
|
14
13
|
router_key_id: routerKeyId, original_model: originalModel,
|
|
15
14
|
});
|
|
@@ -27,7 +26,6 @@ export function insertRejectedLog(params) {
|
|
|
27
26
|
is_stream: isStream ? 1 : 0,
|
|
28
27
|
error_message: errorMessage,
|
|
29
28
|
created_at: new Date().toISOString(),
|
|
30
|
-
request_body: JSON.stringify(originalBody),
|
|
31
29
|
client_request: JSON.stringify({ headers: clientHeaders, body: originalBody }),
|
|
32
30
|
is_failover: isFailover ? 1 : 0,
|
|
33
31
|
original_request_id: originalRequestId,
|
|
@@ -21,6 +21,8 @@ export interface OrchestratorConfig {
|
|
|
21
21
|
isStream: boolean;
|
|
22
22
|
/** 外部生成的 tracker ID,用于 tracker.appendStreamChunk / tracker.update 等回调匹配 */
|
|
23
23
|
trackerId?: string;
|
|
24
|
+
/** Claude Code 的 session ID,从 x-claude-code-session-id 请求头获取 */
|
|
25
|
+
sessionId?: string;
|
|
24
26
|
}
|
|
25
27
|
export interface HandleContext {
|
|
26
28
|
streamTimeoutMs?: number;
|
|
@@ -9,6 +9,8 @@ import { logResilienceResult, collectTransportMetrics, handleIntercept, sanitize
|
|
|
9
9
|
import { buildUpstreamHeaders } from "./proxy-core.js";
|
|
10
10
|
import { UPSTREAM_SUCCESS, ProviderSwitchNeeded } from "./types.js";
|
|
11
11
|
import { SSEMetricsTransform } from "../metrics/sse-metrics-transform.js";
|
|
12
|
+
import { MetricsExtractor } from "../metrics/metrics-extractor.js";
|
|
13
|
+
import { updateLogStreamContent } from "../db/index.js";
|
|
12
14
|
import { callNonStream, callStream } from "./transport.js";
|
|
13
15
|
import { insertRejectedLog } from "./log-helpers.js";
|
|
14
16
|
const HTTP_ERROR_THRESHOLD = 400;
|
|
@@ -120,7 +122,9 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
|
|
|
120
122
|
deps.tracker?.update(logId, {
|
|
121
123
|
streamMetrics: {
|
|
122
124
|
inputTokens: m.input_tokens, outputTokens: m.output_tokens,
|
|
123
|
-
|
|
125
|
+
cacheReadTokens: m.cache_read_tokens,
|
|
126
|
+
ttftMs: m.ttft_ms, tokensPerSecond: m.tokens_per_second,
|
|
127
|
+
stopReason: m.stop_reason, isComplete: m.is_complete === 1,
|
|
124
128
|
},
|
|
125
129
|
});
|
|
126
130
|
},
|
|
@@ -134,6 +138,20 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
|
|
|
134
138
|
return callStream(provider, apiKey, body, cliHdrs, reply, deps.streamTimeoutMs, upstreamPath, buildUpstreamHeaders, metricsTransform, checkEarlyError);
|
|
135
139
|
}
|
|
136
140
|
const result = await callNonStream(provider, apiKey, body, cliHdrs, upstreamPath, buildUpstreamHeaders);
|
|
141
|
+
// 非流式请求:从响应体提取指标并更新 tracker
|
|
142
|
+
if (result.kind === "success") {
|
|
143
|
+
const mr = MetricsExtractor.fromNonStreamResponse(apiType, result.body);
|
|
144
|
+
if (mr) {
|
|
145
|
+
deps.tracker?.update(logId, {
|
|
146
|
+
streamMetrics: {
|
|
147
|
+
inputTokens: mr.input_tokens, outputTokens: mr.output_tokens,
|
|
148
|
+
cacheReadTokens: mr.cache_read_tokens,
|
|
149
|
+
ttftMs: mr.ttft_ms, tokensPerSecond: mr.tokens_per_second,
|
|
150
|
+
stopReason: mr.stop_reason, isComplete: mr.is_complete === 1,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
137
155
|
// 非流式响应注入模型信息标签(模型映射场景)
|
|
138
156
|
if (originalModel && result.kind === "success" && result.statusCode === UPSTREAM_SUCCESS) {
|
|
139
157
|
try {
|
|
@@ -150,13 +168,20 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
|
|
|
150
168
|
return result;
|
|
151
169
|
};
|
|
152
170
|
try {
|
|
153
|
-
const resilienceResult = await deps.orchestrator.handle(request, reply, apiType, { resolved, provider, clientModel: effectiveModel, isStream, trackerId: logId }, { retryMaxAttempts: deps.retryMaxAttempts, retryBaseDelayMs: deps.retryBaseDelayMs, isFailover, ruleMatcher: deps.matcher, transportFn });
|
|
171
|
+
const resilienceResult = await deps.orchestrator.handle(request, reply, apiType, { resolved, provider, clientModel: effectiveModel, isStream, trackerId: logId, sessionId }, { retryMaxAttempts: deps.retryMaxAttempts, retryBaseDelayMs: deps.retryBaseDelayMs, isFailover, ruleMatcher: deps.matcher, transportFn });
|
|
154
172
|
const lastLogId = logResilienceResult(deps.db, {
|
|
155
173
|
apiType, model: effectiveModel, providerId: provider.id, isStream,
|
|
156
|
-
|
|
174
|
+
clientReq, upstreamReqBase, logId, routerKeyId, originalModel,
|
|
157
175
|
failover: { isFailoverIteration, rootLogId: rootLogId },
|
|
158
176
|
}, resilienceResult.attempts, resilienceResult.result, startTime);
|
|
159
177
|
collectTransportMetrics(deps.db, apiType, resilienceResult.result, isStream, lastLogId, provider.id, resolved.backend_model, request);
|
|
178
|
+
// 流式请求:将 tracker 中累积的文本内容持久化到日志
|
|
179
|
+
if (isStream && deps.tracker) {
|
|
180
|
+
const req = deps.tracker.get(lastLogId);
|
|
181
|
+
const text = req?.streamContent?.textContent;
|
|
182
|
+
if (text)
|
|
183
|
+
updateLogStreamContent(deps.db, lastLogId, text);
|
|
184
|
+
}
|
|
160
185
|
// Failover: 单 provider 内重试已耗尽但仍失败,尝试下一个 target
|
|
161
186
|
if (isFailover && !reply.raw.headersSent) {
|
|
162
187
|
const tr = resilienceResult.result;
|
|
@@ -212,7 +237,7 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
|
|
|
212
237
|
id: logId, api_type: apiType, model: effectiveModel, provider_id: provider.id,
|
|
213
238
|
status_code: 502, latency_ms: Date.now() - startTime, is_stream: isStream ? 1 : 0,
|
|
214
239
|
error_message: errMsg || "Upstream connection failed", created_at: new Date().toISOString(),
|
|
215
|
-
|
|
240
|
+
client_request: clientReq, upstream_request: upstreamReqBase,
|
|
216
241
|
is_failover: isFailoverIteration ? 1 : 0, original_request_id: isFailoverIteration ? rootLogId : null,
|
|
217
242
|
router_key_id: routerKeyId, original_model: originalModel,
|
|
218
243
|
});
|