llm-simple-router 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +128 -91
  2. package/dist/admin/logs.js +7 -1
  3. package/dist/admin/metrics.js +7 -3
  4. package/dist/admin/recommended.d.ts +7 -0
  5. package/dist/admin/recommended.js +25 -0
  6. package/dist/admin/routes.js +4 -0
  7. package/dist/admin/usage.d.ts +7 -0
  8. package/dist/admin/usage.js +66 -0
  9. package/dist/config/recommended.d.ts +24 -0
  10. package/dist/config/recommended.js +30 -0
  11. package/dist/db/index.d.ts +3 -1
  12. package/dist/db/index.js +2 -1
  13. package/dist/db/logs.d.ts +6 -0
  14. package/dist/db/logs.js +12 -0
  15. package/dist/db/metrics.d.ts +3 -3
  16. package/dist/db/metrics.js +50 -42
  17. package/dist/db/migrations/019_create_usage_windows.sql +11 -0
  18. package/dist/db/retry-rules.d.ts +0 -5
  19. package/dist/db/retry-rules.js +0 -36
  20. package/dist/db/usage-windows.d.ts +19 -0
  21. package/dist/db/usage-windows.js +37 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +8 -3
  24. package/dist/proxy/usage-window-tracker.d.ts +11 -0
  25. package/dist/proxy/usage-window-tracker.js +75 -0
  26. package/dist/utils/datetime.d.ts +4 -0
  27. package/dist/utils/datetime.js +10 -0
  28. package/frontend-dist/assets/CardContent-fmM_iiuR.js +1 -0
  29. package/frontend-dist/assets/CardHeader-BzzFzZ1B.js +1 -0
  30. package/frontend-dist/assets/CardTitle-09d7O-11.js +1 -0
  31. package/frontend-dist/assets/Checkbox-DH8iqXQd.js +1 -0
  32. package/frontend-dist/assets/CollapsibleTrigger-DCRRORrU.js +1 -0
  33. package/frontend-dist/assets/Collection-DY9-Yue9.js +3 -0
  34. package/frontend-dist/assets/Dashboard-BEzoZuSm.js +3 -0
  35. package/frontend-dist/assets/DialogTitle-BeMGJzYO.js +1 -0
  36. package/frontend-dist/assets/Input-BhvZ-Up7.js +1 -0
  37. package/frontend-dist/assets/Label-DjtouWZ7.js +1 -0
  38. package/frontend-dist/assets/LogDetailDialog-BjRsy_FR.js +3 -0
  39. package/frontend-dist/assets/Login-hOCPB-34.js +1 -0
  40. package/frontend-dist/assets/Logs-C5c3BJsg.js +1 -0
  41. package/frontend-dist/assets/ModelMappings-CDjxwyyz.js +1 -0
  42. package/frontend-dist/assets/Monitor-CPAvIREG.js +1 -0
  43. package/frontend-dist/assets/PopperContent-CHNw_qb6.js +1 -0
  44. package/frontend-dist/assets/Providers-C9ZAqHxO.js +1 -0
  45. package/frontend-dist/assets/ProxyEnhancement-Ct5WbiB7.js +5 -0
  46. package/frontend-dist/assets/RetryRules-CbgyrP6w.js +1 -0
  47. package/frontend-dist/assets/RouterKeys-zmqgFEKp.js +1 -0
  48. package/frontend-dist/assets/SelectValue-CP4Sh7LP.js +1 -0
  49. package/frontend-dist/assets/Setup-BXDEPt4o.js +1 -0
  50. package/frontend-dist/assets/Switch-DF6awXqs.js +1 -0
  51. package/frontend-dist/assets/TableHeader-BKE_yVML.js +1 -0
  52. package/frontend-dist/assets/TabsTrigger-D8R7lxaI.js +1 -0
  53. package/frontend-dist/assets/TooltipTrigger-BjQXeFem.js +1 -0
  54. package/frontend-dist/assets/VisuallyHidden-B_NnkONE.js +1 -0
  55. package/frontend-dist/assets/VisuallyHiddenInput-cjeTgyDe.js +1 -0
  56. package/frontend-dist/assets/alert-dialog-BoGRIC1Q.js +1 -0
  57. package/frontend-dist/assets/badge-DIO8W_W9.js +1 -0
  58. package/frontend-dist/assets/button-qxGNBunr.js +12 -0
  59. package/frontend-dist/assets/{createLucideIcon-DGZkBjcJ.js → createLucideIcon-jHUFhqKn.js} +1 -1
  60. package/frontend-dist/assets/dialog-D8pIXeSs.js +1 -0
  61. package/frontend-dist/assets/index-C_disqMY.js +1 -0
  62. package/frontend-dist/assets/index-DDp6SHfg.css +1 -0
  63. package/frontend-dist/assets/lib-DjpgwSRA.js +1 -0
  64. package/frontend-dist/assets/{ohash.D__AXeF1-B64hB831.js → ohash.D__AXeF1-nmJ7gFbh.js} +1 -1
  65. package/frontend-dist/assets/{useClipboard-CWc1cTDo.js → useClipboard-CmLp2YGk.js} +1 -1
  66. package/frontend-dist/assets/useForwardExpose-awoGXQkg.js +1 -0
  67. package/frontend-dist/assets/useNonce-_2e-GL-A.js +1 -0
  68. package/frontend-dist/assets/x-B0G-wIAB.js +1 -0
  69. package/frontend-dist/index.html +7 -7
  70. package/package.json +1 -1
  71. package/frontend-dist/assets/CardContent-CTnwqTdL.js +0 -1
  72. package/frontend-dist/assets/CardHeader-CfUeY7tk.js +0 -1
  73. package/frontend-dist/assets/CardTitle-CWiDwWqd.js +0 -1
  74. package/frontend-dist/assets/Checkbox-BxNz70R_.js +0 -1
  75. package/frontend-dist/assets/CollapsibleTrigger-Uz1aGdtH.js +0 -1
  76. package/frontend-dist/assets/Collection-1EHC87X5.js +0 -3
  77. package/frontend-dist/assets/Dashboard-C3FL30UN.js +0 -3
  78. package/frontend-dist/assets/DialogTitle-CAOFxr83.js +0 -1
  79. package/frontend-dist/assets/Input-DRIid2C6.js +0 -1
  80. package/frontend-dist/assets/Label-UyNN2jyE.js +0 -1
  81. package/frontend-dist/assets/LogDetailDialog-8BT4vIlV.js +0 -3
  82. package/frontend-dist/assets/Login-CnzH6TdS.js +0 -1
  83. package/frontend-dist/assets/Logs-CbK8NB_X.js +0 -1
  84. package/frontend-dist/assets/ModelMappings-DeRFgsYG.js +0 -1
  85. package/frontend-dist/assets/Monitor-Dd80bdUn.js +0 -1
  86. package/frontend-dist/assets/PopperContent-B3fZao7v.js +0 -1
  87. package/frontend-dist/assets/Providers-B_DbV-_y.js +0 -1
  88. package/frontend-dist/assets/ProxyEnhancement-up1fnPzq.js +0 -5
  89. package/frontend-dist/assets/RetryRules-Dkuhjh0u.js +0 -1
  90. package/frontend-dist/assets/RouterKeys-CvMMAa4t.js +0 -1
  91. package/frontend-dist/assets/RovingFocusItem-X0bfqWWS.js +0 -1
  92. package/frontend-dist/assets/SelectValue-zO8t-tx1.js +0 -1
  93. package/frontend-dist/assets/Setup-ByT2ThOQ.js +0 -1
  94. package/frontend-dist/assets/Switch-BEMjVugO.js +0 -1
  95. package/frontend-dist/assets/TableHeader-DpHWSnxK.js +0 -1
  96. package/frontend-dist/assets/TabsTrigger-Db6RqsZc.js +0 -1
  97. package/frontend-dist/assets/VisuallyHidden-hs8pj8OP.js +0 -1
  98. package/frontend-dist/assets/VisuallyHiddenInput-1m0nNADN.js +0 -1
  99. package/frontend-dist/assets/alert-dialog-PP91kaO8.js +0 -1
  100. package/frontend-dist/assets/button-Dcc0gF5i.js +0 -1
  101. package/frontend-dist/assets/client-DIIo9zPK.js +0 -12
  102. package/frontend-dist/assets/dialog-CxSyR-fN.js +0 -1
  103. package/frontend-dist/assets/index-BL-LAtac.css +0 -1
  104. package/frontend-dist/assets/index-CvT41fGL.js +0 -1
  105. package/frontend-dist/assets/lib-Bl0OuBjh.js +0 -1
  106. package/frontend-dist/assets/useForwardExpose-AkE0lq8y.js +0 -1
  107. package/frontend-dist/assets/useNonce-DGyPxdjq.js +0 -1
  108. package/frontend-dist/assets/x-BuUpx9Fr.js +0 -1
@@ -7,6 +7,7 @@ export function insertMetrics(db, m) {
7
7
  }
8
8
  const PERIOD_OFFSET = {
9
9
  "1h": "-1 hours",
10
+ "5h": "-5 hours",
10
11
  "6h": "-6 hours",
11
12
  "24h": "-1 day",
12
13
  "7d": "-7 days",
@@ -14,6 +15,7 @@ const PERIOD_OFFSET = {
14
15
  };
15
16
  const BUCKET_SECONDS = {
16
17
  "1h": 60,
18
+ "5h": 300,
17
19
  "6h": 300,
18
20
  "24h": 900,
19
21
  "7d": 3600,
@@ -21,10 +23,39 @@ const BUCKET_SECONDS = {
21
23
  };
22
24
  // unix epoch 秒转毫秒的乘数
23
25
  const MS_PER_SEC = 1000;
24
- export function getMetricsSummary(db, period, providerId, backendModel, routerKeyId) {
25
- const offset = PERIOD_OFFSET[period];
26
- const conditions = ["rm.is_complete = 1", "rm.created_at >= datetime('now', ?)"];
27
- const params = [offset];
26
+ // 时间跨度(秒)→ 桶大小(秒)的阶梯映射,与 BUCKET_SECONDS 保持对齐
27
+ const BUCKET_THRESHOLDS = [
28
+ { maxSec: 3600, bucketSec: 60 }, // ≤1h: 1min
29
+ { maxSec: 21600, bucketSec: 300 }, // ≤6h: 5min
30
+ { maxSec: 86400, bucketSec: 900 }, // ≤1d: 15min
31
+ { maxSec: 604800, bucketSec: 3600 }, // ≤7d: 1h
32
+ ];
33
+ const FALLBACK_BUCKET_SEC = 14400; // >7d: 4h
34
+ function calculateBucketSeconds(startTime, endTime) {
35
+ const ms = new Date(endTime).getTime() - new Date(startTime).getTime();
36
+ const sec = ms / MS_PER_SEC;
37
+ const match = BUCKET_THRESHOLDS.find((t) => sec <= t.maxSec);
38
+ return match ? match.bucketSec : FALLBACK_BUCKET_SEC;
39
+ }
40
+ function buildTimeCondition(period, startTime, endTime) {
41
+ if (startTime && endTime) {
42
+ // request_metrics.created_at 用 datetime('now') 格式 (YYYY-MM-DD HH:MM:SS),
43
+ // 前端传入 ISO 8601,需要转换格式以匹配字符串比较
44
+ return {
45
+ timeWhere: "rm.created_at >= datetime(?) AND rm.created_at < datetime(?)",
46
+ timeParams: [startTime, endTime],
47
+ };
48
+ }
49
+ return {
50
+ timeWhere: "rm.created_at >= datetime('now', ?)",
51
+ timeParams: [PERIOD_OFFSET[period]],
52
+ };
53
+ }
54
+ export function getMetricsSummary(db, period, providerId, backendModel, routerKeyId, startTime, endTime) {
55
+ const { timeWhere, timeParams } = buildTimeCondition(period, startTime, endTime);
56
+ const conditions = ["rm.is_complete = 1", timeWhere];
57
+ const params = [...timeParams];
58
+ const joins = ["LEFT JOIN providers p ON p.id = rm.provider_id"];
28
59
  if (providerId) {
29
60
  conditions.push("rm.provider_id = ?");
30
61
  params.push(providerId);
@@ -33,48 +64,23 @@ export function getMetricsSummary(db, period, providerId, backendModel, routerKe
33
64
  conditions.push("rm.backend_model = ?");
34
65
  params.push(backendModel);
35
66
  }
36
- const joins = "LEFT JOIN providers p ON p.id = rm.provider_id";
37
67
  if (routerKeyId) {
68
+ joins.push("LEFT JOIN request_logs rl ON rl.id = rm.request_log_id");
38
69
  conditions.push("rl.router_key_id = ?");
39
70
  params.push(routerKeyId);
40
- return db.prepare(`
41
- SELECT
42
- rm.provider_id, COALESCE(p.name, rm.provider_id) AS provider_name, rm.backend_model,
43
- COUNT(*) AS request_count, AVG(rm.ttft_ms) AS avg_ttft_ms, NULL AS p50_ttft_ms, NULL AS p95_ttft_ms,
44
- AVG(rm.tokens_per_second) AS avg_tps,
45
- COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens, COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens,
46
- COALESCE(SUM(rm.cache_read_tokens), 0) AS total_cache_hit_tokens,
47
- CASE WHEN SUM(rm.input_tokens) > 0 THEN SUM(rm.cache_read_tokens) * 1.0 / SUM(rm.input_tokens) ELSE NULL END AS cache_hit_rate
48
- FROM request_metrics rm
49
- LEFT JOIN providers p ON p.id = rm.provider_id
50
- LEFT JOIN request_logs rl ON rl.id = rm.request_log_id
51
- WHERE ${conditions.join(" AND ")}
52
- GROUP BY rm.provider_id, rm.backend_model ORDER BY request_count DESC
53
- `).all(...params);
54
71
  }
55
- const where = conditions.join(" AND ");
56
72
  return db.prepare(`
57
73
  SELECT
58
- rm.provider_id,
59
- COALESCE(p.name, rm.provider_id) AS provider_name,
60
- rm.backend_model,
61
- COUNT(*) AS request_count,
62
- AVG(rm.ttft_ms) AS avg_ttft_ms,
63
- NULL AS p50_ttft_ms,
64
- NULL AS p95_ttft_ms,
74
+ rm.provider_id, COALESCE(p.name, rm.provider_id) AS provider_name, rm.backend_model,
75
+ COUNT(*) AS request_count, AVG(rm.ttft_ms) AS avg_ttft_ms, NULL AS p50_ttft_ms, NULL AS p95_ttft_ms,
65
76
  AVG(rm.tokens_per_second) AS avg_tps,
66
- COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens,
67
- COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens,
77
+ COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens, COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens,
68
78
  COALESCE(SUM(rm.cache_read_tokens), 0) AS total_cache_hit_tokens,
69
- CASE WHEN SUM(rm.input_tokens) > 0
70
- THEN SUM(rm.cache_read_tokens) * 1.0 / SUM(rm.input_tokens)
71
- ELSE NULL
72
- END AS cache_hit_rate
79
+ CASE WHEN SUM(rm.input_tokens) > 0 THEN SUM(rm.cache_read_tokens) * 1.0 / SUM(rm.input_tokens) ELSE NULL END AS cache_hit_rate
73
80
  FROM request_metrics rm
74
- ${joins}
75
- WHERE ${where}
76
- GROUP BY rm.provider_id, rm.backend_model
77
- ORDER BY request_count DESC
81
+ ${joins.join(" ")}
82
+ WHERE ${conditions.join(" AND ")}
83
+ GROUP BY rm.provider_id, rm.backend_model ORDER BY request_count DESC
78
84
  `).all(...params);
79
85
  }
80
86
  const METRIC_EXPR = {
@@ -87,11 +93,13 @@ const METRIC_EXPR = {
87
93
  output_tokens: "SUM(rm.output_tokens)",
88
94
  cache_hit_tokens: "SUM(rm.cache_read_tokens)",
89
95
  };
90
- export function getMetricsTimeseries(db, period, metric, providerId, backendModel, routerKeyId) {
91
- const offset = PERIOD_OFFSET[period];
92
- const bucketSec = BUCKET_SECONDS[period];
93
- const conditions = ["rm.is_complete = 1", "rm.created_at >= datetime('now', ?)"];
94
- const params = [offset];
96
+ export function getMetricsTimeseries(db, period, metric, providerId, backendModel, routerKeyId, startTime, endTime) {
97
+ const bucketSec = (startTime && endTime)
98
+ ? calculateBucketSeconds(startTime, endTime)
99
+ : BUCKET_SECONDS[period];
100
+ const { timeWhere, timeParams } = buildTimeCondition(period, startTime, endTime);
101
+ const conditions = ["rm.is_complete = 1", timeWhere];
102
+ const params = [...timeParams];
95
103
  if (providerId) {
96
104
  conditions.push("rm.provider_id = ?");
97
105
  params.push(providerId);
@@ -0,0 +1,11 @@
1
+ CREATE TABLE IF NOT EXISTS usage_windows (
2
+ id TEXT PRIMARY KEY,
3
+ router_key_id TEXT,
4
+ start_time TEXT NOT NULL,
5
+ end_time TEXT NOT NULL,
6
+ created_at TEXT DEFAULT (datetime('now'))
7
+ );
8
+
9
+ CREATE INDEX IF NOT EXISTS idx_usage_windows_start ON usage_windows(start_time);
10
+ CREATE INDEX IF NOT EXISTS idx_usage_windows_router_key ON usage_windows(router_key_id);
11
+ CREATE INDEX IF NOT EXISTS idx_usage_windows_end_time ON usage_windows(end_time);
@@ -25,8 +25,3 @@ export declare function createRetryRule(db: Database.Database, rule: {
25
25
  }): string;
26
26
  export declare function updateRetryRule(db: Database.Database, id: string, fields: Partial<Pick<RetryRule, "name" | "status_code" | "body_pattern" | "is_active" | "retry_strategy" | "retry_delay_ms" | "max_retries" | "max_delay_ms">>): void;
27
27
  export declare function deleteRetryRule(db: Database.Database, id: string): void;
28
- /**
29
- * 启动时按名称查重插入默认重试规则。
30
- * 已存在的规则不会被重复插入或覆盖。
31
- */
32
- export declare function seedDefaultRules(db: Database.Database): void;
@@ -27,39 +27,3 @@ export function updateRetryRule(db, id, fields) {
27
27
  export function deleteRetryRule(db, id) {
28
28
  deleteById(db, "retry_rules", id);
29
29
  }
30
- // ---------- Default seed rules ----------
31
- const DEFAULT_RETRY_FIELDS = {
32
- is_active: 1,
33
- retry_strategy: "exponential",
34
- retry_delay_ms: DEFAULT_RETRY_DELAY_MS,
35
- max_retries: DEFAULT_MAX_RETRIES,
36
- max_delay_ms: DEFAULT_MAX_DELAY_MS,
37
- };
38
- const DEFAULT_RULES = [
39
- { name: "429 Too Many Requests", status_code: 429, body_pattern: ".*", ...DEFAULT_RETRY_FIELDS },
40
- { name: "503 Service Unavailable", status_code: 503, body_pattern: ".*", ...DEFAULT_RETRY_FIELDS },
41
- { name: 'ZAI 网络错误 (code 1234)', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*"code"\\s*:\\s*"1234"', ...DEFAULT_RETRY_FIELDS },
42
- { name: 'ZAI 临时不可用', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*请稍后重试', ...DEFAULT_RETRY_FIELDS },
43
- { name: 'ZAI 操作失败 (code 500)', status_code: 400, body_pattern: '"type"\\s*:\\s*"error".*"code"\\s*:\\s*"500"', ...DEFAULT_RETRY_FIELDS },
44
- { name: 'ZAI 速率限制 (HTTP 200, code 1302)', status_code: 200, body_pattern: '"error".*"code"\\s*:\\s*"1302"', ...DEFAULT_RETRY_FIELDS },
45
- { name: 'ZAI SSE 错误 (HTTP 200, code 500)', status_code: 200, body_pattern: '"error".*"code"\\s*:\\s*"500"', ...DEFAULT_RETRY_FIELDS },
46
- { name: 'ZAI SSE 错误 (HTTP 200, code 1234)', status_code: 200, body_pattern: '"error".*"code"\\s*:\\s*"1234"', ...DEFAULT_RETRY_FIELDS },
47
- ];
48
- /**
49
- * 启动时按名称查重插入默认重试规则。
50
- * 已存在的规则不会被重复插入或覆盖。
51
- */
52
- export function seedDefaultRules(db) {
53
- const names = DEFAULT_RULES.map(r => r.name);
54
- const placeholders = names.map(() => '?').join(',');
55
- const existing = db.prepare(`SELECT name FROM retry_rules WHERE name IN (${placeholders})`).all(...names);
56
- const existingSet = new Set(existing.map(r => r.name));
57
- const stmt = db.prepare(`INSERT INTO retry_rules (id, name, status_code, body_pattern, is_active, created_at, retry_strategy, retry_delay_ms, max_retries, max_delay_ms)
58
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
59
- const now = new Date().toISOString();
60
- for (const rule of DEFAULT_RULES) {
61
- if (!existingSet.has(rule.name)) {
62
- stmt.run(randomUUID(), rule.name, rule.status_code, rule.body_pattern, rule.is_active, now, rule.retry_strategy, rule.retry_delay_ms, rule.max_retries, rule.max_delay_ms);
63
- }
64
- }
65
- }
@@ -0,0 +1,19 @@
1
+ import Database from "better-sqlite3";
2
+ export interface UsageWindow {
3
+ id: string;
4
+ router_key_id: string | null;
5
+ start_time: string;
6
+ end_time: string;
7
+ created_at: string;
8
+ }
9
+ export interface WindowUsage {
10
+ request_count: number;
11
+ total_input_tokens: number;
12
+ total_output_tokens: number;
13
+ }
14
+ export declare function insertWindow(db: Database.Database, w: Omit<UsageWindow, "created_at">): string;
15
+ export declare function getLatestWindow(db: Database.Database, routerKeyId?: string): UsageWindow | null;
16
+ /** 返回与 [start, end) 区间有重叠的窗口 */
17
+ export declare function getWindowsInRange(db: Database.Database, start: string, end: string, routerKeyId?: string): UsageWindow[];
18
+ /** 聚合指定时间窗口内的请求计数和 token 用量 */
19
+ export declare function getWindowUsage(db: Database.Database, startTime: string, endTime: string, routerKeyId?: string): WindowUsage;
@@ -0,0 +1,37 @@
1
+ import { randomUUID } from "crypto";
2
+ export function insertWindow(db, w) {
3
+ const id = w.id || randomUUID();
4
+ db.prepare("INSERT INTO usage_windows (id, router_key_id, start_time, end_time) VALUES (?, ?, ?, ?)").run(id, w.router_key_id ?? null, w.start_time, w.end_time);
5
+ return id;
6
+ }
7
+ export function getLatestWindow(db, routerKeyId) {
8
+ const sql = routerKeyId
9
+ ? "SELECT * FROM usage_windows WHERE router_key_id = ? ORDER BY start_time DESC LIMIT 1"
10
+ : "SELECT * FROM usage_windows ORDER BY start_time DESC LIMIT 1";
11
+ const params = routerKeyId ? [routerKeyId] : [];
12
+ return db.prepare(sql).get(...params) ?? null;
13
+ }
14
+ /** 返回与 [start, end) 区间有重叠的窗口 */
15
+ export function getWindowsInRange(db, start, end, routerKeyId) {
16
+ if (routerKeyId) {
17
+ return db.prepare("SELECT * FROM usage_windows WHERE start_time < ? AND end_time > ? AND router_key_id = ? ORDER BY start_time ASC").all(end, start, routerKeyId);
18
+ }
19
+ return db.prepare("SELECT * FROM usage_windows WHERE start_time < ? AND end_time > ? ORDER BY start_time ASC").all(end, start);
20
+ }
21
+ /** 聚合指定时间窗口内的请求计数和 token 用量 */
22
+ export function getWindowUsage(db, startTime, endTime, routerKeyId) {
23
+ const baseSql = `
24
+ SELECT
25
+ COUNT(*) AS request_count,
26
+ COALESCE(SUM(rm.input_tokens), 0) AS total_input_tokens,
27
+ COALESCE(SUM(rm.output_tokens), 0) AS total_output_tokens
28
+ FROM request_metrics rm
29
+ JOIN request_logs rl ON rl.id = rm.request_log_id
30
+ WHERE rm.is_complete = 1
31
+ AND rm.created_at >= datetime(?)
32
+ AND rm.created_at < datetime(?)`;
33
+ if (routerKeyId) {
34
+ return db.prepare(`${baseSql} AND rl.router_key_id = ?`).get(startTime, endTime, routerKeyId);
35
+ }
36
+ return db.prepare(baseSql).get(startTime, endTime);
37
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { FastifyInstance } from "fastify";
3
3
  import { Config } from "./config.js";
4
+ import { UsageWindowTracker } from "./proxy/usage-window-tracker.js";
4
5
  import Database from "better-sqlite3";
5
6
  export interface AppOptions {
6
7
  config?: Config;
@@ -9,6 +10,7 @@ export interface AppOptions {
9
10
  export declare function buildApp(options?: AppOptions): Promise<{
10
11
  app: FastifyInstance;
11
12
  db: Database.Database;
13
+ usageWindowTracker: UsageWindowTracker;
12
14
  close: () => Promise<void>;
13
15
  }>;
14
16
  export declare function main(): Promise<void>;
package/dist/index.js CHANGED
@@ -21,7 +21,8 @@ function getProxyApiType(url) {
21
21
  const __filename = fileURLToPath(import.meta.url);
22
22
  const __dirname = path.dirname(__filename);
23
23
  import { getConfig } from "./config.js";
24
- import { initDatabase, seedDefaultRules, getAllProviders } from "./db/index.js";
24
+ import { initDatabase, getAllProviders } from "./db/index.js";
25
+ import { loadRecommendedConfig } from "./config/recommended.js";
25
26
  import { authMiddleware } from "./middleware/auth.js";
26
27
  import { openaiProxy } from "./proxy/openai.js";
27
28
  import { anthropicProxy } from "./proxy/anthropic.js";
@@ -30,6 +31,7 @@ import { RetryRuleMatcher } from "./proxy/retry-rules.js";
30
31
  import { ProviderSemaphoreManager } from "./proxy/semaphore.js";
31
32
  import { RequestTracker } from "./monitor/request-tracker.js";
32
33
  import { modelState } from "./proxy/model-state.js";
34
+ import { UsageWindowTracker } from "./proxy/usage-window-tracker.js";
33
35
  import fastifyStatic from "@fastify/static";
34
36
  export async function buildApp(options) {
35
37
  const config = options?.config ?? getBaseConfig();
@@ -100,8 +102,7 @@ export async function buildApp(options) {
100
102
  }
101
103
  return reply.code(status).send({ error: { message: fastifyError.message } });
102
104
  });
103
- // 首次启动时插入默认重试规则(表为空时)
104
- seedDefaultRules(db);
105
+ loadRecommendedConfig();
105
106
  // 注入 DB 到 modelState 单例,启用会话级持久化
106
107
  modelState.init(db);
107
108
  const matcher = new RetryRuleMatcher();
@@ -109,6 +110,9 @@ export async function buildApp(options) {
109
110
  const semaphoreManager = new ProviderSemaphoreManager();
110
111
  const tracker = new RequestTracker({ semaphoreManager, logger: app.log });
111
112
  tracker.startPushInterval();
113
+ // 5h 用量窗口追踪器,启动时自动补齐缺失窗口
114
+ const usageWindowTracker = new UsageWindowTracker(db);
115
+ usageWindowTracker.reconcileOnStartup();
112
116
  // 从 DB 读取已有 provider 的并发配置,初始化信号量管理器和 tracker
113
117
  const allProviders = getAllProviders(db);
114
118
  for (const p of allProviders) {
@@ -172,6 +176,7 @@ export async function buildApp(options) {
172
176
  return {
173
177
  app,
174
178
  db,
179
+ usageWindowTracker,
175
180
  close: async () => {
176
181
  tracker.stopPushInterval();
177
182
  await app.close();
@@ -0,0 +1,11 @@
1
+ import Database from "better-sqlite3";
2
+ export declare class UsageWindowTracker {
3
+ private db;
4
+ constructor(db: Database.Database);
5
+ /** 请求成功后调用,按需创建新窗口 */
6
+ recordRequest(routerKeyId?: string): void;
7
+ /** 启动时补齐因宕机/重启而缺失的窗口 */
8
+ reconcileOnStartup(): void;
9
+ /** 从 baseTime 开始,每 5h 一个窗口,直到覆盖 lastLogTime */
10
+ private backfillWindows;
11
+ }
@@ -0,0 +1,75 @@
1
+ import { randomUUID } from "crypto";
2
+ import { getLatestWindow, insertWindow } from "../db/usage-windows.js";
3
+ import { toSqliteDatetime, parseSqliteDatetime as parseDate } from "../utils/datetime.js";
4
+ // eslint-disable-next-line no-magic-numbers
5
+ const WINDOW_DURATION_MS = 5 * 3600_000;
6
+ export class UsageWindowTracker {
7
+ db;
8
+ constructor(db) {
9
+ this.db = db;
10
+ }
11
+ /** 请求成功后调用,按需创建新窗口 */
12
+ recordRequest(routerKeyId) {
13
+ const now = new Date();
14
+ const latest = getLatestWindow(this.db, routerKeyId);
15
+ if (!latest || now > parseDate(latest.end_time)) {
16
+ const startTime = truncateToMinute(now);
17
+ insertWindow(this.db, {
18
+ id: randomUUID(),
19
+ router_key_id: routerKeyId ?? null,
20
+ start_time: toSqliteDatetime(startTime),
21
+ end_time: toSqliteDatetime(new Date(startTime.getTime() + WINDOW_DURATION_MS)),
22
+ });
23
+ }
24
+ }
25
+ /** 启动时补齐因宕机/重启而缺失的窗口 */
26
+ reconcileOnStartup() {
27
+ const latest = getLatestWindow(this.db);
28
+ // 查找 request_logs 中最新一条请求的时间
29
+ const lastLog = this.db.prepare("SELECT created_at FROM request_logs ORDER BY created_at DESC LIMIT 1").get();
30
+ if (!lastLog)
31
+ return;
32
+ if (!latest) {
33
+ // 从未创建过窗口,但有请求记录,从最早请求创建初始窗口
34
+ const firstLog = this.db.prepare("SELECT created_at FROM request_logs ORDER BY created_at ASC LIMIT 1").get();
35
+ if (!firstLog)
36
+ return;
37
+ const start = parseDate(firstLog.created_at);
38
+ const truncated = truncateToMinute(start);
39
+ insertWindow(this.db, {
40
+ id: randomUUID(),
41
+ router_key_id: null,
42
+ start_time: toSqliteDatetime(truncated),
43
+ end_time: toSqliteDatetime(new Date(truncated.getTime() + WINDOW_DURATION_MS)),
44
+ });
45
+ // 继续补齐后续窗口
46
+ this.backfillWindows(truncated);
47
+ return;
48
+ }
49
+ // 有窗口,检查 end_time 之后是否有请求
50
+ this.backfillWindows(parseDate(latest.end_time));
51
+ }
52
+ /** 从 baseTime 开始,每 5h 一个窗口,直到覆盖 lastLogTime */
53
+ backfillWindows(baseTime) {
54
+ const lastLog = this.db.prepare("SELECT created_at FROM request_logs ORDER BY created_at DESC LIMIT 1").get();
55
+ if (!lastLog)
56
+ return;
57
+ const lastLogTime = parseDate(lastLog.created_at);
58
+ let windowStart = baseTime;
59
+ while (windowStart < lastLogTime) {
60
+ const windowEnd = new Date(windowStart.getTime() + WINDOW_DURATION_MS);
61
+ insertWindow(this.db, {
62
+ id: randomUUID(),
63
+ router_key_id: null,
64
+ start_time: toSqliteDatetime(windowStart),
65
+ end_time: toSqliteDatetime(windowEnd),
66
+ });
67
+ windowStart = windowEnd;
68
+ }
69
+ }
70
+ }
71
+ function truncateToMinute(date) {
72
+ const d = new Date(date);
73
+ d.setSeconds(0, 0);
74
+ return d;
75
+ }
@@ -0,0 +1,4 @@
1
+ /** Date → SQLite datetime 文本 (YYYY-MM-DD HH:MM:SS),UTC 时区,与 DEFAULT (datetime('now')) 对齐 */
2
+ export declare function toSqliteDatetime(date: Date): string;
3
+ /** 兼容 ISO 和 SQLite datetime 格式的日期解析,均视为 UTC */
4
+ export declare function parseSqliteDatetime(s: string): Date;
@@ -0,0 +1,10 @@
1
+ /** Date → SQLite datetime 文本 (YYYY-MM-DD HH:MM:SS),UTC 时区,与 DEFAULT (datetime('now')) 对齐 */
2
+ export function toSqliteDatetime(date) {
3
+ return date.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "");
4
+ }
5
+ /** 兼容 ISO 和 SQLite datetime 格式的日期解析,均视为 UTC */
6
+ export function parseSqliteDatetime(s) {
7
+ if (s.includes("T"))
8
+ return new Date(s);
9
+ return new Date(s + "Z");
10
+ }
@@ -0,0 +1 @@
1
+ import{Ft as e,K as t,Lt as n,U as r,at as i,ct as a,r as o}from"./button-qxGNBunr.js";var s=[`data-size`],c=t({__name:`Card`,props:{class:{type:[Boolean,null,String,Object,Array]},size:{default:`default`}},setup(t){let c=t;return(l,u)=>(i(),r(`div`,{"data-slot":`card`,"data-size":t.size,class:n(e(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=t({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(t){let s=t;return(t,c)=>(i(),r(`div`,{"data-slot":`card-content`,class:n(e(o)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[a(t.$slots,`default`)],2))}});export{c as n,l as t};
@@ -0,0 +1 @@
1
+ import{Ft as e,K as t,Lt as n,U as r,at as i,ct as a,r as o}from"./button-qxGNBunr.js";var s=t({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(t){let s=t;return(t,c)=>(i(),r(`div`,{"data-slot":`card-header`,class:n(e(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(t.$slots,`default`)],2))}});export{s as t};
@@ -0,0 +1 @@
1
+ import{Ft as e,K as t,Lt as n,U as r,at as i,ct as a,r as o}from"./button-qxGNBunr.js";var s=t({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(t){let s=t;return(t,c)=>(i(),r(`div`,{"data-slot":`card-title`,class:n(e(o)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[a(t.$slots,`default`)],2))}});export{s as t};
@@ -0,0 +1 @@
1
+ import{$ as e,F as t,Ft as n,G as r,H as i,I as a,J as o,K as s,Rt as c,V as l,at as u,b as d,ct as f,i as p,p as m,r as h,ut as g,yt as _,z as v}from"./button-qxGNBunr.js";import{n as y,t as b}from"./ohash.D__AXeF1-nmJ7gFbh.js";import{S as x,c as S,f as C,y as w}from"./Collection-DY9-Yue9.js";import{n as T}from"./VisuallyHidden-B_NnkONE.js";import{t as E}from"./useForwardExpose-awoGXQkg.js";import{t as D}from"./VisuallyHiddenInput-cjeTgyDe.js";import{s as O}from"./TableHeader-BKE_yVML.js";function k(e,t){return w(e)?!1:Array.isArray(e)?e.some(e=>b(e,t)):b(e,t)}var[A,j]=x(`CheckboxGroupRoot`);function M(e){return e===`indeterminate`}function N(e){return M(e)?`indeterminate`:e?`checked`:`unchecked`}var[P,F]=x(`CheckboxRoot`),I=s({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(r,{emit:o}){let s=r,c=o,{forwardRef:d,currentElement:h}=E(),y=A(null),x=m(s,`modelValue`,c,{defaultValue:s.defaultValue??s.falseValue,passive:s.modelValue===void 0}),S=v(()=>y?.disabled.value||s.disabled),C=v(()=>b(x.value,s.trueValue)),j=v(()=>w(y?.modelValue.value)?x.value===`indeterminate`?`indeterminate`:C.value:k(y.modelValue.value,s.value));function P(){if(w(y?.modelValue.value))x.value===`indeterminate`?x.value=s.trueValue:x.value=C.value?s.falseValue:s.trueValue;else{let e=[...y.modelValue.value||[]];if(k(e,s.value)){let t=e.findIndex(e=>b(e,s.value));e.splice(t,1)}else e.push(s.value);y.modelValue.value=e}}let I=T(h),L=v(()=>s.id&&h.value?document.querySelector(`[for="${s.id}"]`)?.innerText:void 0);return F({disabled:S,state:j}),(r,o)=>(u(),l(g(n(y)?.rovingFocus.value?n(O):n(p)),e(r.$attrs,{id:r.id,ref:n(d),role:`checkbox`,"as-child":r.asChild,as:r.as,type:r.as===`button`?`button`:void 0,"aria-checked":n(M)(j.value)?`mixed`:j.value,"aria-required":r.required,"aria-label":r.$attrs[`aria-label`]||L.value,"data-state":n(N)(j.value),"data-disabled":S.value?``:void 0,disabled:S.value,focusable:n(y)?.rovingFocus.value?!S.value:void 0,onKeydown:t(a(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:_(()=>[f(r.$slots,`default`,{modelValue:n(x),state:j.value}),n(I)&&r.name&&!n(y)?(u(),l(n(D),{key:0,type:`checkbox`,checked:!!j.value,name:r.name,value:r.value,disabled:S.value,required:r.required},null,8,[`checked`,`name`,`value`,`disabled`,`required`])):i(`v-if`,!0)]),_:3},16,[`id`,`as-child`,`as`,`type`,`aria-checked`,`aria-required`,`aria-label`,`data-state`,`data-disabled`,`disabled`,`focusable`,`onKeydown`]))}}),L=s({__name:`CheckboxIndicator`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`span`}},setup(t){let{forwardRef:i}=E(),a=P();return(t,o)=>(u(),l(n(S),{present:t.forceMount||n(M)(n(a).state.value)||n(a).state.value===!0},{default:_(()=>[r(n(p),e({ref:n(i),"data-state":n(N)(n(a).state.value),"data-disabled":n(a).disabled.value?``:void 0,style:{pointerEvents:`none`},"as-child":t.asChild,as:t.as},t.$attrs),{default:_(()=>[f(t.$slots,`default`)]),_:3},16,[`data-state`,`data-disabled`,`as-child`,`as`])]),_:3},8,[`present`]))}}),R=s({__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(t,{emit:i}){let a=t,s=i,p=C(d(a,`class`),s);return(t,i)=>(u(),l(n(I),e({"data-slot":`checkbox`},n(p),{class:n(h)(`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`,a.class)}),{default:_(e=>[r(n(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:_(()=>[f(t.$slots,`default`,c(o(e)),()=>[r(n(y))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};
@@ -0,0 +1 @@
1
+ import{$ as e,Dt as t,Ft as n,G as r,H as i,J as a,K as o,Mt as s,Rt as c,V as l,at as u,ct as d,et as f,gt as p,i as m,nt as h,p as g,u as _,yt as v,z as y}from"./button-qxGNBunr.js";import{S as b,c as x,f as S,u as C}from"./Collection-DY9-Yue9.js";import{t as w}from"./useForwardExpose-awoGXQkg.js";var[T,E]=b(`CollapsibleRoot`),D=o({__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 i=e,a=g(i,`open`,r,{defaultValue:i.defaultOpen,passive:i.open===void 0}),{disabled:o,unmountOnHide:c}=s(i);return E({contentId:``,disabled:o,open:a,unmountOnHide:c,onOpenToggle:()=>{o.value||(a.value=!a.value)}}),t({open:a}),w(),(e,t)=>(u(),l(n(m),{as:e.as,"as-child":i.asChild,"data-state":n(a)?`open`:`closed`,"data-disabled":n(o)?``:void 0},{default:v(()=>[d(e.$slots,`default`,{open:n(a)})]),_:3},8,[`as`,`as-child`,`data-state`,`data-disabled`]))}}),O=o({inheritAttrs:!1,__name:`CollapsibleContent`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`contentFound`],setup(a,{emit:o}){let s=a,c=o,g=T();g.contentId||=C(void 0,`reka-collapsible-content`);let b=t(),{forwardRef:S,currentElement:E}=w(),D=t(0),O=t(0),k=y(()=>g.open.value),A=t(k.value),j=t();p(()=>[k.value,b.value?.present],async()=>{await f();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=y(()=>A.value&&g.open.value);return h(()=>{requestAnimationFrame(()=>{A.value=!1})}),_(E,`beforematch`,e=>{requestAnimationFrame(()=>{g.onOpenToggle(),c(`contentFound`)})}),(t,a)=>(u(),l(n(x),{ref_key:`presentRef`,ref:b,present:t.forceMount||n(g).open.value,"force-mount":!0},{default:v(({present:a})=>[r(n(m),e(t.$attrs,{id:n(g).contentId,ref:n(S),"as-child":s.asChild,as:t.as,hidden:a?void 0:n(g).unmountOnHide.value?``:`until-found`,"data-state":M.value?void 0:n(g).open.value?`open`:`closed`,"data-disabled":n(g).disabled?.value?``:void 0,style:{"--reka-collapsible-content-height":`${O.value}px`,"--reka-collapsible-content-width":`${D.value}px`}}),{default:v(()=>[!n(g).unmountOnHide.value||a?d(t.$slots,`default`,{key:0}):i(`v-if`,!0)]),_:2},1040,[`id`,`as-child`,`as`,`hidden`,`data-state`,`data-disabled`,`style`])]),_:3},8,[`present`]))}}),k=o({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`}},setup(e){let t=e;w();let r=T();return(e,i)=>(u(),l(n(m),{type:e.as===`button`?`button`:void 0,as:e.as,"as-child":t.asChild,"aria-controls":n(r).contentId,"aria-expanded":n(r).open.value,"data-state":n(r).open.value?`open`:`closed`,"data-disabled":n(r).disabled?.value?``:void 0,disabled:n(r).disabled?.value,onClick:n(r).onOpenToggle},{default:v(()=>[d(e.$slots,`default`)]),_:3},8,[`type`,`as`,`as-child`,`aria-controls`,`aria-expanded`,`data-state`,`data-disabled`,`disabled`,`onClick`]))}}),A=o({__name:`Collapsible`,props:{defaultOpen:{type:Boolean},open:{type:Boolean},disabled:{type:Boolean},unmountOnHide:{type:Boolean},asChild:{type:Boolean},as:{}},emits:[`update:open`],setup(t,{emit:r}){let i=S(t,r);return(t,r)=>(u(),l(n(D),e({"data-slot":`collapsible`},n(i)),{default:v(e=>[d(t.$slots,`default`,c(a(e)))]),_:3},16))}}),j=o({__name:`CollapsibleContent`,props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{}},setup(t){let r=t;return(t,i)=>(u(),l(n(O),e({"data-slot":`collapsible-content`},r),{default:v(()=>[d(t.$slots,`default`)]),_:3},16))}}),M=o({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean},as:{}},setup(t){let r=t;return(t,i)=>(u(),l(n(k),e({"data-slot":`collapsible-trigger`},r),{default:v(()=>[d(t.$slots,`default`)]),_:3},16))}});export{j as n,A as r,M as t};
@@ -0,0 +1,3 @@
1
+ import{Dt as e,Et as t,Ft as n,H as r,It as i,K as a,M as o,Mt as s,Nt as c,R as l,S as u,V as d,Vt as f,X as p,Y as m,_ as h,_t as g,a as _,at as v,c as ee,ct as y,d as te,et as b,g as ne,gt as x,i as S,jt as re,l as C,o as ie,ot as w,pt as ae,q as T,rt as E,s as oe,u as se,v as D,wt as ce,y as le,yt as ue,z as O,zt as de}from"./button-qxGNBunr.js";import{t as k}from"./useForwardExpose-awoGXQkg.js";function A(e,t){let n=typeof e==`string`&&!t?`${e}Context`:t,r=Symbol(n);return[t=>{let n=p(r,t);if(n||n===null)return n;throw Error(`Injection \`${r.toString()}\` not found. Component must be used within ${Array.isArray(e)?`one of the following components: ${e.join(`, `)}`:`\`${e}\``}`)},e=>(w(r,e),e)]}function j(){let e=document.activeElement;if(e==null)return null;for(;e!=null&&e.shadowRoot!=null&&e.shadowRoot.activeElement!=null;)e=e.shadowRoot.activeElement;return e}function M(e,t,n){let r=n.originalEvent.target,i=new CustomEvent(e,{bubbles:!1,cancelable:!0,detail:n});t&&r.addEventListener(e,t,{once:!0}),r.dispatchEvent(i)}function N(e){return e==null}var[P,fe]=A(`ConfigProvider`);function F(e){if(typeof e!=`object`||!e)return!1;let t=Object.getPrototypeOf(e);return t!==null&&t!==Object.prototype&&Object.getPrototypeOf(t)!==null||Symbol.iterator in e?!1:Symbol.toStringTag in e?Object.prototype.toString.call(e)===`[object Module]`:!0}function I(e,t,n=`.`,r){if(!F(t))return I(e,{},n,r);let i={...t};for(let t of Object.keys(e)){if(t===`__proto__`||t===`constructor`)continue;let a=e[t];a!=null&&(r&&r(i,t,a,n)||(Array.isArray(a)&&Array.isArray(i[t])?i[t]=[...a,...i[t]]:F(a)&&F(i[t])?i[t]=I(a,i[t],(n?`${n}.`:``)+t.toString(),r):i[t]=a))}return i}function pe(e){return(...t)=>t.reduce((t,n)=>I(t,n,``,e),{})}var L=pe(),me=h(()=>{let t=e(new Map),n=e(),r=O(()=>{for(let e of t.value.values())if(e)return!0;return!1}),i=P({scrollBody:e(!0)}),a=null,o=()=>{document.body.style.paddingRight=``,document.body.style.marginRight=``,document.body.style.pointerEvents=``,document.documentElement.style.removeProperty(`--scrollbar-width`),document.body.style.overflow=n.value??``,le&&a?.(),n.value=void 0};return x(r,(e,t)=>{if(!D)return;if(!e){t&&o();return}n.value===void 0&&(n.value=document.body.style.overflow);let s=window.innerWidth-document.documentElement.clientWidth,c={padding:s,margin:0},l=i.scrollBody?.value?typeof i.scrollBody.value==`object`?L({padding:i.scrollBody.value.padding===!0?s:i.scrollBody.value.padding,margin:i.scrollBody.value.margin===!0?s:i.scrollBody.value.margin},c):c:{padding:0,margin:0};s>0&&(document.body.style.paddingRight=typeof l.padding==`number`?`${l.padding}px`:String(l.padding),document.body.style.marginRight=typeof l.margin==`number`?`${l.margin}px`:String(l.margin),document.documentElement.style.setProperty(`--scrollbar-width`,`${s}px`),document.body.style.overflow=`hidden`),le&&(a=se(document,`touchmove`,e=>_e(e),{passive:!1})),b(()=>{r.value&&(document.body.style.pointerEvents=`none`,document.body.style.overflow=`hidden`)})},{immediate:!0,flush:`sync`}),t});function he(e){let t=Math.random().toString(36).substring(2,7),n=me();n.value.set(t,e??!1);let r=O({get:()=>n.value.get(t)??!1,set:e=>n.value.set(t,e)});return u(()=>{n.value.delete(t)}),r}function ge(e){let t=window.getComputedStyle(e);if(t.overflowX===`scroll`||t.overflowY===`scroll`||t.overflowX===`auto`&&e.clientWidth<e.scrollWidth||t.overflowY===`auto`&&e.clientHeight<e.scrollHeight)return!0;{let t=e.parentNode;return!(t instanceof Element)||t.tagName===`BODY`?!1:ge(t)}}function _e(e){let t=e||window.event,n=t.target;return n instanceof Element&&ge(n)?!1:t.touches.length>1?!0:(t.preventDefault&&t.cancelable&&t.preventDefault(),!1)}function ve(t){let n=P({dir:e(`ltr`)});return O(()=>t?.value||n.dir?.value||`ltr`)}function R(e){let t=T(),n=t?.type.emits,r={};return n?.length||console.warn(`No emitted event found. Please check component: ${t?.type.__name}`),n?.forEach(t=>{r[f(i(t))]=(...n)=>e(t,...n)}),r}function z(e){let t=T(),n=Object.keys(t?.type.props??{}).reduce((e,n)=>{let r=(t?.type.props[n]).default;return r!==void 0&&(e[n]=r),e},{}),r=re(e);return O(()=>{let e={},a=t?.vnode.props??{};return Object.keys(a).forEach(t=>{e[i(t)]=a[t]}),Object.keys({...n,...e}).reduce((e,t)=>(r.value[t]!==void 0&&(e[t]=r.value[t]),e),{})})}function ye(e,t){let n=z(e),r=t?R(t):{};return O(()=>({...n.value,...r}))}var be=function(e){return typeof document>`u`?null:(Array.isArray(e)?e[0]:e).ownerDocument.body},B=new WeakMap,V=new WeakMap,H={},U=0,W=function(e){return e&&(e.host||W(e.parentNode))},xe=function(e,t){return t.map(function(t){if(e.contains(t))return t;var n=W(t);return n&&e.contains(n)?n:(console.error(`aria-hidden`,t,`in not contained inside`,e,`. Doing nothing`),null)}).filter(function(e){return!!e})},Se=function(e,t,n,r){var i=xe(t,Array.isArray(e)?e:[e]);H[n]||(H[n]=new WeakMap);var a=H[n],o=[],s=new Set,c=new Set(i),l=function(e){!e||s.has(e)||(s.add(e),l(e.parentNode))};i.forEach(l);var u=function(e){!e||c.has(e)||Array.prototype.forEach.call(e.children,function(e){if(s.has(e))u(e);else try{var t=e.getAttribute(r),i=t!==null&&t!==`false`,c=(B.get(e)||0)+1,l=(a.get(e)||0)+1;B.set(e,c),a.set(e,l),o.push(e),c===1&&i&&V.set(e,!0),l===1&&e.setAttribute(n,`true`),i||e.setAttribute(r,`true`)}catch(t){console.error(`aria-hidden: cannot operate on `,e,t)}})};return u(t),s.clear(),U++,function(){o.forEach(function(e){var t=B.get(e)-1,i=a.get(e)-1;B.set(e,t),a.set(e,i),t||(V.has(e)||e.removeAttribute(r),V.delete(e)),i||e.removeAttribute(n)}),U--,U||(B=new WeakMap,B=new WeakMap,V=new WeakMap,H={})}},Ce=function(e,t,n){n===void 0&&(n=`data-aria-hidden`);var r=Array.from(Array.isArray(e)?e:[e]),i=t||be(e);return i?(r.push.apply(r,Array.from(i.querySelectorAll(`[aria-live], script`))),Se(r,i,n,`aria-hidden`)):function(){return null}};function we(e){let t;x(()=>C(e),e=>{let n=!1;try{n=!!e?.closest(`[popover]:not(:popover-open)`)}catch{}e&&!n?t=Ce(e):t&&t()}),E(()=>{t&&t()})}var Te=0;function Ee(e,t=`reka`){if(e)return e;let n;return n=`useId`in o?ae?.():P({useId:void 0}).useId?.()??`${++Te}`,t?`${t}-${n}`:n}function G(t,n){let r=e(t);function i(e){return n[r.value][e]??r.value}return{state:r,dispatch:e=>{r.value=i(e)}}}function De(t,n){let r=e({}),i=e(`none`),a=e(t),o=t.value?`mounted`:`unmounted`,s,c=n.value?.ownerDocument.defaultView??oe,{state:l,dispatch:u}=G(o,{mounted:{UNMOUNT:`unmounted`,ANIMATION_OUT:`unmountSuspended`},unmountSuspended:{MOUNT:`mounted`,ANIMATION_END:`unmounted`},unmounted:{MOUNT:`mounted`}}),d=e=>{if(D){let t=new CustomEvent(e,{bubbles:!1,cancelable:!1});n.value?.dispatchEvent(t)}};x(t,async(e,t)=>{let a=t!==e;if(await b(),a){let a=i.value,o=K(n.value);e?(u(`MOUNT`),d(`enter`),o===`none`&&d(`after-enter`)):o===`none`||o===`undefined`||r.value?.display===`none`?(u(`UNMOUNT`),d(`leave`),d(`after-leave`)):t&&a!==o?(u(`ANIMATION_OUT`),d(`leave`)):(u(`UNMOUNT`),d(`after-leave`))}},{immediate:!0});let f=e=>{let t=K(n.value),r=t.includes(CSS.escape(e.animationName)),i=l.value===`mounted`?`enter`:`leave`;if(e.target===n.value&&r&&(d(`after-${i}`),u(`ANIMATION_END`),!a.value)){let e=n.value.style.animationFillMode;n.value.style.animationFillMode=`forwards`,s=c?.setTimeout(()=>{n.value?.style.animationFillMode===`forwards`&&(n.value.style.animationFillMode=e)})}e.target===n.value&&t===`none`&&u(`ANIMATION_END`)},p=e=>{e.target===n.value&&(i.value=K(n.value))},m=x(n,(e,t)=>{e?(r.value=getComputedStyle(e),e.addEventListener(`animationstart`,p),e.addEventListener(`animationcancel`,f),e.addEventListener(`animationend`,f)):(u(`ANIMATION_END`),s!==void 0&&c?.clearTimeout(s),t?.removeEventListener(`animationstart`,p),t?.removeEventListener(`animationcancel`,f),t?.removeEventListener(`animationend`,f))},{immediate:!0}),h=x(l,()=>{let e=K(n.value);i.value=l.value===`mounted`?e:`none`});return E(()=>{m(),h()}),{isPresent:O(()=>[`mounted`,`unmountSuspended`].includes(l.value))}}function K(e){return e&&getComputedStyle(e).animationName||`none`}var Oe=a({name:`Presence`,props:{present:{type:Boolean,required:!0},forceMount:{type:Boolean}},slots:{},setup(t,{slots:n,expose:r}){let{present:i,forceMount:a}=s(t),o=e(),{isPresent:c}=De(i,o);r({present:c});let l=n.default({present:c.value});l=ie(l||[]);let u=T();if(l&&l?.length>1){let e=u?.parent?.type.name?`<${u.parent.type.name} />`:`component`;throw Error([`Detected an invalid children for \`${e}\` for \`Presence\` component.`,``,"Note: Presence works similarly to `v-if` directly, but it waits for animation/transition to finished before unmounting. So it expect only one direct child of valid VNode type.",`You can apply a few solutions:`,["Provide a single child element so that `presence` directive attach correctly.",`Ensure the first child is an actual element instead of a raw text node or comment node.`].map(e=>` - ${e}`).join(`
2
+ `)].join(`
3
+ `))}return()=>a.value||i.value||c.value?m(n.default({present:c.value})[0],{ref:e=>{let t=C(e);return t?.hasAttribute===void 0||(t?.hasAttribute(`data-reka-popper-content-wrapper`)?o.value=t.firstElementChild:o.value=t),t}}):null}});function q(){let t=e();return{primitiveElement:t,currentElement:O(()=>[`#text`,`#comment`].includes(t.value?.$el.nodeName)?t.value?.$el.nextElementSibling:C(t))}}var ke=`dismissableLayer.pointerDownOutside`,Ae=`dismissableLayer.focusOutside`;function J(e,t){if(!(t instanceof Element))return!1;let n=t.closest(`[data-dismissable-layer]`),r=e.dataset.dismissableLayer===``?e:e.querySelector(`[data-dismissable-layer]`),i=Array.from(e.ownerDocument.querySelectorAll(`[data-dismissable-layer]`));return!!(n&&(r===n||i.indexOf(r)<i.indexOf(n)))}function je(t,n,r=!0){let i=n?.value?.ownerDocument??globalThis?.document,a=e(!1),o=e(()=>{});return g(e=>{if(!D||!c(r))return;let s=async e=>{let r=e.target;if(!(!n?.value||!r)){if(J(n.value,r)){a.value=!1;return}if(e.target&&!a.value){let n={originalEvent:e};function r(){M(ke,t,n)}e.pointerType===`touch`?(i.removeEventListener(`click`,o.value),o.value=r,i.addEventListener(`click`,o.value,{once:!0})):r()}else i.removeEventListener(`click`,o.value);a.value=!1}},l=window.setTimeout(()=>{i.addEventListener(`pointerdown`,s)},0);e(()=>{window.clearTimeout(l),i.removeEventListener(`pointerdown`,s),i.removeEventListener(`click`,o.value)})}),{onPointerDownCapture:()=>{c(r)&&(a.value=!0)}}}function Me(t,n,r=!0){let i=n?.value?.ownerDocument??globalThis?.document,a=e(!1);return g(e=>{if(!D||!c(r))return;let o=async e=>{if(!n?.value)return;await b(),await b();let r=e.target;!n.value||!r||J(n.value,r)||e.target&&!a.value&&M(Ae,t,{originalEvent:e})};i.addEventListener(`focusin`,o),e(()=>i.removeEventListener(`focusin`,o))}),{onFocusCapture:()=>{c(r)&&(a.value=!0)},onBlurCapture:()=>{c(r)&&(a.value=!1)}}}var Y=t({layersRoot:new Set,layersWithOutsidePointerEventsDisabled:new Set,originalBodyPointerEvents:void 0,branches:new Set}),Ne=a({__name:`DismissableLayer`,props:{disableOutsidePointerEvents:{type:Boolean,required:!1,default:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`escapeKeyDown`,`pointerDownOutside`,`focusOutside`,`interactOutside`,`dismiss`],setup(e,{emit:t}){let r=e,i=t,{forwardRef:a,currentElement:o}=k(),s=O(()=>o.value?.ownerDocument??globalThis.document),c=O(()=>Y.layersRoot),l=O(()=>o.value?Array.from(c.value).indexOf(o.value):-1),u=O(()=>Y.layersWithOutsidePointerEventsDisabled.size>0),f=O(()=>{let e=Array.from(c.value),[t]=[...Y.layersWithOutsidePointerEventsDisabled].slice(-1),n=e.indexOf(t);return l.value>=n}),p=je(async e=>{let t=[...Y.branches].some(t=>t?.contains(e.target));!f.value||t||(i(`pointerDownOutside`,e),i(`interactOutside`,e),await b(),e.defaultPrevented||i(`dismiss`))},o),m=Me(e=>{[...Y.branches].some(t=>t?.contains(e.target))||(i(`focusOutside`,e),i(`interactOutside`,e),e.defaultPrevented||i(`dismiss`))},o);return ee(`Escape`,e=>{l.value===c.value.size-1&&(i(`escapeKeyDown`,e),e.defaultPrevented||i(`dismiss`))}),g(e=>{o.value&&(r.disableOutsidePointerEvents&&(Y.layersWithOutsidePointerEventsDisabled.size===0&&(Y.originalBodyPointerEvents=s.value.body.style.pointerEvents,s.value.body.style.pointerEvents=`none`),Y.layersWithOutsidePointerEventsDisabled.add(o.value)),c.value.add(o.value),e(()=>{r.disableOutsidePointerEvents&&Y.layersWithOutsidePointerEventsDisabled.size===1&&!N(Y.originalBodyPointerEvents)&&(s.value.body.style.pointerEvents=Y.originalBodyPointerEvents)}))}),g(e=>{e(()=>{o.value&&(c.value.delete(o.value),Y.layersWithOutsidePointerEventsDisabled.delete(o.value))})}),(e,t)=>(v(),d(n(S),{ref:n(a),"as-child":e.asChild,as:e.as,"data-dismissable-layer":``,style:de({pointerEvents:u.value?f.value?`auto`:`none`:void 0}),onFocusCapture:n(m).onFocusCapture,onBlurCapture:n(m).onBlurCapture,onPointerdownCapture:n(p).onPointerDownCapture},{default:ue(()=>[y(e.$slots,`default`)]),_:3},8,[`as-child`,`as`,`style`,`onFocusCapture`,`onBlurCapture`,`onPointerdownCapture`]))}}),Pe=ne(()=>e([]));function Fe(){let e=Pe();return{add(t){let n=e.value[0];t!==n&&n?.pause(),e.value=X(e.value,t),e.value.unshift(t)},remove(t){e.value=X(e.value,t),e.value[0]?.resume()}}}function X(e,t){let n=[...e],r=n.indexOf(t);return r!==-1&&n.splice(r,1),n}var Z=`focusScope.autoFocusOnMount`,Q=`focusScope.autoFocusOnUnmount`,Ie={bubbles:!1,cancelable:!0};function Le(e,{select:t=!1}={}){let n=j();for(let r of e)if($(r,{select:t}),j()!==n)return!0}function Re(e){let t=ze(e);return[Be(t,e),Be(t.reverse(),e)]}function ze(e){let t=[],n=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT,{acceptNode:e=>{let t=e.tagName===`INPUT`&&e.type===`hidden`;return e.disabled||e.hidden||t?NodeFilter.FILTER_SKIP:e.tabIndex>=0?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP}});for(;n.nextNode();)t.push(n.currentNode);return t}function Be(e,t){for(let n of e)if(!Ve(n,{upTo:t}))return n}function Ve(e,{upTo:t}){if(getComputedStyle(e).visibility===`hidden`)return!0;for(;e;){if(t!==void 0&&e===t)return!1;if(getComputedStyle(e).display===`none`)return!0;e=e.parentElement}return!1}function He(e){return e instanceof HTMLInputElement&&`select`in e}function $(e,{select:t=!1}={}){if(e&&e.focus){let n=j();e.focus({preventScroll:!0}),e!==n&&He(e)&&t&&e.select()}}var Ue=a({__name:`FocusScope`,props:{loop:{type:Boolean,required:!1,default:!1},trapped:{type:Boolean,required:!1,default:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`mountAutoFocus`,`unmountAutoFocus`],setup(r,{emit:i}){let a=r,o=i,{currentRef:s,currentElement:c}=k(),l=e(null),u=Fe(),f=t({paused:!1,pause(){this.paused=!0},resume(){this.paused=!1}});g(e=>{if(!D)return;let t=c.value;if(!a.trapped)return;function n(e){if(f.paused||!t)return;let n=e.target;t.contains(n)?l.value=n:$(l.value,{select:!0})}function r(e){if(f.paused||!t)return;let n=e.relatedTarget;n!==null&&(t.contains(n)||$(l.value,{select:!0}))}function i(e){let n=l.value;n!==null&&e.some(e=>e.removedNodes.length>0)&&(t.contains(n)||$(t))}document.addEventListener(`focusin`,n),document.addEventListener(`focusout`,r);let o=new MutationObserver(i);t&&o.observe(t,{childList:!0,subtree:!0}),e(()=>{document.removeEventListener(`focusin`,n),document.removeEventListener(`focusout`,r),o.disconnect()})}),g(async e=>{let t=c.value;if(await b(),!t)return;u.add(f);let n=j();if(!t.contains(n)){let e=new CustomEvent(Z,Ie);t.addEventListener(Z,e=>o(`mountAutoFocus`,e)),t.dispatchEvent(e),e.defaultPrevented||(Le(ze(t),{select:!0}),j()===n&&$(t))}e(()=>{t.removeEventListener(Z,e=>o(`mountAutoFocus`,e));let e=new CustomEvent(Q,Ie),r=e=>{o(`unmountAutoFocus`,e)};t.addEventListener(Q,r),t.dispatchEvent(e),setTimeout(()=>{e.defaultPrevented||$(n??document.body,{select:!0}),t.removeEventListener(Q,r),u.remove(f)},0)})});function p(e){if(!a.loop&&!a.trapped||f.paused)return;let t=e.key===`Tab`&&!e.altKey&&!e.ctrlKey&&!e.metaKey,n=j();if(t&&n){let t=e.currentTarget,[r,i]=Re(t);r&&i?!e.shiftKey&&n===i?(e.preventDefault(),a.loop&&$(r,{select:!0})):e.shiftKey&&n===r&&(e.preventDefault(),a.loop&&$(i,{select:!0})):n===t&&e.preventDefault()}}return(e,t)=>(v(),d(n(S),{ref_key:`currentRef`,ref:s,tabindex:`-1`,"as-child":e.asChild,as:e.as,onKeydown:p},{default:ue(()=>[y(e.$slots,`default`)]),_:3},8,[`as-child`,`as`]))}}),We=[`Enter`,` `],Ge=[`ArrowDown`,`PageUp`,`Home`],Ke=[`ArrowUp`,`PageDown`,`End`];[...Ge,...Ke],[...We],[...We];function qe(e){return e?`open`:`closed`}function Je(e){let t=j();for(let n of e)if(n===t||(n.focus(),j()!==t))return}var Ye=a({__name:`Teleport`,props:{to:{type:null,required:!1,default:`body`},disabled:{type:Boolean,required:!1},defer:{type:Boolean,required:!1},forceMount:{type:Boolean,required:!1}},setup(e){let t=te();return(e,i)=>n(t)||e.forceMount?(v(),d(l,{key:0,to:e.to,disabled:e.disabled,defer:e.defer},[y(e.$slots,`default`)],8,[`to`,`disabled`,`defer`])):r(`v-if`,!0)}}),Xe=`data-reka-collection-item`;function Ze(t={}){let{key:n=``,isProvider:r=!1}=t,i=`${n}CollectionProvider`,o;if(r){let t=e(new Map);o={collectionRef:e(),itemMap:t},w(i,o)}else o=p(i);let s=(e=!1)=>{let t=o.collectionRef.value;if(!t)return[];let n=Array.from(t.querySelectorAll(`[${Xe}]`)),r=Array.from(o.itemMap.value.values()).sort((e,t)=>n.indexOf(e.ref)-n.indexOf(t.ref));return e?r:r.filter(e=>e.ref.dataset.disabled!==``)},c=a({name:`CollectionSlot`,inheritAttrs:!1,setup(e,{slots:t,attrs:n}){let{primitiveElement:r,currentElement:i}=q();return x(i,()=>{o.collectionRef.value=i.value}),()=>m(_,{ref:r,...n},t)}}),l=a({name:`CollectionItem`,inheritAttrs:!1,props:{value:{validator:()=>!0}},setup(e,{slots:t,attrs:n}){let{primitiveElement:r,currentElement:i}=q();return g(t=>{if(i.value){let n=ce(i.value);o.itemMap.value.set(n,{ref:i.value,value:e.value}),t(()=>o.itemMap.value.delete(n))}}),()=>m(_,{...n,[Xe]:``,ref:r},t)}});return{getItems:s,reactiveItems:O(()=>Array.from(o.itemMap.value.values())),itemMapSize:O(()=>o.itemMap.value.size),CollectionSlot:c,CollectionItem:l}}export{A as S,L as _,Ue as a,M as b,Oe as c,we as d,ye as f,he as g,ve as h,qe as i,G as l,R as m,Ye as n,Ne as o,z as p,Je as r,q as s,Ze as t,Ee as u,P as v,j as x,N as y};