llm-simple-router 0.10.11 → 0.10.13
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/usage.js +0 -1
- package/dist/core/monitor/types.d.ts +2 -0
- package/dist/core/types.d.ts +3 -0
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/metrics.js +3 -3
- package/dist/db/migrations/047_fix_remaining_is_complete.sql +16 -0
- package/dist/db/stats.js +0 -1
- package/dist/db/usage-windows.d.ts +2 -0
- package/dist/db/usage-windows.js +4 -1
- package/dist/metrics/sse-parser.js +6 -3
- package/dist/proxy/handler/failover-loop.js +5 -3
- package/dist/proxy/orchestration/orchestrator.d.ts +3 -1
- package/dist/proxy/orchestration/orchestrator.js +1 -0
- package/dist/proxy/pipeline-snapshot.d.ts +2 -0
- package/dist/proxy/routing/mapping-resolver.js +8 -2
- package/dist/utils/mapping-reason-parser.d.ts +7 -0
- package/dist/utils/mapping-reason-parser.js +43 -0
- package/dist/utils/time-range.js +6 -2
- package/frontend-dist/assets/{CardContent-BinBedFB.js → CardContent-DfVo-N85.js} +1 -1
- package/frontend-dist/assets/{CardTitle-DYzajCM3.js → CardTitle-npwJSAlz.js} +1 -1
- package/frontend-dist/assets/{Checkbox-BKbm21iJ.js → Checkbox-Ddnzkh_i.js} +1 -1
- package/frontend-dist/assets/{CollapsibleContent-Dec5HJHa.js → CollapsibleContent-BTVazeoQ.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-DMwCWydb.js → CollapsibleTrigger-DCQeyHrt.js} +1 -1
- package/frontend-dist/assets/{Dashboard-Cl-WroBl.js → Dashboard-DjnImtwH.js} +1 -1
- package/frontend-dist/assets/{Input-BM3GbnIl.js → Input-Ey_q_5_r.js} +1 -1
- package/frontend-dist/assets/{Label-birmlOXE.js → Label-Dw5HcYsL.js} +1 -1
- package/frontend-dist/assets/{Login-C7taA6PX.js → Login-CSrfhhm9.js} +1 -1
- package/frontend-dist/assets/{Logs-CK-PZhH3.js → Logs-HR1DZs1M.js} +1 -1
- package/frontend-dist/assets/{MappingEntryEditor-pNMmCPFo.js → MappingEntryEditor-C9pgNL0Q.js} +1 -1
- package/frontend-dist/assets/{ModelCard-Bq5fCmh_.js → ModelCard-IQMwlnCm.js} +1 -1
- package/frontend-dist/assets/{ModelMappings-8Udu3uKC.js → ModelMappings-kRx-GL_7.js} +1 -1
- package/frontend-dist/assets/{Monitor-D8jwoxpP.js → Monitor-y1ofDNK7.js} +1 -1
- package/frontend-dist/assets/{Providers-B-7RP1SK.js → Providers-C1bP2PoM.js} +1 -1
- package/frontend-dist/assets/{ProxyEnhancement-CePqXqlz.js → ProxyEnhancement-DQx4coxn.js} +1 -1
- package/frontend-dist/assets/{QuickSetup-C9s5YJIx.js → QuickSetup-DHX9-CnO.js} +1 -1
- package/frontend-dist/assets/{RetryRules-T5erHKxg.js → RetryRules-zdJE0bFL.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-CXFTloiI.js → RouterKeys-CD0rI4kv.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-4aJoL5yC.js → RovingFocusItem-CFmjbm49.js} +1 -1
- package/frontend-dist/assets/{Schedules-XHeewD4H.js → Schedules-BUm3cC6w.js} +1 -1
- package/frontend-dist/assets/{Settings-DAM3J0JW.js → Settings-D7z5IRkY.js} +1 -1
- package/frontend-dist/assets/{Setup-Cb9g-O9o.js → Setup-i9inmgjB.js} +1 -1
- package/frontend-dist/assets/{Switch-C7cnRCS5.js → Switch-C9DeYAnK.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-C1BCMETT.js → TooltipTrigger-Dr6kqGSH.js} +1 -1
- package/frontend-dist/assets/{TransformRulesForm-Cv_Ih3In.js → TransformRulesForm-CyXh4jHa.js} +1 -1
- package/frontend-dist/assets/{UnifiedRequestDialog-CBIzu-3j.js → UnifiedRequestDialog-6ZRBfjko.js} +3 -3
- package/frontend-dist/assets/{VisuallyHiddenInput-C9vzGFfc.js → VisuallyHiddenInput-CwE9jREu.js} +1 -1
- package/frontend-dist/assets/{button-DTwl8zzX.js → button-C7HO6Dyb.js} +2 -2
- package/frontend-dist/assets/{copy-Mm5hFXXX.js → copy-DxwFlq2A.js} +1 -1
- package/frontend-dist/assets/{dialog-B8BVRE0T.js → dialog-BWB1aLcT.js} +1 -1
- package/frontend-dist/assets/{index-DjBiyR45.js → index-itL9--Q_.js} +2 -2
- package/frontend-dist/assets/requestDetail-3KCtYe1N.js +1 -0
- package/frontend-dist/assets/requestDetail-DZ55ph4h.js +1 -0
- package/frontend-dist/assets/{trash-2-Blf2lMVT.js → trash-2-D2SrfECO.js} +1 -1
- package/frontend-dist/assets/{useClipboard-D4oejP66.js → useClipboard-CttzUerj.js} +1 -1
- package/frontend-dist/assets/{useLogRetention-aAVdCW7-.js → useLogRetention-Dv0deAan.js} +1 -1
- package/frontend-dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend-dist/assets/requestDetail-DZltcrAt.js +0 -1
- package/frontend-dist/assets/requestDetail-NrvqHtpI.js +0 -1
package/dist/admin/usage.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MappingReason } from "../types.js";
|
|
1
2
|
/** Abstract SSE client interface (decouples from Node http ServerResponse). */
|
|
2
3
|
export interface SSEClient {
|
|
3
4
|
write(data: string): void;
|
|
@@ -35,6 +36,7 @@ export interface ActiveRequest {
|
|
|
35
36
|
streamContent?: StreamContentSnapshot;
|
|
36
37
|
clientIp?: string;
|
|
37
38
|
sessionId?: string;
|
|
39
|
+
mappingReason?: MappingReason;
|
|
38
40
|
clientRequest?: string;
|
|
39
41
|
upstreamRequest?: string;
|
|
40
42
|
completedAt?: number;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface ConcurrencyOverride {
|
|
|
21
21
|
queue_timeout_ms?: number;
|
|
22
22
|
max_queue_size?: number;
|
|
23
23
|
}
|
|
24
|
+
export type MappingReason = "direct_format" | "group_base_rule" | "group_schedule" | "fallback_provider" | "overflow_redirect" | "failover_retry";
|
|
24
25
|
export interface ResolveResult {
|
|
25
26
|
target: Target;
|
|
26
27
|
concurrency_override?: ConcurrencyOverride;
|
|
@@ -28,6 +29,8 @@ export interface ResolveResult {
|
|
|
28
29
|
targetCount: number;
|
|
29
30
|
/** 排除前的完整 target 列表,用于请求级缓存(BP-H2) */
|
|
30
31
|
allTargets?: Target[];
|
|
32
|
+
/** 映射解析原因,标识走了哪条解析路径 */
|
|
33
|
+
mappingReason: MappingReason;
|
|
31
34
|
}
|
|
32
35
|
export interface MetricsResult {
|
|
33
36
|
input_tokens: number | null;
|
package/dist/db/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ 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";
|
|
19
|
-
export { insertWindow, getLatestWindow, getWindowsInRange, getWindowUsage, } from "./usage-windows.js";
|
|
19
|
+
export { insertWindow, getLatestWindow, getLatestWindowByProvider, getWindowsInRange, getWindowUsage, } from "./usage-windows.js";
|
|
20
20
|
export type { UsageWindow, WindowUsage } from "./usage-windows.js";
|
|
21
21
|
export { getModelContextWindowOverride, getModelInfoForProvider, setModelInfoForProvider, deleteAllModelInfoForProvider, getAllModelInfo, } from "./model-info.js";
|
|
22
22
|
export type { ProviderModelInfo } from "./model-info.js";
|
package/dist/db/index.js
CHANGED
|
@@ -148,7 +148,7 @@ export { getMetricsSummary, getMetricsTimeseries, insertMetrics, getClientTypeBr
|
|
|
148
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
|
-
export { insertWindow, getLatestWindow, getWindowsInRange, getWindowUsage, } from "./usage-windows.js";
|
|
151
|
+
export { insertWindow, getLatestWindow, getLatestWindowByProvider, getWindowsInRange, getWindowUsage, } from "./usage-windows.js";
|
|
152
152
|
export { getModelContextWindowOverride, getModelInfoForProvider, setModelInfoForProvider, deleteAllModelInfoForProvider, getAllModelInfo, } from "./model-info.js";
|
|
153
153
|
export { getSchedulesByGroup, getActiveSchedulesForGroup, getScheduleById, getAllSchedules, createSchedule, updateSchedule, deleteSchedule, deleteSchedulesByGroup, } from "./schedules.js";
|
|
154
154
|
export { collectDbSizeInfo, runSizeBasedCleanup, scheduleDbSizeMonitor, } from "./db-size-monitor.js";
|
package/dist/db/metrics.js
CHANGED
|
@@ -54,7 +54,7 @@ function buildTimeCondition(period, startTime, endTime) {
|
|
|
54
54
|
}
|
|
55
55
|
export function getMetricsSummary(db, period, providerId, backendModel, routerKeyId, startTime, endTime, clientType) {
|
|
56
56
|
const { timeWhere, timeParams } = buildTimeCondition(period, startTime, endTime);
|
|
57
|
-
const conditions = [
|
|
57
|
+
const conditions = [timeWhere];
|
|
58
58
|
const params = [...timeParams];
|
|
59
59
|
const joins = ["LEFT JOIN providers p ON p.id = rm.provider_id"];
|
|
60
60
|
if (providerId) {
|
|
@@ -89,7 +89,7 @@ export function getMetricsSummary(db, period, providerId, backendModel, routerKe
|
|
|
89
89
|
}
|
|
90
90
|
export function getClientTypeBreakdown(db, period, providerId, backendModel, routerKeyId, startTime, endTime) {
|
|
91
91
|
const { timeWhere, timeParams } = buildTimeCondition(period, startTime, endTime);
|
|
92
|
-
const conditions = [
|
|
92
|
+
const conditions = [timeWhere];
|
|
93
93
|
const params = [...timeParams];
|
|
94
94
|
if (providerId) {
|
|
95
95
|
conditions.push("rm.provider_id = ?");
|
|
@@ -135,7 +135,7 @@ export function getMetricsTimeseries(db, period, metric, providerId, backendMode
|
|
|
135
135
|
? calcBucketSec((new Date(endTime).getTime() - new Date(startTime).getTime()) / MS_PER_SECOND)
|
|
136
136
|
: calcBucketSec(PERIOD_TOTAL_SEC[period]);
|
|
137
137
|
const { timeWhere, timeParams } = buildTimeCondition(period, startTime, endTime);
|
|
138
|
-
const conditions = [
|
|
138
|
+
const conditions = [timeWhere];
|
|
139
139
|
const params = [...timeParams];
|
|
140
140
|
if (providerId) {
|
|
141
141
|
conditions.push("rm.provider_id = ?");
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- Fix remaining historical request_metrics where is_complete=0 despite
|
|
2
|
+
-- successful HTTP responses (status_code=200).
|
|
3
|
+
--
|
|
4
|
+
-- Migration 046 already fixed records with output_tokens>0 AND total_duration_ms>0,
|
|
5
|
+
-- but missed:
|
|
6
|
+
-- 1. Records created after migration 046 ran (the root cause: SSE parser swallowed
|
|
7
|
+
-- [DONE] events, so metrics extractor never set complete=true)
|
|
8
|
+
-- 2. Records with status_code=200 but null/zero tokens or duration (e.g. empty
|
|
9
|
+
-- responses, or streams where [DONE] was the only event)
|
|
10
|
+
--
|
|
11
|
+
-- Since the dashboard no longer filters by is_complete (see dashboard query fixes),
|
|
12
|
+
-- this is primarily a data integrity fix.
|
|
13
|
+
UPDATE request_metrics
|
|
14
|
+
SET is_complete = 1
|
|
15
|
+
WHERE is_complete = 0
|
|
16
|
+
AND status_code = 200;
|
package/dist/db/stats.js
CHANGED
|
@@ -16,7 +16,6 @@ export function getLatestMetricTime(db, providerId, routerKeyId) {
|
|
|
16
16
|
}
|
|
17
17
|
export function getStats(db, startTime, endTime, routerKeyId, providerId, backendModel) {
|
|
18
18
|
const conditions = [
|
|
19
|
-
"rm.is_complete = 1",
|
|
20
19
|
"rm.created_at >= datetime(?)",
|
|
21
20
|
"rm.created_at < datetime(?)",
|
|
22
21
|
];
|
|
@@ -14,6 +14,8 @@ export interface WindowUsage {
|
|
|
14
14
|
}
|
|
15
15
|
export declare function insertWindow(db: Database.Database, w: Omit<UsageWindow, "created_at">): string;
|
|
16
16
|
export declare function getLatestWindow(db: Database.Database, routerKeyId?: string, providerId?: string): UsageWindow | null;
|
|
17
|
+
/** 获取指定 provider 的最新窗口,忽略 router_key_id 过滤。当 dashboard 等调用方不知道 router_key_id 时使用。 */
|
|
18
|
+
export declare function getLatestWindowByProvider(db: Database.Database, providerId: string): UsageWindow | null;
|
|
17
19
|
/** 返回与 [start, end) 区间有重叠的窗口。可选参数不传表示不过滤该维度(与 getLatestWindow 的 IS NULL 语义不同) */
|
|
18
20
|
export declare function getWindowsInRange(db: Database.Database, start: string, end: string, routerKeyId?: string, providerId?: string): UsageWindow[];
|
|
19
21
|
/** 聚合指定时间窗口内的请求计数和 token 用量 */
|
package/dist/db/usage-windows.js
CHANGED
|
@@ -24,6 +24,10 @@ export function getLatestWindow(db, routerKeyId, providerId) {
|
|
|
24
24
|
const sql = `SELECT * FROM usage_windows WHERE ${conditions.join(" AND ")} ORDER BY start_time DESC LIMIT 1`;
|
|
25
25
|
return db.prepare(sql).get(...params) ?? null;
|
|
26
26
|
}
|
|
27
|
+
/** 获取指定 provider 的最新窗口,忽略 router_key_id 过滤。当 dashboard 等调用方不知道 router_key_id 时使用。 */
|
|
28
|
+
export function getLatestWindowByProvider(db, providerId) {
|
|
29
|
+
return db.prepare("SELECT * FROM usage_windows WHERE provider_id = ? ORDER BY start_time DESC LIMIT 1").get(providerId) ?? null;
|
|
30
|
+
}
|
|
27
31
|
/** 返回与 [start, end) 区间有重叠的窗口。可选参数不传表示不过滤该维度(与 getLatestWindow 的 IS NULL 语义不同) */
|
|
28
32
|
export function getWindowsInRange(db, start, end, routerKeyId, providerId) {
|
|
29
33
|
const conditions = ["start_time < ?", "end_time > ?"];
|
|
@@ -41,7 +45,6 @@ export function getWindowsInRange(db, start, end, routerKeyId, providerId) {
|
|
|
41
45
|
/** 聚合指定时间窗口内的请求计数和 token 用量 */
|
|
42
46
|
export function getWindowUsage(db, startTime, endTime, routerKeyId, providerId) {
|
|
43
47
|
const conditions = [
|
|
44
|
-
"rm.is_complete = 1",
|
|
45
48
|
"rm.created_at >= datetime(?)",
|
|
46
49
|
"rm.created_at < datetime(?)",
|
|
47
50
|
];
|
|
@@ -61,12 +61,15 @@ export class SSEParser {
|
|
|
61
61
|
}
|
|
62
62
|
else if (line.startsWith("data:")) {
|
|
63
63
|
const value = this.extractFieldValue(line);
|
|
64
|
-
// [DONE]
|
|
64
|
+
// [DONE] 是流结束信号,标记 isDone 阻止后续解析,同时作为普通事件返回
|
|
65
|
+
// 让 metrics extractor 的 processOpenAIEvent 能收到并设置 complete = true
|
|
65
66
|
if (value === "[DONE]") {
|
|
66
67
|
this.isDone = true;
|
|
67
|
-
|
|
68
|
+
dataLines.push(value);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
dataLines.push(value);
|
|
68
72
|
}
|
|
69
|
-
dataLines.push(value);
|
|
70
73
|
}
|
|
71
74
|
// 其他 field(id:, retry:, etc.)按 SSE 规范忽略
|
|
72
75
|
}
|
|
@@ -150,7 +150,7 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
|
|
|
150
150
|
if (cachedTargets) {
|
|
151
151
|
const filtered = filterExcluded(cachedTargets, excludeTargets);
|
|
152
152
|
resolveResult = filtered.length > 0
|
|
153
|
-
? { target: filtered[0], concurrency_override: cachedConcurrencyOverride, targetCount: cachedTargets.length }
|
|
153
|
+
? { target: filtered[0], concurrency_override: cachedConcurrencyOverride, targetCount: cachedTargets.length, mappingReason: "failover_retry" }
|
|
154
154
|
: null;
|
|
155
155
|
}
|
|
156
156
|
else {
|
|
@@ -191,6 +191,7 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
// --- 溢出重定向 ---
|
|
194
|
+
let effectiveMappingReason = resolveResult.mappingReason;
|
|
194
195
|
const overflowResult = applyOverflowRedirect(resolved, db, currentBody);
|
|
195
196
|
if (overflowResult) {
|
|
196
197
|
const overflowProvider = getProviderById(db, overflowResult.provider_id);
|
|
@@ -198,6 +199,7 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
|
|
|
198
199
|
resolved = { ...resolved, provider_id: overflowResult.provider_id, backend_model: overflowResult.backend_model };
|
|
199
200
|
provider = overflowProvider;
|
|
200
201
|
currentBody = { ...currentBody, model: overflowResult.backend_model };
|
|
202
|
+
effectiveMappingReason = "overflow_redirect";
|
|
201
203
|
}
|
|
202
204
|
}
|
|
203
205
|
// 当前迭代的工具错误刷新闭包(统一 6 处调用)
|
|
@@ -210,7 +212,7 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
|
|
|
210
212
|
const needsTransform = resolvedPath.needsTransform;
|
|
211
213
|
// --- routing ---
|
|
212
214
|
currentBody = { ...currentBody, model: resolved.backend_model };
|
|
213
|
-
iterationSnapshot.add({ stage: "routing", client_model: clientModel, backend_model: resolved.backend_model, provider_id: resolved.provider_id, strategy: resolveResult.targetCount > 1 ? "failover" : "scheduled" });
|
|
215
|
+
iterationSnapshot.add({ stage: "routing", client_model: clientModel, backend_model: resolved.backend_model, provider_id: resolved.provider_id, strategy: resolveResult.targetCount > 1 ? "failover" : "scheduled", mapping_reason: effectiveMappingReason });
|
|
214
216
|
iterationSnapshot.add({ stage: "overflow", triggered: overflowResult != null });
|
|
215
217
|
// --- Plugin 调整 body 和 headers ---
|
|
216
218
|
const pluginResult = applyPluginAdjustments(pluginRegistry, currentBody, clientApiType, provider);
|
|
@@ -289,7 +291,7 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
|
|
|
289
291
|
const pipelineSnapshot = iterationSnapshot.toJSON();
|
|
290
292
|
// --- Execute through orchestrator ---
|
|
291
293
|
try {
|
|
292
|
-
const resilienceResult = await orchestrator.handle(request, reply, clientApiType, { resolved, provider, clientModel, isStream, trackerId: logId, sessionId: ctx.metadata.get("session_id"), clientRequest: clientReq, upstreamRequest: upstreamReqBase, concurrencyOverride }, { retryBaseDelayMs: config.RETRY_BASE_DELAY_MS, isFailover, ruleMatcher: matcher, transportFn });
|
|
294
|
+
const resilienceResult = await orchestrator.handle(request, reply, clientApiType, { resolved, provider, clientModel, isStream, trackerId: logId, sessionId: ctx.metadata.get("session_id"), clientRequest: clientReq, upstreamRequest: upstreamReqBase, concurrencyOverride, mappingReason: effectiveMappingReason }, { retryBaseDelayMs: config.RETRY_BASE_DELAY_MS, isFailover, ruleMatcher: matcher, transportFn });
|
|
293
295
|
// 日志记录
|
|
294
296
|
const lastLogId = logResilienceResult(db, {
|
|
295
297
|
apiType: clientApiType,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
2
|
import type { TransportResult } from "../types.js";
|
|
3
|
-
import type { Target, ConcurrencyOverride } from "../../core/types.js";
|
|
3
|
+
import type { Target, ConcurrencyOverride, MappingReason } from "../../core/types.js";
|
|
4
4
|
import type { ResilienceLayer, ResilienceResult } from "./resilience.js";
|
|
5
5
|
import type { RetryRuleMatcher } from "./retry-rules.js";
|
|
6
6
|
import type { SemaphoreScope } from "./scope.js";
|
|
@@ -30,6 +30,8 @@ export interface OrchestratorConfig {
|
|
|
30
30
|
upstreamRequest?: string;
|
|
31
31
|
/** Schedule 层的并发覆盖配置,覆盖 Provider 默认并发限制 */
|
|
32
32
|
concurrencyOverride?: ConcurrencyOverride;
|
|
33
|
+
/** 映射解析原因 */
|
|
34
|
+
mappingReason?: MappingReason;
|
|
33
35
|
}
|
|
34
36
|
export interface HandleContext {
|
|
35
37
|
streamTimeoutMs?: number;
|
|
@@ -12,6 +12,7 @@ export type StageRecord = {
|
|
|
12
12
|
backend_model: string;
|
|
13
13
|
provider_id: string;
|
|
14
14
|
strategy: string;
|
|
15
|
+
mapping_reason?: MappingReason;
|
|
15
16
|
} | {
|
|
16
17
|
stage: "overflow";
|
|
17
18
|
triggered: boolean;
|
|
@@ -21,6 +22,7 @@ export type StageRecord = {
|
|
|
21
22
|
stage: "provider_patch";
|
|
22
23
|
types: string[];
|
|
23
24
|
};
|
|
25
|
+
import type { MappingReason } from "../core/types.js";
|
|
24
26
|
export declare class PipelineSnapshot {
|
|
25
27
|
private readonly stages;
|
|
26
28
|
constructor(initial?: StageRecord[]);
|
|
@@ -118,7 +118,7 @@ export function resolveMapping(db, clientModel, context) {
|
|
|
118
118
|
if (provider) {
|
|
119
119
|
const modelEntries = parseModels(provider.models);
|
|
120
120
|
if (modelEntries.some(m => m.name === backendModel)) {
|
|
121
|
-
return { target: { backend_model: backendModel, provider_id: provider.id }, targetCount: 1 };
|
|
121
|
+
return { target: { backend_model: backendModel, provider_id: provider.id }, targetCount: 1, mappingReason: "direct_format" };
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
return null;
|
|
@@ -131,7 +131,7 @@ export function resolveMapping(db, clientModel, context) {
|
|
|
131
131
|
for (const p of providers) {
|
|
132
132
|
const modelEntries = parseModels(p.models);
|
|
133
133
|
if (modelEntries.some(m => m.name === clientModel)) {
|
|
134
|
-
return { target: { backend_model: clientModel, provider_id: p.id }, targetCount: 1 };
|
|
134
|
+
return { target: { backend_model: clientModel, provider_id: p.id }, targetCount: 1, mappingReason: "fallback_provider" };
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
return null;
|
|
@@ -158,10 +158,15 @@ export function resolveMapping(db, clientModel, context) {
|
|
|
158
158
|
// 5. 确定使用的 targets:schedule 优先,否则 base
|
|
159
159
|
let activeTargets = baseTargets;
|
|
160
160
|
let concurrencyOverride;
|
|
161
|
+
let mappingReason = "group_base_rule";
|
|
161
162
|
if (matchedSchedule) {
|
|
162
163
|
const scheduleTargets = parseScheduleTargets(matchedSchedule.mapping_rule);
|
|
164
|
+
// schedule 命中但 targets 解析失败时,activeTargets 仍为 base targets
|
|
165
|
+
// mappingReason 保持 group_base_rule(实际使用的 targets 来源)
|
|
166
|
+
// concurrencyOverride 来自 schedule(有意保留,schedule 控制并发上限)
|
|
163
167
|
if (scheduleTargets.length > 0) {
|
|
164
168
|
activeTargets = scheduleTargets;
|
|
169
|
+
mappingReason = "group_schedule";
|
|
165
170
|
}
|
|
166
171
|
concurrencyOverride = parseConcurrencyRule(matchedSchedule.concurrency_rule);
|
|
167
172
|
}
|
|
@@ -174,5 +179,6 @@ export function resolveMapping(db, clientModel, context) {
|
|
|
174
179
|
concurrency_override: concurrencyOverride,
|
|
175
180
|
targetCount: activeTargets.length,
|
|
176
181
|
allTargets: activeTargets,
|
|
182
|
+
mappingReason,
|
|
177
183
|
};
|
|
178
184
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从 pipeline_snapshot JSON 中提取映射原因。
|
|
3
|
+
* 优先检查 overflow stage(triggered === true → "overflow_redirect"),
|
|
4
|
+
* 否则取 routing stage 的 mapping_reason 字段。
|
|
5
|
+
* 只返回白名单中的值,避免未知的 mapping_reason 导致 i18n key 断裂。
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseMappingReason(snapshot: string | null | undefined): string | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const KNOWN_MAPPING_REASONS = new Set([
|
|
2
|
+
"direct_format",
|
|
3
|
+
"group_base_rule",
|
|
4
|
+
"group_schedule",
|
|
5
|
+
"fallback_provider",
|
|
6
|
+
"overflow_redirect",
|
|
7
|
+
"failover_retry",
|
|
8
|
+
]);
|
|
9
|
+
/**
|
|
10
|
+
* 从 pipeline_snapshot JSON 中提取映射原因。
|
|
11
|
+
* 优先检查 overflow stage(triggered === true → "overflow_redirect"),
|
|
12
|
+
* 否则取 routing stage 的 mapping_reason 字段。
|
|
13
|
+
* 只返回白名单中的值,避免未知的 mapping_reason 导致 i18n key 断裂。
|
|
14
|
+
*/
|
|
15
|
+
export function parseMappingReason(snapshot) {
|
|
16
|
+
if (!snapshot)
|
|
17
|
+
return undefined;
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(snapshot);
|
|
20
|
+
const stages = Array.isArray(parsed)
|
|
21
|
+
? parsed
|
|
22
|
+
: [];
|
|
23
|
+
if (stages.length === 0)
|
|
24
|
+
return undefined;
|
|
25
|
+
for (const stage of stages) {
|
|
26
|
+
if (stage.stage === "overflow" && stage.triggered === true) {
|
|
27
|
+
return "overflow_redirect";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const stage of stages) {
|
|
31
|
+
if (stage.stage === "routing" &&
|
|
32
|
+
typeof stage.mapping_reason === "string") {
|
|
33
|
+
return KNOWN_MAPPING_REASONS.has(stage.mapping_reason)
|
|
34
|
+
? stage.mapping_reason
|
|
35
|
+
: undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
package/dist/utils/time-range.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getLatestWindow } from "../db/usage-windows.js";
|
|
1
|
+
import { getLatestWindow, getLatestWindowByProvider } from "../db/usage-windows.js";
|
|
2
2
|
import { toSqliteDatetime, parseSqliteDatetime } from "./datetime.js";
|
|
3
3
|
import { getLatestMetricTime } from "../db/stats.js";
|
|
4
4
|
const WINDOW_HOURS = 5;
|
|
@@ -14,7 +14,11 @@ export function resolveTimeRange(period, db, routerKeyId, providerId) {
|
|
|
14
14
|
const now = new Date();
|
|
15
15
|
switch (period) {
|
|
16
16
|
case "window": {
|
|
17
|
-
|
|
17
|
+
// 有 providerId 但无 routerKeyId 时,忽略 router_key_id 查找最新窗口
|
|
18
|
+
// (dashboard 等调用方不知道 router_key_id 时,也能匹配到实际窗口)
|
|
19
|
+
const latest = providerId && !routerKeyId
|
|
20
|
+
? getLatestWindowByProvider(db, providerId)
|
|
21
|
+
: getLatestWindow(db, routerKeyId, providerId);
|
|
18
22
|
if (latest && now <= parseSqliteDatetime(latest.end_time)) {
|
|
19
23
|
// 有未过期窗口 → 直接使用窗口范围
|
|
20
24
|
return { startTime: latest.start_time, endTime: latest.end_time };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Ut as e,Vt as t,Z as n,et as r,ft as i,ht as a,r as o}from"./button-
|
|
1
|
+
import{Ut as e,Vt as t,Z as n,et as r,ft as i,ht as a,r as o}from"./button-C7HO6Dyb.js";var s=[`data-size`],c=r({__name:`Card`,props:{class:{type:[Boolean,null,String,Object,Array]},size:{default:`default`}},setup(r){let c=r;return(l,u)=>(i(),n(`div`,{"data-slot":`card`,"data-size":r.size,class:e(t(o)(`ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-lg py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg group/card flex flex-col`,c.class))},[a(l.$slots,`default`)],10,s))}}),l=r({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(r){let s=r;return(r,c)=>(i(),n(`div`,{"data-slot":`card-content`,class:e(t(o)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[a(r.$slots,`default`)],2))}});export{c as n,l as t};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Ut as e,Vt as t,Z as n,et as r,ft as i,ht as a,r as o}from"./button-
|
|
1
|
+
import{Ut as e,Vt as t,Z as n,et as r,ft as i,ht as a,r as o}from"./button-C7HO6Dyb.js";var s=r({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(r){let s=r;return(r,c)=>(i(),n(`div`,{"data-slot":`card-header`,class:e(t(o)(`gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]`,s.class))},[a(r.$slots,`default`)],2))}}),c=r({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(r){let s=r;return(r,c)=>(i(),n(`div`,{"data-slot":`card-title`,class:e(t(o)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[a(r.$slots,`default`)],2))}});export{s as n,c as t};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{$ as e,Tt as t,U as n,Vt as r,W as i,Wt as a,X as o,Y as s,_t as c,et as l,ft as u,ht as d,i as f,m as p,nt as m,o as h,q as g,r as _,st as v,x as y}from"./button-
|
|
1
|
+
import{$ as e,Tt as t,U as n,Vt as r,W as i,Wt as a,X as o,Y as s,_t as c,et as l,ft as u,ht as d,i as f,m as p,nt as m,o as h,q as g,r as _,st as v,x as y}from"./button-C7HO6Dyb.js";import{t as b}from"./VisuallyHiddenInput-CwE9jREu.js";import{t as x}from"./RovingFocusItem-CFmjbm49.js";import{B as S,G as C,H as w,L as T,Y as E,q as D,ut as O}from"./index-itL9--Q_.js";function k(e,t){return C(e)?!1:Array.isArray(e)?e.some(e=>E(e,t)):E(e,t)}var[A,j]=D(`CheckboxGroupRoot`);function M(e){return e===`indeterminate`}function N(e){return M(e)?`indeterminate`:e?`checked`:`unchecked`}var[P,F]=D(`CheckboxRoot`),I=l({inheritAttrs:!1,__name:`CheckboxRoot`,props:{defaultValue:{type:null,required:!1},modelValue:{type:null,required:!1,default:void 0},disabled:{type:Boolean,required:!1},value:{type:null,required:!1,default:`on`},id:{type:String,required:!1},trueValue:{type:null,required:!1,default:()=>!0},falseValue:{type:null,required:!1,default:()=>!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`},name:{type:String,required:!1},required:{type:Boolean,required:!1}},emits:[`update:modelValue`],setup(e,{emit:a}){let l=e,m=a,{forwardRef:_,currentElement:y}=h(),S=A(null),T=p(l,`modelValue`,m,{defaultValue:l.defaultValue??l.falseValue,passive:l.modelValue===void 0}),D=g(()=>S?.disabled.value||l.disabled),O=g(()=>E(T.value,l.trueValue)),j=g(()=>C(S?.modelValue.value)?T.value===`indeterminate`?`indeterminate`:O.value:k(S.modelValue.value,l.value));function P(){if(C(S?.modelValue.value))T.value===`indeterminate`?T.value=l.trueValue:T.value=O.value?l.falseValue:l.trueValue;else{let e=[...S.modelValue.value||[]];if(k(e,l.value)){let t=e.findIndex(e=>E(e,l.value));e.splice(t,1)}else e.push(l.value);S.modelValue.value=e}}let I=w(y),L=g(()=>l.id&&y.value?document.querySelector(`[for="${l.id}"]`)?.innerText:void 0);return F({disabled:D,state:j}),(e,a)=>(u(),s(c(r(S)?.rovingFocus.value?r(x):r(f)),v(e.$attrs,{id:e.id,ref:r(_),role:`checkbox`,"as-child":e.asChild,as:e.as,type:e.as===`button`?`button`:void 0,"aria-checked":r(M)(j.value)?`mixed`:j.value,"aria-required":e.required,"aria-label":e.$attrs[`aria-label`]||L.value,"data-state":r(N)(j.value),"data-disabled":D.value?``:void 0,disabled:D.value,focusable:r(S)?.rovingFocus.value?!D.value:void 0,onKeydown:n(i(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:t(()=>[d(e.$slots,`default`,{modelValue:r(T),state:j.value}),r(I)&&e.name&&!r(S)?(u(),s(r(b),{key:0,type:`checkbox`,checked:!!j.value,name:e.name,value:e.value,disabled:D.value,required:e.required},null,8,[`checked`,`name`,`value`,`disabled`,`required`])):o(`v-if`,!0)]),_:3},16,[`id`,`as-child`,`as`,`type`,`aria-checked`,`aria-required`,`aria-label`,`data-state`,`data-disabled`,`disabled`,`focusable`,`onKeydown`]))}}),L=l({__name:`CheckboxIndicator`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`span`}},setup(n){let{forwardRef:i}=h(),a=P();return(n,o)=>(u(),s(r(T),{present:n.forceMount||r(M)(r(a).state.value)||r(a).state.value===!0},{default:t(()=>[e(r(f),v({ref:r(i),"data-state":r(N)(r(a).state.value),"data-disabled":r(a).disabled.value?``:void 0,style:{pointerEvents:`none`},"as-child":n.asChild,as:n.as},n.$attrs),{default:t(()=>[d(n.$slots,`default`)]),_:3},16,[`data-state`,`data-disabled`,`as-child`,`as`])]),_:3},8,[`present`]))}}),R=l({__name:`Checkbox`,props:{defaultValue:{},modelValue:{},disabled:{type:Boolean},value:{},id:{},trueValue:{},falseValue:{},asChild:{type:Boolean},as:{},name:{},required:{type:Boolean},class:{type:[Boolean,null,String,Object,Array]}},emits:[`update:modelValue`],setup(n,{emit:i}){let o=n,c=i,l=S(y(o,`class`),c);return(n,i)=>(u(),s(r(I),v({"data-slot":`checkbox`},r(l),{class:r(_)(`border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-md border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50`,o.class)}),{default:t(i=>[e(r(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:t(()=>[d(n.$slots,`default`,a(m(i)),()=>[e(r(O))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};
|
package/frontend-dist/assets/{CollapsibleContent-Dec5HJHa.js → CollapsibleContent-BTVazeoQ.js}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{$ as e,Nt as t,Rt as n,St as r,Tt as i,Vt as a,Wt as o,X as s,Y as c,ct as l,d as u,et as d,ft as f,ht as p,i as m,m as h,nt as g,o as _,q as v,st as y,ut as b}from"./button-
|
|
1
|
+
import{$ as e,Nt as t,Rt as n,St as r,Tt as i,Vt as a,Wt as o,X as s,Y as c,ct as l,d as u,et as d,ft as f,ht as p,i as m,m as h,nt as g,o as _,q as v,st as y,ut as b}from"./button-C7HO6Dyb.js";import{B as x,L as S,q as C,z as w}from"./index-itL9--Q_.js";var[T,E]=C(`CollapsibleRoot`),D=d({__name:`CollapsibleRoot`,props:{defaultOpen:{type:Boolean,required:!1,default:!1},open:{type:Boolean,required:!1,default:void 0},disabled:{type:Boolean,required:!1},unmountOnHide:{type:Boolean,required:!1,default:!0},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`update:open`],setup(e,{expose:t,emit:r}){let o=e,s=h(o,`open`,r,{defaultValue:o.defaultOpen,passive:o.open===void 0}),{disabled:l,unmountOnHide:u}=n(o);return E({contentId:``,disabled:l,open:s,unmountOnHide:u,onOpenToggle:()=>{l.value||(s.value=!s.value)}}),t({open:s}),_(),(e,t)=>(f(),c(a(m),{as:e.as,"as-child":o.asChild,"data-state":a(s)?`open`:`closed`,"data-disabled":a(l)?``:void 0},{default:i(()=>[p(e.$slots,`default`,{open:a(s)})]),_:3},8,[`as`,`as-child`,`data-state`,`data-disabled`]))}}),O=d({inheritAttrs:!1,__name:`CollapsibleContent`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`contentFound`],setup(n,{emit:o}){let d=n,h=o,g=T();g.contentId||=w(void 0,`reka-collapsible-content`);let x=t(),{forwardRef:C,currentElement:E}=_(),D=t(0),O=t(0),k=v(()=>g.open.value),A=t(k.value),j=t();r(()=>[k.value,x.value?.present],async()=>{await l();let e=E.value;if(!e)return;j.value=j.value||{transitionDuration:e.style.transitionDuration,animationName:e.style.animationName},e.style.transitionDuration=`0s`,e.style.animationName=`none`;let t=e.getBoundingClientRect();O.value=t.height,D.value=t.width,A.value||(e.style.transitionDuration=j.value.transitionDuration,e.style.animationName=j.value.animationName)},{immediate:!0});let M=v(()=>A.value&&g.open.value);return b(()=>{requestAnimationFrame(()=>{A.value=!1})}),u(E,`beforematch`,e=>{requestAnimationFrame(()=>{g.onOpenToggle(),h(`contentFound`)})}),(t,n)=>(f(),c(a(S),{ref_key:`presentRef`,ref:x,present:t.forceMount||a(g).open.value,"force-mount":!0},{default:i(({present:n})=>[e(a(m),y(t.$attrs,{id:a(g).contentId,ref:a(C),"as-child":d.asChild,as:t.as,hidden:n?void 0:a(g).unmountOnHide.value?``:`until-found`,"data-state":M.value?void 0:a(g).open.value?`open`:`closed`,"data-disabled":a(g).disabled?.value?``:void 0,style:{"--reka-collapsible-content-height":`${O.value}px`,"--reka-collapsible-content-width":`${D.value}px`}}),{default:i(()=>[!a(g).unmountOnHide.value||n?p(t.$slots,`default`,{key:0}):s(`v-if`,!0)]),_:2},1040,[`id`,`as-child`,`as`,`hidden`,`data-state`,`data-disabled`,`style`])]),_:3},8,[`present`]))}}),k=d({__name:`Collapsible`,props:{defaultOpen:{type:Boolean},open:{type:Boolean},disabled:{type:Boolean},unmountOnHide:{type:Boolean},asChild:{type:Boolean},as:{}},emits:[`update:open`],setup(e,{emit:t}){let n=x(e,t);return(e,t)=>(f(),c(a(D),y({"data-slot":`collapsible`},a(n)),{default:i(t=>[p(e.$slots,`default`,o(g(t)))]),_:3},16))}}),A=d({__name:`CollapsibleContent`,props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{}},setup(e){let t=e;return(e,n)=>(f(),c(a(O),y({"data-slot":`collapsible-content`},t),{default:i(()=>[p(e.$slots,`default`)]),_:3},16))}});export{k as n,T as r,A as t};
|
package/frontend-dist/assets/{CollapsibleTrigger-DMwCWydb.js → CollapsibleTrigger-DCQeyHrt.js}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Tt as e,Vt as t,Y as n,et as r,ft as i,ht as a,i as o,o as s,st as c}from"./button-
|
|
1
|
+
import{Tt as e,Vt as t,Y as n,et as r,ft as i,ht as a,i as o,o as s,st as c}from"./button-C7HO6Dyb.js";import{r as l}from"./CollapsibleContent-BTVazeoQ.js";var u=r({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`}},setup(r){let c=r;s();let u=l();return(r,s)=>(i(),n(t(o),{type:r.as===`button`?`button`:void 0,as:r.as,"as-child":c.asChild,"aria-controls":t(u).contentId,"aria-expanded":t(u).open.value,"data-state":t(u).open.value?`open`:`closed`,"data-disabled":t(u).disabled?.value?``:void 0,disabled:t(u).disabled?.value,onClick:t(u).onOpenToggle},{default:e(()=>[a(r.$slots,`default`)]),_:3},8,[`type`,`as`,`as-child`,`aria-controls`,`aria-expanded`,`data-state`,`data-disabled`,`disabled`,`onClick`]))}}),d=r({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean},as:{}},setup(r){let o=r;return(r,s)=>(i(),n(t(u),c({"data-slot":`collapsible-trigger`},o),{default:e(()=>[a(r.$slots,`default`)]),_:3},16))}});export{d as t};
|