llm-simple-router 0.10.9 → 0.10.11
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/proxy-enhancement.js +2 -0
- package/dist/core/container.d.ts +0 -1
- package/dist/core/container.js +0 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/logs.d.ts +0 -8
- package/dist/db/logs.js +2 -23
- package/dist/db/metrics.d.ts +0 -10
- package/dist/db/metrics.js +2 -19
- package/dist/db/migrations/046_fix_is_complete_for_successful_requests.sql +10 -0
- package/dist/db/stats.d.ts +2 -0
- package/dist/db/stats.js +16 -0
- package/dist/index.js +0 -16
- package/dist/metrics/sse-metrics-transform.d.ts +2 -0
- package/dist/metrics/sse-metrics-transform.js +8 -0
- package/dist/proxy/format/registry.d.ts +2 -2
- package/dist/proxy/format/registry.js +9 -10
- package/dist/proxy/format/types.d.ts +2 -2
- package/dist/proxy/format/types.js +2 -2
- package/dist/proxy/handler/failover-loop.js +7 -8
- package/dist/proxy/routing/usage-window-tracker.d.ts +3 -5
- package/dist/proxy/routing/usage-window-tracker.js +28 -43
- package/dist/proxy/transform/message-mapper.js +22 -2
- package/dist/proxy/transform/request-bridge-responses.js +178 -12
- package/dist/proxy/transform/request-transform-responses.js +96 -17
- package/dist/proxy/transform/request-transform.js +9 -1
- package/dist/proxy/transform/response-bridge-responses.d.ts +2 -2
- package/dist/proxy/transform/response-bridge-responses.js +8 -8
- package/dist/proxy/transform/response-transform-responses.d.ts +3 -2
- package/dist/proxy/transform/response-transform-responses.js +51 -10
- package/dist/proxy/transform/response-transform.d.ts +4 -4
- package/dist/proxy/transform/response-transform.js +32 -23
- package/dist/proxy/transform/stream-ant2oa.js +10 -2
- package/dist/proxy/transform/stream-ant2resp.d.ts +6 -0
- package/dist/proxy/transform/stream-ant2resp.js +43 -13
- package/dist/proxy/transform/stream-bridge-chat2resp.d.ts +3 -0
- package/dist/proxy/transform/stream-bridge-chat2resp.js +20 -5
- package/dist/proxy/transform/stream-bridge-resp2chat.d.ts +1 -0
- package/dist/proxy/transform/stream-bridge-resp2chat.js +24 -0
- package/dist/proxy/transform/stream-oa2ant.d.ts +1 -0
- package/dist/proxy/transform/stream-oa2ant.js +11 -3
- package/dist/proxy/transform/stream-resp2ant.d.ts +1 -0
- package/dist/proxy/transform/stream-resp2ant.js +10 -3
- package/dist/proxy/transform/thinking-mapper.js +11 -1
- package/dist/proxy/transform/tool-mapper.js +10 -7
- package/dist/proxy/transform/types.d.ts +11 -0
- package/dist/proxy/transform/usage-mapper.js +18 -10
- package/dist/proxy/transport/stream.js +3 -0
- package/dist/utils/time-range.js +18 -23
- package/frontend-dist/assets/{CardContent-VHoT9rgw.js → CardContent-BinBedFB.js} +1 -1
- package/frontend-dist/assets/{CardTitle-BSx_tBcL.js → CardTitle-DYzajCM3.js} +1 -1
- package/frontend-dist/assets/{Checkbox-TieofFS-.js → Checkbox-BKbm21iJ.js} +1 -1
- package/frontend-dist/assets/{CollapsibleContent-Dnt-UfRz.js → CollapsibleContent-Dec5HJHa.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-NW8lciwZ.js → CollapsibleTrigger-DMwCWydb.js} +1 -1
- package/frontend-dist/assets/{Dashboard-BbzVyAgq.js → Dashboard-Cl-WroBl.js} +1 -1
- package/frontend-dist/assets/{Input-BRW038ir.js → Input-BM3GbnIl.js} +1 -1
- package/frontend-dist/assets/{Label-D7a61NN6.js → Label-birmlOXE.js} +1 -1
- package/frontend-dist/assets/{Login-CQtZ3Okd.js → Login-C7taA6PX.js} +1 -1
- package/frontend-dist/assets/{Logs-DYTA66To.js → Logs-CK-PZhH3.js} +1 -1
- package/frontend-dist/assets/{MappingEntryEditor-CHFARLUr.js → MappingEntryEditor-pNMmCPFo.js} +1 -1
- package/frontend-dist/assets/{ModelCard-DArtFLqF.js → ModelCard-Bq5fCmh_.js} +1 -1
- package/frontend-dist/assets/{ModelMappings-dT8omnXQ.js → ModelMappings-8Udu3uKC.js} +1 -1
- package/frontend-dist/assets/{Monitor-B5BNg5oz.js → Monitor-D8jwoxpP.js} +1 -1
- package/frontend-dist/assets/{Providers-DLd-sAsh.js → Providers-B-7RP1SK.js} +1 -1
- package/frontend-dist/assets/{ProxyEnhancement-D3s6ihsi.js → ProxyEnhancement-CePqXqlz.js} +1 -1
- package/frontend-dist/assets/{QuickSetup-BjLJXdQm.js → QuickSetup-C9s5YJIx.js} +1 -1
- package/frontend-dist/assets/{RetryRules-C5LT8jA4.js → RetryRules-T5erHKxg.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-VDfx0YUM.js → RouterKeys-CXFTloiI.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-Dvp7T0YJ.js → RovingFocusItem-4aJoL5yC.js} +1 -1
- package/frontend-dist/assets/{Schedules-CGk4VMEW.js → Schedules-XHeewD4H.js} +1 -1
- package/frontend-dist/assets/{Settings-C0Bq5Vlz.js → Settings-DAM3J0JW.js} +1 -1
- package/frontend-dist/assets/{Setup-CkKRbnLE.js → Setup-Cb9g-O9o.js} +1 -1
- package/frontend-dist/assets/{Switch-DPzewpLO.js → Switch-C7cnRCS5.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-CySCLI9s.js → TooltipTrigger-C1BCMETT.js} +1 -1
- package/frontend-dist/assets/{TransformRulesForm-BFhiXjTu.js → TransformRulesForm-Cv_Ih3In.js} +1 -1
- package/frontend-dist/assets/{UnifiedRequestDialog-COo6SGUN.js → UnifiedRequestDialog-CBIzu-3j.js} +2 -2
- package/frontend-dist/assets/{VisuallyHiddenInput-D7rPKtIA.js → VisuallyHiddenInput-C9vzGFfc.js} +1 -1
- package/frontend-dist/assets/{button-Cd7Qd3fz.js → button-DTwl8zzX.js} +2 -2
- package/frontend-dist/assets/{copy-JcgwJljq.js → copy-Mm5hFXXX.js} +1 -1
- package/frontend-dist/assets/{dialog-BZpiHm7Q.js → dialog-B8BVRE0T.js} +1 -1
- package/frontend-dist/assets/{index-D6ACTK1W.js → index-DjBiyR45.js} +2 -2
- package/frontend-dist/assets/{trash-2-CJq-sfA4.js → trash-2-Blf2lMVT.js} +1 -1
- package/frontend-dist/assets/{useClipboard-C2mKqlsb.js → useClipboard-D4oejP66.js} +1 -1
- package/frontend-dist/assets/{useLogRetention-DYo82fwB.js → useLogRetention-aAVdCW7-.js} +1 -1
- package/frontend-dist/index.html +2 -2
- package/package.json +1 -1
- package/dist/db/log-write-buffer.d.ts +0 -43
- package/dist/db/log-write-buffer.js +0 -91
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getSetting, setSetting } from "../db/settings.js";
|
|
3
|
+
import { clearEnhancementConfigCache } from "../proxy/routing/enhancement-config.js";
|
|
3
4
|
const UpdateProxyEnhancementSchema = Type.Object({
|
|
4
5
|
tool_call_loop_enabled: Type.Boolean(),
|
|
5
6
|
stream_loop_enabled: Type.Boolean(),
|
|
@@ -35,6 +36,7 @@ export const adminProxyEnhancementRoutes = (app, options, done) => {
|
|
|
35
36
|
tool_error_logging_enabled: body.tool_error_logging_enabled,
|
|
36
37
|
};
|
|
37
38
|
setSetting(db, "proxy_enhancement", JSON.stringify(config));
|
|
39
|
+
clearEnhancementConfigCache();
|
|
38
40
|
return reply.send({ success: true });
|
|
39
41
|
});
|
|
40
42
|
done();
|
package/dist/core/container.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ export declare const SERVICE_KEYS: {
|
|
|
10
10
|
readonly pluginRegistry: "pluginRegistry";
|
|
11
11
|
readonly formatRegistry: "formatRegistry";
|
|
12
12
|
readonly logFileWriter: "logFileWriter";
|
|
13
|
-
readonly logWriteBuffer: "logWriteBuffer";
|
|
14
13
|
readonly proxyAgentFactory: "proxyAgentFactory";
|
|
15
14
|
};
|
|
16
15
|
export type ServiceKey = (typeof SERVICE_KEYS)[keyof typeof SERVICE_KEYS];
|
package/dist/core/container.js
CHANGED
package/dist/db/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { getRouterKeyByHash, getAllRouterKeys, getRouterKeyById, createRouterKey
|
|
|
12
12
|
export type { RouterKey } from "./router-keys.js";
|
|
13
13
|
export { getMetricsSummary, getMetricsTimeseries, insertMetrics, getClientTypeBreakdown } from "./metrics.js";
|
|
14
14
|
export type { MetricsSummaryRow, MetricsTimeseriesRow, MetricsPeriod, MetricsMetric, MetricsRow, MetricsInsert, ClientTypeBreakdown } from "./metrics.js";
|
|
15
|
-
export { getStats } from "./stats.js";
|
|
15
|
+
export { getStats, getLatestMetricTime } from "./stats.js";
|
|
16
16
|
export type { Stats } from "./stats.js";
|
|
17
17
|
export { getSetting, setSetting, isInitialized } from "./settings.js";
|
|
18
18
|
export { getDbMaxSizeMb, setDbMaxSizeMb, getLogTableMaxSizeMb, setLogTableMaxSizeMb, } from "./settings.js";
|
package/dist/db/index.js
CHANGED
|
@@ -145,7 +145,7 @@ export { getActiveRetryRules, getAllRetryRules, getRetryRuleById, createRetryRul
|
|
|
145
145
|
export { insertRequestLog, getRequestLogs, getRequestLogById, deleteLogsBefore, getRequestLogChildren, getRequestLogsGrouped, updateLogStreamContent, updateLogClientStatus, estimateLogTableSize, deleteOldestLogs, getLogCount, updateLogPipelineSnapshot, } from "./logs.js";
|
|
146
146
|
export { getRouterKeyByHash, getAllRouterKeys, getRouterKeyById, createRouterKey, updateRouterKey, deleteRouterKey, getAvailableModels, } from "./router-keys.js";
|
|
147
147
|
export { getMetricsSummary, getMetricsTimeseries, insertMetrics, getClientTypeBreakdown } from "./metrics.js";
|
|
148
|
-
export { getStats } from "./stats.js";
|
|
148
|
+
export { getStats, getLatestMetricTime } from "./stats.js";
|
|
149
149
|
export { getSetting, setSetting, isInitialized } from "./settings.js";
|
|
150
150
|
export { getDbMaxSizeMb, setDbMaxSizeMb, getLogTableMaxSizeMb, setLogTableMaxSizeMb, } from "./settings.js";
|
|
151
151
|
export { insertWindow, getLatestWindow, getWindowsInRange, getWindowUsage, } from "./usage-windows.js";
|
package/dist/db/logs.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
2
|
import type { LogFileWriter } from "../storage/log-file-writer.js";
|
|
3
3
|
import { type RetryMatcher } from "../proxy/log-detail-policy.js";
|
|
4
|
-
import type { LogWriteBuffer } from "./log-write-buffer.js";
|
|
5
4
|
export interface RequestLog {
|
|
6
5
|
id: string;
|
|
7
6
|
api_type: string;
|
|
@@ -55,13 +54,6 @@ export interface LogWriteContext {
|
|
|
55
54
|
logFileWriter?: LogFileWriter | null;
|
|
56
55
|
responseBody?: string | null;
|
|
57
56
|
}
|
|
58
|
-
/** 初始化日志缓冲(buildApp 时调用) */
|
|
59
|
-
export declare function initLogBuffer(buffer: LogWriteBuffer): void;
|
|
60
|
-
/** 停止日志缓冲,同步 flush 剩余数据(close 时调用) */
|
|
61
|
-
export declare function stopLogBuffer(): void;
|
|
62
|
-
/** 原始 DB INSERT 逻辑(无缓冲) */
|
|
63
|
-
declare function rawInsertRequestLog(db: Database.Database, log: RequestLogInsert, writeContext?: LogWriteContext): void;
|
|
64
|
-
export { rawInsertRequestLog };
|
|
65
57
|
export declare function insertRequestLog(db: Database.Database, log: RequestLogInsert, writeContext?: LogWriteContext): void;
|
|
66
58
|
export declare function getRequestLogs(db: Database.Database, options: {
|
|
67
59
|
page: number;
|
package/dist/db/logs.js
CHANGED
|
@@ -10,20 +10,7 @@ const LOG_LIST_SELECT = `rl.id, rl.api_type, rl.model, rl.provider_id, rl.status
|
|
|
10
10
|
rm.input_tokens_estimated, rm.client_type, rm.cache_read_tokens_estimated,
|
|
11
11
|
COALESCE(p.name, rl.provider_id) AS provider_name`;
|
|
12
12
|
const LOG_LIST_JOIN = `LEFT JOIN providers p ON p.id = rl.provider_id LEFT JOIN request_metrics rm ON rm.request_log_id = rl.id`;
|
|
13
|
-
/**
|
|
14
|
-
let logBuffer = null;
|
|
15
|
-
/** 初始化日志缓冲(buildApp 时调用) */
|
|
16
|
-
export function initLogBuffer(buffer) {
|
|
17
|
-
logBuffer = buffer;
|
|
18
|
-
}
|
|
19
|
-
/** 停止日志缓冲,同步 flush 剩余数据(close 时调用) */
|
|
20
|
-
export function stopLogBuffer() {
|
|
21
|
-
if (logBuffer) {
|
|
22
|
-
logBuffer.stop();
|
|
23
|
-
logBuffer = null;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
/** 原始 DB INSERT 逻辑(无缓冲) */
|
|
13
|
+
/** DB INSERT 逻辑 */
|
|
27
14
|
function rawInsertRequestLog(db, log, writeContext) {
|
|
28
15
|
// 详情保留判定
|
|
29
16
|
const preserveDetail = shouldPreserveDetail(log.status_code, writeContext?.responseBody ?? null, writeContext?.matcher ?? null, !!writeContext?.logFileWriter);
|
|
@@ -32,8 +19,6 @@ function rawInsertRequestLog(db, log, writeContext) {
|
|
|
32
19
|
is_retry, is_failover, original_request_id, router_key_id, original_model, session_id, pipeline_snapshot)
|
|
33
20
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(log.id, log.api_type, log.model, log.provider_id, log.status_code, log.client_status_code ?? null, log.latency_ms, log.is_stream, log.error_message, log.created_at, preserveDetail ? (log.client_request ?? null) : null, preserveDetail ? (log.upstream_request ?? null) : null, preserveDetail ? (log.upstream_response ?? null) : null, log.is_retry ?? 0, log.is_failover ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null, log.session_id ?? null, log.pipeline_snapshot ?? null);
|
|
34
21
|
}
|
|
35
|
-
// 导出给 LogWriteBuffer 的原始插入函数引用
|
|
36
|
-
export { rawInsertRequestLog };
|
|
37
22
|
export function insertRequestLog(db, log, writeContext) {
|
|
38
23
|
// 文件写入:始终同步调用(WriteStream 内部异步,不阻塞事件循环)
|
|
39
24
|
if (writeContext?.logFileWriter) {
|
|
@@ -49,13 +34,7 @@ export function insertRequestLog(db, log, writeContext) {
|
|
|
49
34
|
pipeline_snapshot: log.pipeline_snapshot ?? null,
|
|
50
35
|
});
|
|
51
36
|
}
|
|
52
|
-
|
|
53
|
-
if (logBuffer) {
|
|
54
|
-
logBuffer.pushLog(log, writeContext);
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
rawInsertRequestLog(db, log, writeContext);
|
|
58
|
-
}
|
|
37
|
+
rawInsertRequestLog(db, log, writeContext);
|
|
59
38
|
}
|
|
60
39
|
function buildLogWhereClause(options, baseCondition) {
|
|
61
40
|
let where = baseCondition;
|
package/dist/db/metrics.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
|
-
import type { LogWriteBuffer } from "./log-write-buffer.js";
|
|
3
2
|
export type MetricsPeriod = "1h" | "5h" | "6h" | "24h" | "7d" | "30d";
|
|
4
3
|
export type MetricsMetric = "ttft" | "tps" | "text_tps" | "thinking_tps" | "tool_use_tps" | "non_thinking_tps" | "total_tps" | "tokens" | "cache_rate" | "request_count" | "input_tokens" | "output_tokens" | "cache_hit_tokens";
|
|
5
4
|
export interface MetricsRow {
|
|
@@ -49,15 +48,6 @@ export type MetricsInsert = {
|
|
|
49
48
|
non_thinking_tps?: number | null;
|
|
50
49
|
total_tps?: number | null;
|
|
51
50
|
};
|
|
52
|
-
/** 设置缓冲实例(由 index.ts buildApp 调用,传入与 logs.ts 共享的 buffer) */
|
|
53
|
-
export declare function setLogBuffer(buffer: LogWriteBuffer): void;
|
|
54
|
-
/** 清除缓冲引用(由 stopLogBuffer 调用) */
|
|
55
|
-
export declare function clearLogBuffer(): void;
|
|
56
|
-
/** 原始 DB INSERT 逻辑(无缓冲) */
|
|
57
|
-
declare function rawInsertMetrics(db: Database.Database, m: MetricsInsert & {
|
|
58
|
-
id: string;
|
|
59
|
-
}): void;
|
|
60
|
-
export { rawInsertMetrics };
|
|
61
51
|
export declare function insertMetrics(db: Database.Database, m: MetricsInsert): string;
|
|
62
52
|
export interface MetricsSummaryRow {
|
|
63
53
|
provider_id: string;
|
package/dist/db/metrics.js
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import { MS_PER_SECOND } from "../core/constants.js";
|
|
3
3
|
import { getCachedStmt } from "./helpers.js";
|
|
4
|
-
/**
|
|
5
|
-
let logBuffer = null;
|
|
6
|
-
/** 设置缓冲实例(由 index.ts buildApp 调用,传入与 logs.ts 共享的 buffer) */
|
|
7
|
-
export function setLogBuffer(buffer) {
|
|
8
|
-
logBuffer = buffer;
|
|
9
|
-
}
|
|
10
|
-
/** 清除缓冲引用(由 stopLogBuffer 调用) */
|
|
11
|
-
export function clearLogBuffer() {
|
|
12
|
-
logBuffer = null;
|
|
13
|
-
}
|
|
14
|
-
/** 原始 DB INSERT 逻辑(无缓冲) */
|
|
4
|
+
/** DB INSERT 逻辑 */
|
|
15
5
|
function rawInsertMetrics(db, m) {
|
|
16
6
|
getCachedStmt(db, `INSERT INTO request_metrics (id, request_log_id, provider_id, backend_model, api_type, router_key_id, status_code,
|
|
17
7
|
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, ttft_ms, total_duration_ms, tokens_per_second, stop_reason, is_complete, input_tokens_estimated,
|
|
@@ -20,16 +10,9 @@ function rawInsertMetrics(db, m) {
|
|
|
20
10
|
thinking_tps, total_tps, non_thinking_duration_ms, non_thinking_tps)
|
|
21
11
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(m.id, m.request_log_id, m.provider_id, m.backend_model, m.api_type, m.router_key_id ?? null, m.status_code ?? null, 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, m.input_tokens_estimated ?? 0, m.client_type ?? 'unknown', m.cache_read_tokens_estimated ?? 0, m.thinking_tokens ?? null, m.text_tokens ?? null, m.tool_use_tokens ?? null, m.thinking_duration_ms ?? null, m.thinking_tps ?? null, m.total_tps ?? null, m.non_thinking_duration_ms ?? null, m.non_thinking_tps ?? null);
|
|
22
12
|
}
|
|
23
|
-
// 导出给 LogWriteBuffer 的原始插入函数引用
|
|
24
|
-
export { rawInsertMetrics };
|
|
25
13
|
export function insertMetrics(db, m) {
|
|
26
14
|
const id = randomUUID();
|
|
27
|
-
|
|
28
|
-
logBuffer.pushMetrics({ ...m, id });
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
rawInsertMetrics(db, { ...m, id });
|
|
32
|
-
}
|
|
15
|
+
rawInsertMetrics(db, { ...m, id });
|
|
33
16
|
return id;
|
|
34
17
|
}
|
|
35
18
|
const PERIOD_OFFSET = {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- Fix historical request_metrics where is_complete=0 despite successful responses.
|
|
2
|
+
-- Root cause: StreamProxy.onEnd() collected metrics before SSE parser flush,
|
|
3
|
+
-- causing is_complete to always be 0 for providers using OpenAI SSE format.
|
|
4
|
+
-- Only fix records with clear success signals (status 200 + output tokens + duration).
|
|
5
|
+
UPDATE request_metrics
|
|
6
|
+
SET is_complete = 1
|
|
7
|
+
WHERE is_complete = 0
|
|
8
|
+
AND status_code = 200
|
|
9
|
+
AND output_tokens > 0
|
|
10
|
+
AND total_duration_ms > 0;
|
package/dist/db/stats.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
|
+
/** 获取指定条件下的最近一条 metric 的 created_at(用于窗口补齐定位,不限制 is_complete) */
|
|
3
|
+
export declare function getLatestMetricTime(db: Database.Database, providerId?: string, routerKeyId?: string): string | null;
|
|
2
4
|
export interface Stats {
|
|
3
5
|
totalRequests: number;
|
|
4
6
|
successRate: number;
|
package/dist/db/stats.js
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/** 获取指定条件下的最近一条 metric 的 created_at(用于窗口补齐定位,不限制 is_complete) */
|
|
2
|
+
export function getLatestMetricTime(db, providerId, routerKeyId) {
|
|
3
|
+
const conditions = [];
|
|
4
|
+
const params = [];
|
|
5
|
+
if (providerId) {
|
|
6
|
+
conditions.push("rm.provider_id = ?");
|
|
7
|
+
params.push(providerId);
|
|
8
|
+
}
|
|
9
|
+
if (routerKeyId) {
|
|
10
|
+
conditions.push("rm.router_key_id = ?");
|
|
11
|
+
params.push(routerKeyId);
|
|
12
|
+
}
|
|
13
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
14
|
+
const row = db.prepare(`SELECT rm.created_at FROM request_metrics rm ${where} ORDER BY rm.created_at DESC LIMIT 1`).get(...params);
|
|
15
|
+
return row?.created_at ?? null;
|
|
16
|
+
}
|
|
1
17
|
export function getStats(db, startTime, endTime, routerKeyId, providerId, backendModel) {
|
|
2
18
|
const conditions = [
|
|
3
19
|
"rm.is_complete = 1",
|
package/dist/index.js
CHANGED
|
@@ -5,11 +5,6 @@ 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
|
-
import { initLogBuffer, stopLogBuffer } from "./db/logs.js";
|
|
9
|
-
import { setLogBuffer, clearLogBuffer } from "./db/metrics.js";
|
|
10
|
-
import { LogWriteBuffer } from "./db/log-write-buffer.js";
|
|
11
|
-
import { rawInsertRequestLog } from "./db/logs.js";
|
|
12
|
-
import { rawInsertMetrics } from "./db/metrics.js";
|
|
13
8
|
import { HTTP_NOT_FOUND, HTTP_INTERNAL_ERROR, getProxyApiType } from "./core/constants.js";
|
|
14
9
|
import { API_CODE, apiError, isAdminApiResponse, statusToApiCode } from "./admin/api-response.js";
|
|
15
10
|
const PROVIDER_DEFAULT_QUEUE_TIMEOUT_MS = 5000;
|
|
@@ -219,14 +214,6 @@ export async function buildApp(options) {
|
|
|
219
214
|
? null
|
|
220
215
|
: new LogFileWriter(logsDir, { enabled: getDetailLogEnabled(db) });
|
|
221
216
|
container.register(SERVICE_KEYS.logFileWriter, () => logFileWriter);
|
|
222
|
-
// 日志 DB 写入缓冲(非 :memory: 模式)
|
|
223
|
-
const logWriteBuffer = isMemoryDb
|
|
224
|
-
? null
|
|
225
|
-
: new LogWriteBuffer(db, rawInsertRequestLog, rawInsertMetrics);
|
|
226
|
-
if (logWriteBuffer) {
|
|
227
|
-
initLogBuffer(logWriteBuffer);
|
|
228
|
-
setLogBuffer(logWriteBuffer);
|
|
229
|
-
}
|
|
230
217
|
// 注册 AdaptiveController(依赖已注册的 semaphoreManager)
|
|
231
218
|
container.register(SERVICE_KEYS.adaptiveController, (c) => {
|
|
232
219
|
const ac = new AdaptiveController(c.resolve(SERVICE_KEYS.semaphoreManager), app.log);
|
|
@@ -343,9 +330,6 @@ export async function buildApp(options) {
|
|
|
343
330
|
proxyAgentFactory.invalidateAll();
|
|
344
331
|
const sessionTracker = container.resolve(SERVICE_KEYS.sessionTracker);
|
|
345
332
|
sessionTracker.stop();
|
|
346
|
-
// 同步 flush DB 日志缓冲(在 flush 文件缓冲之前)
|
|
347
|
-
stopLogBuffer();
|
|
348
|
-
clearLogBuffer();
|
|
349
333
|
// Flush LogFileWriter 的 WriteStream 缓冲数据到磁盘
|
|
350
334
|
await logFileWriter?.stop();
|
|
351
335
|
// 等待活跃代理请求自然完成,超时后强制关闭所有连接。
|
|
@@ -30,6 +30,8 @@ export declare class SSEMetricsTransform extends Transform {
|
|
|
30
30
|
constructor(apiType: "openai" | "openai-responses" | "anthropic", requestStartTime: number, options?: MetricsTransformOptions);
|
|
31
31
|
_transform(chunk: Buffer, _encoding: BufferEncoding, callback: TransformCallback): void;
|
|
32
32
|
_flush(callback: TransformCallback): void;
|
|
33
|
+
/** Flush SSE parser 缓冲区并处理残余事件,确保 extractor 状态完整 */
|
|
34
|
+
flushParser(): void;
|
|
33
35
|
getExtractor(): MetricsExtractor;
|
|
34
36
|
/** 从 SSE 事件中提取内容文本,触发 onContentDelta 回调 */
|
|
35
37
|
private emitContentDelta;
|
|
@@ -54,6 +54,14 @@ export class SSEMetricsTransform extends Transform {
|
|
|
54
54
|
}
|
|
55
55
|
callback();
|
|
56
56
|
}
|
|
57
|
+
/** Flush SSE parser 缓冲区并处理残余事件,确保 extractor 状态完整 */
|
|
58
|
+
flushParser() {
|
|
59
|
+
const events = this.parser.flush();
|
|
60
|
+
for (const event of events) {
|
|
61
|
+
this.extractor.processEvent(event);
|
|
62
|
+
this.emitContentDelta(event);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
57
65
|
getExtractor() {
|
|
58
66
|
return this.extractor;
|
|
59
67
|
}
|
|
@@ -11,7 +11,7 @@ export declare class FormatRegistry {
|
|
|
11
11
|
body: Record<string, unknown>;
|
|
12
12
|
upstreamPath: string;
|
|
13
13
|
};
|
|
14
|
-
transformResponse(
|
|
15
|
-
transformError(
|
|
14
|
+
transformResponse(body: Record<string, unknown>, source: string, target: string): Record<string, unknown>;
|
|
15
|
+
transformError(body: Record<string, unknown>, source: string, target: string): string;
|
|
16
16
|
createStreamTransform(source: string, target: string, model: string): Transform | undefined;
|
|
17
17
|
}
|
|
@@ -21,26 +21,25 @@ export class FormatRegistry {
|
|
|
21
21
|
return { body, upstreamPath };
|
|
22
22
|
return { body: converter.transformRequest(body, model), upstreamPath };
|
|
23
23
|
}
|
|
24
|
-
transformResponse(
|
|
24
|
+
transformResponse(body, source, target) {
|
|
25
25
|
const converter = this.converters.get(`${source}→${target}`);
|
|
26
26
|
if (!converter)
|
|
27
|
-
return
|
|
28
|
-
return converter.transformResponse(
|
|
27
|
+
return body;
|
|
28
|
+
return converter.transformResponse(body);
|
|
29
29
|
}
|
|
30
|
-
transformError(
|
|
30
|
+
transformError(body, source, target) {
|
|
31
31
|
if (source === target)
|
|
32
|
-
return
|
|
32
|
+
return JSON.stringify(body);
|
|
33
33
|
try {
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const code = parsed.error?.code ?? parsed.code;
|
|
34
|
+
const message = body.error?.message ?? body.message ?? JSON.stringify(body);
|
|
35
|
+
const code = body.error?.code ?? body.code;
|
|
37
36
|
const targetAdapter = this.adapters.get(target);
|
|
38
37
|
if (!targetAdapter)
|
|
39
|
-
return
|
|
38
|
+
return JSON.stringify(body);
|
|
40
39
|
return JSON.stringify(targetAdapter.formatError(message, code));
|
|
41
40
|
}
|
|
42
41
|
catch {
|
|
43
|
-
return
|
|
42
|
+
return JSON.stringify(body);
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
45
|
createStreamTransform(source, target, model) {
|
|
@@ -14,7 +14,7 @@ export interface FormatConverter {
|
|
|
14
14
|
readonly sourceType: string;
|
|
15
15
|
readonly targetType: string;
|
|
16
16
|
transformRequest(body: Record<string, unknown>, model: string): Record<string, unknown>;
|
|
17
|
-
transformResponse(
|
|
17
|
+
transformResponse(body: Record<string, unknown>): Record<string, unknown>;
|
|
18
18
|
createStreamTransform(model: string): Transform;
|
|
19
19
|
}
|
|
20
20
|
/** Factory: eliminates repetitive object literal structure across 6 converters. */
|
|
@@ -22,6 +22,6 @@ export declare function createConverter(deps: {
|
|
|
22
22
|
sourceType: string;
|
|
23
23
|
targetType: string;
|
|
24
24
|
requestTransform: (body: Record<string, unknown>) => Record<string, unknown>;
|
|
25
|
-
responseTransform: (
|
|
25
|
+
responseTransform: (body: Record<string, unknown>) => Record<string, unknown>;
|
|
26
26
|
streamTransformClass: new (model: string) => Transform;
|
|
27
27
|
}): FormatConverter;
|
|
@@ -6,8 +6,8 @@ export function createConverter(deps) {
|
|
|
6
6
|
transformRequest(body) {
|
|
7
7
|
return deps.requestTransform(body);
|
|
8
8
|
},
|
|
9
|
-
transformResponse(
|
|
10
|
-
return deps.responseTransform(
|
|
9
|
+
transformResponse(body) {
|
|
10
|
+
return deps.responseTransform(body);
|
|
11
11
|
},
|
|
12
12
|
createStreamTransform(model) {
|
|
13
13
|
return new deps.streamTransformClass(model);
|
|
@@ -251,25 +251,24 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
|
|
|
251
251
|
try {
|
|
252
252
|
const parsed = JSON.parse(bodyStr);
|
|
253
253
|
if (parsed.type === "error" || parsed.error) {
|
|
254
|
-
return formatRegistry.transformError(
|
|
254
|
+
return formatRegistry.transformError(parsed, provider.api_type, ctx.apiType);
|
|
255
255
|
}
|
|
256
|
-
let transformed = formatRegistry.transformResponse(
|
|
256
|
+
let transformed = formatRegistry.transformResponse(parsed, provider.api_type, ctx.apiType);
|
|
257
257
|
if (pluginRegistry && !isStream) {
|
|
258
258
|
try {
|
|
259
|
-
const respObj = JSON.parse(transformed);
|
|
260
259
|
const respCtx = {
|
|
261
|
-
response:
|
|
260
|
+
response: transformed,
|
|
262
261
|
sourceApiType: provider.api_type,
|
|
263
262
|
targetApiType: clientApiType,
|
|
264
263
|
provider: { id: provider.id, name: provider.name, base_url: provider.base_url, api_type: provider.api_type },
|
|
265
264
|
};
|
|
266
265
|
pluginRegistry.applyBeforeResponse(respCtx);
|
|
267
266
|
pluginRegistry.applyAfterResponse(respCtx);
|
|
268
|
-
transformed =
|
|
267
|
+
transformed = respCtx.response;
|
|
269
268
|
}
|
|
270
269
|
catch { /* response hooks best-effort */ } // eslint-disable-line taste/no-silent-catch
|
|
271
270
|
}
|
|
272
|
-
return transformed;
|
|
271
|
+
return JSON.stringify(transformed);
|
|
273
272
|
}
|
|
274
273
|
catch (err) {
|
|
275
274
|
request.log.error({ err }, "responseTransform failed");
|
|
@@ -393,10 +392,10 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
|
|
|
393
392
|
return reply;
|
|
394
393
|
}
|
|
395
394
|
// 其他未知错误
|
|
396
|
-
const errMsg = e instanceof Error ? e.message :
|
|
395
|
+
const errMsg = e instanceof Error ? e.message : JSON.stringify(e);
|
|
397
396
|
request.log.debug({ logId, error: errMsg, action: "upstream_error" });
|
|
398
397
|
insertRequestLog(db, {
|
|
399
|
-
id:
|
|
398
|
+
id: randomUUID(), api_type: clientApiType,
|
|
400
399
|
model: clientModel, provider_id: provider.id,
|
|
401
400
|
status_code: UPSTREAM_ERROR_STATUS, latency_ms: Date.now() - startTime, is_stream: isStream ? 1 : 0,
|
|
402
401
|
error_message: errMsg || "Upstream connection failed", created_at: new Date().toISOString(),
|
|
@@ -2,12 +2,10 @@ import Database from "better-sqlite3";
|
|
|
2
2
|
export declare class UsageWindowTracker {
|
|
3
3
|
private db;
|
|
4
4
|
constructor(db: Database.Database);
|
|
5
|
-
/**
|
|
5
|
+
/** 请求成功后调用,按需创建新窗口(前向模式:窗口 = [now, now + 5h]) */
|
|
6
6
|
recordRequest(providerId: string, routerKeyId?: string): void;
|
|
7
|
-
/** 启动时按活跃 provider
|
|
7
|
+
/** 启动时按活跃 provider 补齐缺失的窗口(每个 provider 仅创建一个前向窗口) */
|
|
8
8
|
reconcileOnStartup(): void;
|
|
9
|
-
/** 为单个 provider
|
|
9
|
+
/** 为单个 provider 补齐窗口:无窗口时基于最新 log 创建前向窗口 */
|
|
10
10
|
private reconcileProvider;
|
|
11
|
-
/** 从 baseTime 开始,每 5h 一个窗口,直到覆盖 lastLogTime */
|
|
12
|
-
private backfillProviderWindows;
|
|
13
11
|
}
|
|
@@ -4,71 +4,56 @@ import { getAllProviders } from "../../db/providers.js";
|
|
|
4
4
|
import { toSqliteDatetime, parseSqliteDatetime as parseDate } from "../../utils/datetime.js";
|
|
5
5
|
// eslint-disable-next-line no-magic-numbers
|
|
6
6
|
const WINDOW_DURATION_MS = 5 * 3600_000;
|
|
7
|
+
const MS_PER_MINUTE = 60000;
|
|
8
|
+
// 过期判断最小间隔(毫秒),同分钟内不重复创建窗口
|
|
9
|
+
const WINDOW_GRACE_PERIOD_MS = MS_PER_MINUTE;
|
|
7
10
|
export class UsageWindowTracker {
|
|
8
11
|
db;
|
|
9
12
|
constructor(db) {
|
|
10
13
|
this.db = db;
|
|
11
14
|
}
|
|
12
|
-
/**
|
|
15
|
+
/** 请求成功后调用,按需创建新窗口(前向模式:窗口 = [now, now + 5h]) */
|
|
13
16
|
recordRequest(providerId, routerKeyId) {
|
|
14
17
|
const now = new Date();
|
|
15
18
|
const latest = getLatestWindow(this.db, routerKeyId, providerId);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
});
|
|
19
|
+
// 无窗口 → 创建。有窗口但已过期(end_time <= now)且跨分钟 → 新窗口。
|
|
20
|
+
// 同分钟内的快速调用不重复创建,避免同一分钟内的多次请求产生多个窗口。
|
|
21
|
+
if (!latest) {
|
|
22
|
+
createForwardWindow(this.db, now, routerKeyId, providerId);
|
|
23
|
+
}
|
|
24
|
+
else if (parseDate(latest.end_time) <= now
|
|
25
|
+
&& now.getTime() - parseDate(latest.end_time).getTime() >= WINDOW_GRACE_PERIOD_MS) {
|
|
26
|
+
createForwardWindow(this.db, now, routerKeyId, providerId);
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
|
-
/** 启动时按活跃 provider
|
|
29
|
+
/** 启动时按活跃 provider 补齐缺失的窗口(每个 provider 仅创建一个前向窗口) */
|
|
28
30
|
reconcileOnStartup() {
|
|
29
31
|
const providers = getAllProviders(this.db).filter((p) => p.is_active);
|
|
30
32
|
for (const provider of providers) {
|
|
31
33
|
this.reconcileProvider(provider.id);
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
|
-
/** 为单个 provider
|
|
36
|
+
/** 为单个 provider 补齐窗口:无窗口时基于最新 log 创建前向窗口 */
|
|
35
37
|
reconcileProvider(providerId) {
|
|
36
38
|
const latest = getLatestWindow(this.db, undefined, providerId);
|
|
37
|
-
|
|
38
|
-
if (!lastLog)
|
|
39
|
+
if (latest)
|
|
39
40
|
return;
|
|
40
|
-
if (!latest) {
|
|
41
|
-
const firstLog = this.db.prepare("SELECT created_at FROM request_logs WHERE provider_id = ? ORDER BY created_at ASC LIMIT 1").get(providerId);
|
|
42
|
-
if (!firstLog)
|
|
43
|
-
return;
|
|
44
|
-
const truncated = truncateToMinute(parseDate(firstLog.created_at));
|
|
45
|
-
this.backfillProviderWindows(providerId, truncated);
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
this.backfillProviderWindows(providerId, parseDate(latest.end_time));
|
|
49
|
-
}
|
|
50
|
-
/** 从 baseTime 开始,每 5h 一个窗口,直到覆盖 lastLogTime */
|
|
51
|
-
backfillProviderWindows(providerId, baseTime) {
|
|
52
41
|
const lastLog = this.db.prepare("SELECT created_at FROM request_logs WHERE provider_id = ? ORDER BY created_at DESC LIMIT 1").get(providerId);
|
|
53
42
|
if (!lastLog)
|
|
54
43
|
return;
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
while (windowStart < lastLogTime) {
|
|
58
|
-
const windowEnd = new Date(windowStart.getTime() + WINDOW_DURATION_MS);
|
|
59
|
-
insertWindow(this.db, {
|
|
60
|
-
id: randomUUID(),
|
|
61
|
-
router_key_id: null,
|
|
62
|
-
provider_id: providerId,
|
|
63
|
-
start_time: toSqliteDatetime(windowStart),
|
|
64
|
-
end_time: toSqliteDatetime(windowEnd),
|
|
65
|
-
});
|
|
66
|
-
windowStart = windowEnd;
|
|
67
|
-
}
|
|
44
|
+
const anchor = parseDate(lastLog.created_at);
|
|
45
|
+
createForwardWindow(this.db, anchor, undefined, providerId);
|
|
68
46
|
}
|
|
69
47
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
48
|
+
/** 创建 5h 前向窗口:窗口 = [start, start + 5h],start 向下取整到分钟 */
|
|
49
|
+
function createForwardWindow(db, anchor, routerKeyId, providerId) {
|
|
50
|
+
const start = new Date(Math.floor(anchor.getTime() / MS_PER_MINUTE) * MS_PER_MINUTE);
|
|
51
|
+
const end = new Date(start.getTime() + WINDOW_DURATION_MS);
|
|
52
|
+
insertWindow(db, {
|
|
53
|
+
id: randomUUID(),
|
|
54
|
+
router_key_id: routerKeyId ?? null,
|
|
55
|
+
provider_id: providerId ?? null,
|
|
56
|
+
start_time: toSqliteDatetime(start),
|
|
57
|
+
end_time: toSqliteDatetime(end),
|
|
58
|
+
});
|
|
74
59
|
}
|
|
@@ -66,6 +66,8 @@ export function convertMessagesOA2Ant(messages) {
|
|
|
66
66
|
// tool_calls → tool_use blocks
|
|
67
67
|
if (msg.tool_calls) {
|
|
68
68
|
for (const tc of msg.tool_calls) {
|
|
69
|
+
if (!tc.function)
|
|
70
|
+
continue;
|
|
69
71
|
const input = parseToolArguments(tc.function.arguments);
|
|
70
72
|
blocks.push({ type: "tool_use", id: sanitizeToolUseId(tc.id), name: tc.function.name, input });
|
|
71
73
|
}
|
|
@@ -160,10 +162,28 @@ export function convertMessagesAnt2OA(system, messages) {
|
|
|
160
162
|
if (!content?.length)
|
|
161
163
|
continue;
|
|
162
164
|
const textParts = content.filter((b) => b.type === "text");
|
|
165
|
+
const imageParts = content.filter((b) => b.type === "image");
|
|
163
166
|
const toolResults = content.filter((b) => b.type === "tool_result");
|
|
164
|
-
|
|
167
|
+
// 仅有 text → 简单字符串 content
|
|
168
|
+
if (textParts.length > 0 && imageParts.length === 0) {
|
|
165
169
|
result.push({ role: "user", content: textParts.map(b => b.text).join("") });
|
|
166
170
|
}
|
|
171
|
+
else if (textParts.length > 0 || imageParts.length > 0) {
|
|
172
|
+
// 有 image 或混合 → content part 数组
|
|
173
|
+
const parts = [];
|
|
174
|
+
for (const tb of textParts) {
|
|
175
|
+
parts.push({ type: "text", text: tb.text });
|
|
176
|
+
}
|
|
177
|
+
for (const ib of imageParts) {
|
|
178
|
+
if (ib.source.type === "base64" && ib.source.data) {
|
|
179
|
+
parts.push({ type: "image_url", image_url: { url: `data:${ib.source.media_type};base64,${ib.source.data}` } });
|
|
180
|
+
}
|
|
181
|
+
else if (ib.source.type === "url" && ib.source.url) {
|
|
182
|
+
parts.push({ type: "image_url", image_url: { url: ib.source.url } });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
result.push({ role: "user", content: parts });
|
|
186
|
+
}
|
|
167
187
|
for (const tr of toolResults) {
|
|
168
188
|
let toolCallId = tr.tool_use_id;
|
|
169
189
|
// 空 tool_use_id → 按顺序配对到预生成的 UUID
|
|
@@ -198,7 +218,7 @@ export function convertMessagesAnt2OA(system, messages) {
|
|
|
198
218
|
oaiMsg.tool_calls = toolBlocks.map((b, i) => ({
|
|
199
219
|
id: b.id || (idMap ? idMap.get(i) || randomUUID() : randomUUID()),
|
|
200
220
|
type: "function",
|
|
201
|
-
function: { name: b.name, arguments: JSON.stringify(b.input ?? {}) },
|
|
221
|
+
function: { name: b.name ?? "unknown", arguments: JSON.stringify(b.input ?? {}) },
|
|
202
222
|
}));
|
|
203
223
|
}
|
|
204
224
|
if (oaiMsg.content || oaiMsg.tool_calls) {
|