llm-simple-router 0.10.7 → 0.10.8

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 (54) hide show
  1. package/dist/admin/providers.js +1 -0
  2. package/dist/core/container.d.ts +1 -0
  3. package/dist/core/container.js +1 -0
  4. package/dist/db/log-write-buffer.d.ts +43 -0
  5. package/dist/db/log-write-buffer.js +91 -0
  6. package/dist/db/logs.d.ts +8 -0
  7. package/dist/db/logs.js +32 -7
  8. package/dist/db/metrics.d.ts +10 -0
  9. package/dist/db/metrics.js +24 -3
  10. package/dist/db/migrations/045_add_metrics_composite_indexes.sql +8 -0
  11. package/dist/index.js +16 -0
  12. package/dist/metrics/metrics-extractor.js +8 -10
  13. package/dist/proxy/transport/stream.js +1 -3
  14. package/dist/utils/token-counter.d.ts +5 -0
  15. package/dist/utils/token-counter.js +23 -0
  16. package/frontend-dist/assets/{CardContent-CyvsM1dh.js → CardContent-B4vB3Kxw.js} +1 -1
  17. package/frontend-dist/assets/{CardTitle-CKxB6jLn.js → CardTitle-BSki67ff.js} +1 -1
  18. package/frontend-dist/assets/{Checkbox-RMorZJxv.js → Checkbox-dvUbwMMH.js} +1 -1
  19. package/frontend-dist/assets/{CollapsibleContent-lX7fgPmd.js → CollapsibleContent-DRIfmabo.js} +1 -1
  20. package/frontend-dist/assets/{CollapsibleTrigger-BGLyyoL-.js → CollapsibleTrigger-jbSdCpiC.js} +1 -1
  21. package/frontend-dist/assets/{Dashboard-CUUJmaq6.js → Dashboard-eXf2n3wh.js} +2 -2
  22. package/frontend-dist/assets/{Input-qvtpg1mU.js → Input-CbiXfK8n.js} +1 -1
  23. package/frontend-dist/assets/{Label-KbeWdVSH.js → Label-CoK65l1t.js} +1 -1
  24. package/frontend-dist/assets/{Login-CFEaXT7h.js → Login-NhjpJfPs.js} +1 -1
  25. package/frontend-dist/assets/{Logs-ItgRttZ7.js → Logs-X2RH5tcC.js} +1 -1
  26. package/frontend-dist/assets/{MappingEntryEditor-JtB6vvsH.js → MappingEntryEditor-CjHMOGsf.js} +1 -1
  27. package/frontend-dist/assets/{ModelCard-C0-g-WOb.js → ModelCard-DJwfR5SC.js} +1 -1
  28. package/frontend-dist/assets/{ModelMappings-RVrNS0aC.js → ModelMappings-C1yFuU0K.js} +1 -1
  29. package/frontend-dist/assets/{Monitor-3xt32Bbl.js → Monitor-4HZvjdBX.js} +1 -1
  30. package/frontend-dist/assets/{Providers-Ro-AjfzZ.js → Providers-Cw9Ba8PT.js} +1 -1
  31. package/frontend-dist/assets/{ProxyEnhancement-BlDtv_4M.js → ProxyEnhancement-PXI_P4Da.js} +1 -1
  32. package/frontend-dist/assets/{QuickSetup-lEC9e6wW.js → QuickSetup-CAmUIwv2.js} +1 -1
  33. package/frontend-dist/assets/{RetryRules-DJMh2I-9.js → RetryRules-D2ykGSLa.js} +1 -1
  34. package/frontend-dist/assets/{RouterKeys-DvXfMOW6.js → RouterKeys-BvvL8qcy.js} +1 -1
  35. package/frontend-dist/assets/{RovingFocusItem-CTJ2Vqf3.js → RovingFocusItem-gP6WU5WD.js} +1 -1
  36. package/frontend-dist/assets/{Schedules-DKWJFNUk.js → Schedules-DvfShW7S.js} +1 -1
  37. package/frontend-dist/assets/{Settings-DkIN3IsO.js → Settings-BnUEzcvF.js} +1 -1
  38. package/frontend-dist/assets/{Setup-DIGMoSiu.js → Setup-BIZZg-XO.js} +1 -1
  39. package/frontend-dist/assets/{Switch-EL-MDXz3.js → Switch-CXemfUY5.js} +1 -1
  40. package/frontend-dist/assets/{TooltipTrigger-B5rqpLBL.js → TooltipTrigger-BwLkoBZK.js} +1 -1
  41. package/frontend-dist/assets/{TransformRulesForm-CrYYvjo8.js → TransformRulesForm-0xoRLIOz.js} +1 -1
  42. package/frontend-dist/assets/{UnifiedRequestDialog-Dzes8usX.js → UnifiedRequestDialog-CFrJXdaw.js} +1 -1
  43. package/frontend-dist/assets/{VisuallyHiddenInput-CNa5CPMY.js → VisuallyHiddenInput-CvUbgFmg.js} +1 -1
  44. package/frontend-dist/assets/{button-Bq3kR6lP.js → button-BqYUfybJ.js} +2 -2
  45. package/frontend-dist/assets/{copy-B7rgYQq3.js → copy-AYfsh6Pt.js} +1 -1
  46. package/frontend-dist/assets/{dialog-XzDsW73J.js → dialog-CqEJe3JV.js} +1 -1
  47. package/frontend-dist/assets/{index-Ba1Z6tMV.js → index-DcD6M87r.js} +2 -2
  48. package/frontend-dist/assets/index-DeeDpH_W.css +1 -0
  49. package/frontend-dist/assets/{trash-2-DCsvygvC.js → trash-2-BNKlpO30.js} +1 -1
  50. package/frontend-dist/assets/{useClipboard-D32vuT2b.js → useClipboard-ByGWVweg.js} +1 -1
  51. package/frontend-dist/assets/{useLogRetention-D20-qsiv.js → useLogRetention-BASEOb38.js} +1 -1
  52. package/frontend-dist/index.html +6 -3
  53. package/package.json +1 -1
  54. package/frontend-dist/assets/index-C6lfMcX8.css +0 -1
@@ -138,6 +138,7 @@ export const adminProviderRoutes = (app, options, done) => {
138
138
  name: s.name,
139
139
  api_type: s.api_type,
140
140
  base_url: s.base_url,
141
+ upstream_path: s.upstream_path,
141
142
  api_key: s.api_key ? decrypt(s.api_key, encryptionKey) : "",
142
143
  models: buildModelInfoList(modelEntries, overrides),
143
144
  is_active: s.is_active,
@@ -10,6 +10,7 @@ export declare const SERVICE_KEYS: {
10
10
  readonly pluginRegistry: "pluginRegistry";
11
11
  readonly formatRegistry: "formatRegistry";
12
12
  readonly logFileWriter: "logFileWriter";
13
+ readonly logWriteBuffer: "logWriteBuffer";
13
14
  readonly proxyAgentFactory: "proxyAgentFactory";
14
15
  };
15
16
  export type ServiceKey = (typeof SERVICE_KEYS)[keyof typeof SERVICE_KEYS];
@@ -10,6 +10,7 @@ export const SERVICE_KEYS = {
10
10
  pluginRegistry: "pluginRegistry",
11
11
  formatRegistry: "formatRegistry",
12
12
  logFileWriter: "logFileWriter",
13
+ logWriteBuffer: "logWriteBuffer",
13
14
  proxyAgentFactory: "proxyAgentFactory",
14
15
  };
15
16
  /**
@@ -0,0 +1,43 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { RequestLogInsert, LogWriteContext } from "./logs.js";
3
+ import type { MetricsInsert } from "./metrics.js";
4
+ export interface LogWriteBufferOptions {
5
+ flushIntervalMs?: number;
6
+ maxBufferSize?: number;
7
+ }
8
+ /**
9
+ * 批量缓冲 DB 日志写入,减少 SQLite 事务频率。
10
+ *
11
+ * 透明使用:logs.ts / metrics.ts 内部判断 buffer 是否存在,
12
+ * 存在则 push 到缓冲,否则走原始同步路径。
13
+ *
14
+ * flush 策略:定时(100ms)+ 阈值(50 条)+ stop() 时同步 flush。
15
+ */
16
+ export declare class LogWriteBuffer {
17
+ private readonly db;
18
+ private readonly rawInsertLog;
19
+ private readonly rawInsertMetrics;
20
+ private buffer;
21
+ private flushTimer;
22
+ private readonly flushIntervalMs;
23
+ private readonly maxBufferSize;
24
+ private stopped;
25
+ constructor(db: Database.Database, rawInsertLog: (db: Database.Database, data: RequestLogInsert, ctx?: LogWriteContext) => void, rawInsertMetrics: (db: Database.Database, data: MetricsInsert & {
26
+ id: string;
27
+ }) => void, options?: LogWriteBufferOptions);
28
+ /** 推入日志条目到缓冲,达到阈值时立即 flush */
29
+ pushLog(data: RequestLogInsert, context?: LogWriteContext): void;
30
+ /**
31
+ * 推入 metrics 条目到缓冲。
32
+ * 调用方需预生成 UUID 并在 data.id 中传入。
33
+ */
34
+ pushMetrics(data: MetricsInsert & {
35
+ id: string;
36
+ }): void;
37
+ /** 同步批量写入所有缓冲条目(db.transaction 包裹) */
38
+ flush(): void;
39
+ /** 停止定时器 + 同步 flush 剩余数据。stop 后的 push 直接走同步写入。 */
40
+ stop(): void;
41
+ /** 当前缓冲区中的条目数(测试用) */
42
+ get pendingCount(): number;
43
+ }
@@ -0,0 +1,91 @@
1
+ const DEFAULT_FLUSH_INTERVAL_MS = 100;
2
+ const DEFAULT_MAX_BUFFER_SIZE = 50;
3
+ /**
4
+ * 批量缓冲 DB 日志写入,减少 SQLite 事务频率。
5
+ *
6
+ * 透明使用:logs.ts / metrics.ts 内部判断 buffer 是否存在,
7
+ * 存在则 push 到缓冲,否则走原始同步路径。
8
+ *
9
+ * flush 策略:定时(100ms)+ 阈值(50 条)+ stop() 时同步 flush。
10
+ */
11
+ export class LogWriteBuffer {
12
+ db;
13
+ rawInsertLog;
14
+ rawInsertMetrics;
15
+ buffer = [];
16
+ flushTimer = null;
17
+ flushIntervalMs;
18
+ maxBufferSize;
19
+ stopped = false;
20
+ constructor(db, rawInsertLog, rawInsertMetrics, options) {
21
+ this.db = db;
22
+ this.rawInsertLog = rawInsertLog;
23
+ this.rawInsertMetrics = rawInsertMetrics;
24
+ this.flushIntervalMs = options?.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
25
+ this.maxBufferSize = options?.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;
26
+ this.flushTimer = setInterval(() => {
27
+ this.flush();
28
+ }, this.flushIntervalMs);
29
+ // 不阻止进程退出
30
+ if (this.flushTimer && typeof this.flushTimer === "object" && "unref" in this.flushTimer) {
31
+ this.flushTimer.unref();
32
+ }
33
+ }
34
+ /** 推入日志条目到缓冲,达到阈值时立即 flush */
35
+ pushLog(data, context) {
36
+ if (this.stopped) {
37
+ this.rawInsertLog(this.db, data, context);
38
+ return;
39
+ }
40
+ this.buffer.push({ type: "log", data, context });
41
+ if (this.buffer.length >= this.maxBufferSize) {
42
+ this.flush();
43
+ }
44
+ }
45
+ /**
46
+ * 推入 metrics 条目到缓冲。
47
+ * 调用方需预生成 UUID 并在 data.id 中传入。
48
+ */
49
+ pushMetrics(data) {
50
+ if (this.stopped) {
51
+ this.rawInsertMetrics(this.db, data);
52
+ return;
53
+ }
54
+ this.buffer.push({ type: "metrics", data });
55
+ if (this.buffer.length >= this.maxBufferSize) {
56
+ this.flush();
57
+ }
58
+ }
59
+ /** 同步批量写入所有缓冲条目(db.transaction 包裹) */
60
+ flush() {
61
+ if (this.buffer.length === 0)
62
+ return;
63
+ // 取出当前缓冲,重置数组,避免 flush 期间新 push 的条目丢失
64
+ const entries = this.buffer;
65
+ this.buffer = [];
66
+ const transaction = this.db.transaction(() => {
67
+ for (const entry of entries) {
68
+ if (entry.type === "log") {
69
+ this.rawInsertLog(this.db, entry.data, entry.context);
70
+ }
71
+ else {
72
+ this.rawInsertMetrics(this.db, entry.data);
73
+ }
74
+ }
75
+ });
76
+ transaction();
77
+ }
78
+ /** 停止定时器 + 同步 flush 剩余数据。stop 后的 push 直接走同步写入。 */
79
+ stop() {
80
+ this.stopped = true;
81
+ if (this.flushTimer) {
82
+ clearInterval(this.flushTimer);
83
+ this.flushTimer = null;
84
+ }
85
+ this.flush();
86
+ }
87
+ /** 当前缓冲区中的条目数(测试用) */
88
+ get pendingCount() {
89
+ return this.buffer.length;
90
+ }
91
+ }
package/dist/db/logs.d.ts CHANGED
@@ -1,6 +1,7 @@
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";
4
5
  export interface RequestLog {
5
6
  id: string;
6
7
  api_type: string;
@@ -54,6 +55,13 @@ export interface LogWriteContext {
54
55
  logFileWriter?: LogFileWriter | null;
55
56
  responseBody?: string | null;
56
57
  }
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 };
57
65
  export declare function insertRequestLog(db: Database.Database, log: RequestLogInsert, writeContext?: LogWriteContext): void;
58
66
  export declare function getRequestLogs(db: Database.Database, options: {
59
67
  page: number;
package/dist/db/logs.js CHANGED
@@ -10,8 +10,32 @@ 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
+ /** 模块级缓冲实例,由 initLogBuffer 设置 */
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 逻辑(无缓冲) */
27
+ function rawInsertRequestLog(db, log, writeContext) {
28
+ // 详情保留判定
29
+ const preserveDetail = shouldPreserveDetail(log.status_code, writeContext?.responseBody ?? null, writeContext?.matcher ?? null, !!writeContext?.logFileWriter);
30
+ getCachedStmt(db, `INSERT INTO request_logs (id, api_type, model, provider_id, status_code, client_status_code, latency_ms,
31
+ is_stream, error_message, created_at, client_request, upstream_request, upstream_response,
32
+ is_retry, is_failover, original_request_id, router_key_id, original_model, session_id, pipeline_snapshot)
33
+ 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
+ }
35
+ // 导出给 LogWriteBuffer 的原始插入函数引用
36
+ export { rawInsertRequestLog };
13
37
  export function insertRequestLog(db, log, writeContext) {
14
- // 文件写入:始终写入全文
38
+ // 文件写入:始终同步调用(WriteStream 内部异步,不阻塞事件循环)
15
39
  if (writeContext?.logFileWriter) {
16
40
  writeContext.logFileWriter.write({
17
41
  id: log.id,
@@ -25,12 +49,13 @@ export function insertRequestLog(db, log, writeContext) {
25
49
  pipeline_snapshot: log.pipeline_snapshot ?? null,
26
50
  });
27
51
  }
28
- // 详情保留判定
29
- const preserveDetail = shouldPreserveDetail(log.status_code, writeContext?.responseBody ?? null, writeContext?.matcher ?? null, !!writeContext?.logFileWriter);
30
- getCachedStmt(db, `INSERT INTO request_logs (id, api_type, model, provider_id, status_code, client_status_code, latency_ms,
31
- is_stream, error_message, created_at, client_request, upstream_request, upstream_response,
32
- is_retry, is_failover, original_request_id, router_key_id, original_model, session_id, pipeline_snapshot)
33
- 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);
52
+ // DB INSERT:缓冲已初始化则走缓冲,否则走同步
53
+ if (logBuffer) {
54
+ logBuffer.pushLog(log, writeContext);
55
+ }
56
+ else {
57
+ rawInsertRequestLog(db, log, writeContext);
58
+ }
34
59
  }
35
60
  function buildLogWhereClause(options, baseCondition) {
36
61
  let where = baseCondition;
@@ -1,4 +1,5 @@
1
1
  import Database from "better-sqlite3";
2
+ import type { LogWriteBuffer } from "./log-write-buffer.js";
2
3
  export type MetricsPeriod = "1h" | "5h" | "6h" | "24h" | "7d" | "30d";
3
4
  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";
4
5
  export interface MetricsRow {
@@ -48,6 +49,15 @@ export type MetricsInsert = {
48
49
  non_thinking_tps?: number | null;
49
50
  total_tps?: number | null;
50
51
  };
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 };
51
61
  export declare function insertMetrics(db: Database.Database, m: MetricsInsert): string;
52
62
  export interface MetricsSummaryRow {
53
63
  provider_id: string;
@@ -1,14 +1,35 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import { MS_PER_SECOND } from "../core/constants.js";
3
3
  import { getCachedStmt } from "./helpers.js";
4
- export function insertMetrics(db, m) {
5
- const id = randomUUID();
4
+ /** 模块级缓冲实例,与 logs.ts 共享同一个 LogWriteBuffer */
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 逻辑(无缓冲) */
15
+ function rawInsertMetrics(db, m) {
6
16
  getCachedStmt(db, `INSERT INTO request_metrics (id, request_log_id, provider_id, backend_model, api_type, router_key_id, status_code,
7
17
  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,
8
18
  client_type, cache_read_tokens_estimated,
9
19
  thinking_tokens, text_tokens, tool_use_tokens, thinking_duration_ms,
10
20
  thinking_tps, total_tps, non_thinking_duration_ms, non_thinking_tps)
11
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(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);
21
+ 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
+ }
23
+ // 导出给 LogWriteBuffer 的原始插入函数引用
24
+ export { rawInsertMetrics };
25
+ export function insertMetrics(db, m) {
26
+ const id = randomUUID();
27
+ if (logBuffer) {
28
+ logBuffer.pushMetrics({ ...m, id });
29
+ }
30
+ else {
31
+ rawInsertMetrics(db, { ...m, id });
32
+ }
12
33
  return id;
13
34
  }
14
35
  const PERIOD_OFFSET = {
@@ -0,0 +1,8 @@
1
+ -- request_metrics 核心聚合索引(Dashboard getMetricsSummary + getMetricsTimeseries)
2
+ CREATE INDEX IF NOT EXISTS idx_metrics_agg
3
+ ON request_metrics(is_complete, created_at DESC, provider_id, backend_model);
4
+
5
+ -- request_logs 子请求+时间排序复合索引
6
+ -- 注:018 已有 idx_request_logs_original_request_id(original_request_id),本索引额外覆盖时间排序
7
+ CREATE INDEX IF NOT EXISTS idx_logs_original_time
8
+ ON request_logs(original_request_id, created_at DESC);
package/dist/index.js CHANGED
@@ -5,6 +5,11 @@ 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";
8
13
  import { HTTP_NOT_FOUND, HTTP_INTERNAL_ERROR, getProxyApiType } from "./core/constants.js";
9
14
  import { API_CODE, apiError, isAdminApiResponse, statusToApiCode } from "./admin/api-response.js";
10
15
  const PROVIDER_DEFAULT_QUEUE_TIMEOUT_MS = 5000;
@@ -214,6 +219,14 @@ export async function buildApp(options) {
214
219
  ? null
215
220
  : new LogFileWriter(logsDir, { enabled: getDetailLogEnabled(db) });
216
221
  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
+ }
217
230
  // 注册 AdaptiveController(依赖已注册的 semaphoreManager)
218
231
  container.register(SERVICE_KEYS.adaptiveController, (c) => {
219
232
  const ac = new AdaptiveController(c.resolve(SERVICE_KEYS.semaphoreManager), app.log);
@@ -330,6 +343,9 @@ export async function buildApp(options) {
330
343
  proxyAgentFactory.invalidateAll();
331
344
  const sessionTracker = container.resolve(SERVICE_KEYS.sessionTracker);
332
345
  sessionTracker.stop();
346
+ // 同步 flush DB 日志缓冲(在 flush 文件缓冲之前)
347
+ stopLogBuffer();
348
+ clearLogBuffer();
333
349
  // Flush LogFileWriter 的 WriteStream 缓冲数据到磁盘
334
350
  await logFileWriter?.stop();
335
351
  // 等待活跃代理请求自然完成,超时后强制关闭所有连接。
@@ -1,6 +1,6 @@
1
1
  // TODO: 当文件超过 400 行时拆分为 metrics-streaming.ts(流式事件处理 + TPS 计算)和 metrics-extractor.ts(非流式 + 类型)
2
2
  import { MS_PER_SECOND } from "../core/constants.js";
3
- import { countTokens } from "../utils/token-counter.js";
3
+ import { countTokensFromChunks } from "../utils/token-counter.js";
4
4
  export class MetricsExtractor {
5
5
  apiType;
6
6
  requestStartTime;
@@ -54,8 +54,8 @@ export class MetricsExtractor {
54
54
  let thinkingDurationMs = null;
55
55
  let textTokens = null;
56
56
  let toolUseTokens = null;
57
- const thinkingContent = this.thinkingChunks.join("");
58
- const hasThinking = thinkingContent.length > 0;
57
+ const thinkingTotalLength = this.thinkingTotalLength;
58
+ const hasThinking = thinkingTotalLength > 0;
59
59
  if (this.streamEndTime !== null &&
60
60
  this.outputTokens !== null) {
61
61
  // total_duration: T6 - T0 (proxy end-to-end, not just stream window)
@@ -64,7 +64,7 @@ export class MetricsExtractor {
64
64
  totalTps = this.outputTokens / (totalDurationMs / MS_PER_SECOND);
65
65
  }
66
66
  if (hasThinking) {
67
- thinkingTokens = countTokens(thinkingContent);
67
+ thinkingTokens = countTokensFromChunks(this.thinkingChunks);
68
68
  // thinking_duration: T3 - T0 (includes network RTT + generation)
69
69
  if (this.thinkingStreamEndTime !== null) {
70
70
  thinkingDurationMs = this.thinkingStreamEndTime - this.requestStartTime;
@@ -87,13 +87,11 @@ export class MetricsExtractor {
87
87
  }
88
88
  }
89
89
  // content token counts (for analysis only)
90
- const textContent = this.textChunks.join("");
91
- if (textContent.length > 0) {
92
- textTokens = countTokens(textContent);
90
+ if (this.textTotalLength > 0) {
91
+ textTokens = countTokensFromChunks(this.textChunks);
93
92
  }
94
- const toolUseContent = this.toolUseChunks.join("");
95
- if (toolUseContent.length > 0) {
96
- toolUseTokens = countTokens(toolUseContent);
93
+ if (this.toolUseTotalLength > 0) {
94
+ toolUseTokens = countTokensFromChunks(this.toolUseChunks);
97
95
  }
98
96
  }
99
97
  return {
@@ -20,7 +20,6 @@ class StreamProxy {
20
20
  resolveFn = null;
21
21
  pendingResult = null;
22
22
  bufferChunks = [];
23
- captureChunks = [];
24
23
  totalBuffered = 0;
25
24
  lastChunkEndedWithNewline = false;
26
25
  idleTimer = null;
@@ -208,7 +207,6 @@ class StreamProxy {
208
207
  return;
209
208
  this.resetIdleTimer();
210
209
  if (this.state === "BUFFERING") {
211
- this.captureChunks.push(chunk);
212
210
  this.bufferChunks.push(chunk);
213
211
  this.totalBuffered += chunk.length;
214
212
  // 快速路径:检查大小限制(无需 concat)
@@ -281,7 +279,7 @@ class StreamProxy {
281
279
  if (this.idleTimer)
282
280
  clearTimeout(this.idleTimer);
283
281
  if (this.state === "BUFFERING" && this.checkEarlyError) {
284
- const text = Buffer.concat(this.captureChunks).toString("utf-8");
282
+ const text = Buffer.concat(this.bufferChunks).toString("utf-8");
285
283
  if (this.checkEarlyError(text)) {
286
284
  this.transition("EARLY_ERROR");
287
285
  this.terminal("stream_error", { body: text });
@@ -1,5 +1,10 @@
1
1
  /** 使用 gpt-tokenizer (o200k_base) 估算文本的 token 数 */
2
2
  export declare function countTokens(text: string): number;
3
+ /**
4
+ * 从 string chunks 中估算 token 数,避免 join 成完整大字符串。
5
+ * 累积到 SAMPLE_SIZE 字符后停止拼接,用采样外推。
6
+ */
7
+ export declare function countTokensFromChunks(chunks: string[]): number;
3
8
  /** 从请求体中提取所有需要计算 token 的文本 */
4
9
  export declare function extractAllText(body: Record<string, unknown>): string;
5
10
  /**
@@ -11,6 +11,29 @@ export function countTokens(text) {
11
11
  const sampleTokens = encode(sample).length;
12
12
  return Math.ceil((sampleTokens / sample.length) * text.length);
13
13
  }
14
+ /**
15
+ * 从 string chunks 中估算 token 数,避免 join 成完整大字符串。
16
+ * 累积到 SAMPLE_SIZE 字符后停止拼接,用采样外推。
17
+ */
18
+ export function countTokensFromChunks(chunks) {
19
+ if (chunks.length === 0)
20
+ return 0;
21
+ let combined = "";
22
+ let totalChars = 0;
23
+ for (const chunk of chunks) {
24
+ totalChars += chunk.length;
25
+ if (combined.length < SAMPLE_SIZE) {
26
+ combined += combined.length + chunk.length <= SAMPLE_SIZE
27
+ ? chunk
28
+ : chunk.slice(0, SAMPLE_SIZE - combined.length);
29
+ }
30
+ }
31
+ if (totalChars <= SAMPLE_SIZE) {
32
+ return countTokens(combined);
33
+ }
34
+ const sampleTokens = countTokens(combined);
35
+ return Math.ceil(sampleTokens * (totalChars / combined.length));
36
+ }
14
37
  /** 从 message content 中提取文本(兼容 OpenAI 和 Anthropic 格式) */
15
38
  function extractTextFromContent(content) {
16
39
  if (typeof content === "string")
@@ -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-Bq3kR6lP.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
+ import{Ut as e,Vt as t,Z as n,et as r,ft as i,ht as a,r as o}from"./button-BqYUfybJ.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-Bq3kR6lP.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
+ import{Ut as e,Vt as t,Z as n,et as r,ft as i,ht as a,r as o}from"./button-BqYUfybJ.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-Bq3kR6lP.js";import{t as b}from"./VisuallyHiddenInput-CNa5CPMY.js";import{t as x}from"./RovingFocusItem-CTJ2Vqf3.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-Ba1Z6tMV.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};
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-BqYUfybJ.js";import{t as b}from"./VisuallyHiddenInput-CvUbgFmg.js";import{t as x}from"./RovingFocusItem-gP6WU5WD.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-DcD6M87r.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};
@@ -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-Bq3kR6lP.js";import{B as x,L as S,q as C,z as w}from"./index-Ba1Z6tMV.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};
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-BqYUfybJ.js";import{B as x,L as S,q as C,z as w}from"./index-DcD6M87r.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};
@@ -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-Bq3kR6lP.js";import{r as l}from"./CollapsibleContent-lX7fgPmd.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};
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-BqYUfybJ.js";import{r as l}from"./CollapsibleContent-DRIfmabo.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};