llm-simple-router 0.4.3 → 0.5.0

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 (99) hide show
  1. package/README.md +67 -18
  2. package/dist/admin/monitor.js +8 -0
  3. package/dist/admin/routes.js +2 -0
  4. package/dist/admin/settings-import-export.d.ts +11 -0
  5. package/dist/admin/settings-import-export.js +144 -0
  6. package/dist/admin/settings.js +40 -2
  7. package/dist/db/db-size-monitor.d.ts +22 -0
  8. package/dist/db/db-size-monitor.js +81 -0
  9. package/dist/db/index.d.ts +4 -1
  10. package/dist/db/index.js +4 -1
  11. package/dist/db/log-cleaner.js +16 -7
  12. package/dist/db/logs.d.ts +9 -1
  13. package/dist/db/logs.js +55 -5
  14. package/dist/db/migrations/022_add_session_id_and_incremental_vacuum.sql +5 -0
  15. package/dist/db/settings.d.ts +4 -0
  16. package/dist/db/settings.js +18 -1
  17. package/dist/index.js +5 -0
  18. package/dist/metrics/metrics-extractor.js +4 -0
  19. package/dist/monitor/request-tracker.d.ts +2 -0
  20. package/dist/monitor/request-tracker.js +20 -1
  21. package/dist/monitor/types.d.ts +1 -0
  22. package/dist/proxy/log-helpers.d.ts +2 -0
  23. package/dist/proxy/log-helpers.js +4 -2
  24. package/dist/proxy/model-state.d.ts +2 -0
  25. package/dist/proxy/model-state.js +4 -0
  26. package/dist/proxy/orchestrator.d.ts +2 -0
  27. package/dist/proxy/orchestrator.js +1 -0
  28. package/dist/proxy/proxy-handler.js +66 -34
  29. package/dist/proxy/proxy-logging.d.ts +2 -1
  30. package/dist/proxy/proxy-logging.js +5 -1
  31. package/dist/proxy/semaphore.d.ts +2 -0
  32. package/dist/proxy/semaphore.js +11 -0
  33. package/frontend-dist/assets/{CardContent-BIgWZo6N.js → CardContent-CIO85eT6.js} +1 -1
  34. package/frontend-dist/assets/{CardTitle-BVVp36Pq.js → CardTitle-DiqIReMT.js} +1 -1
  35. package/frontend-dist/assets/Checkbox-C2u5pIp4.js +1 -0
  36. package/frontend-dist/assets/CollapsibleTrigger-RKFL41om.js +1 -0
  37. package/frontend-dist/assets/Collection-iiNnuTQj.js +1 -0
  38. package/frontend-dist/assets/{Dashboard-D9ZMKiZw.js → Dashboard-DOEqP6gF.js} +1 -1
  39. package/frontend-dist/assets/DialogTitle-CEqndrf6.js +1 -0
  40. package/frontend-dist/assets/{Input-BA5UO-Ab.js → Input-l5ZurXX5.js} +1 -1
  41. package/frontend-dist/assets/{Label-CmG21jDR.js → Label-PgGtS8v2.js} +1 -1
  42. package/frontend-dist/assets/{Login-Dqwj0s5l.js → Login-DaN6ZcCx.js} +1 -1
  43. package/frontend-dist/assets/Logs-CleRQ7Xk.js +1 -0
  44. package/frontend-dist/assets/{ModelMappings-qxaFWRsq.js → ModelMappings-CacA_ua_.js} +1 -1
  45. package/frontend-dist/assets/{Monitor-Zy3O9UQA.js → Monitor-LSMFOBN2.js} +1 -1
  46. package/frontend-dist/assets/PopperContent-zLFHqQP0.js +1 -0
  47. package/frontend-dist/assets/{Providers-DRMSjTlb.js → Providers-NT5MUDU0.js} +1 -1
  48. package/frontend-dist/assets/{ProxyEnhancement-CtlDQ_oK.js → ProxyEnhancement-DhOy8nNy.js} +1 -1
  49. package/frontend-dist/assets/{RetryRules-CxbZt6dv.js → RetryRules-7arWa3jB.js} +1 -1
  50. package/frontend-dist/assets/{RouterKeys-drAASJqH.js → RouterKeys-CdaZunRg.js} +1 -1
  51. package/frontend-dist/assets/SelectValue-CSg-MKW_.js +1 -0
  52. package/frontend-dist/assets/Settings-1ntV9XE3.js +6 -0
  53. package/frontend-dist/assets/{Setup-CfscyFFo.js → Setup-CXLTDhYJ.js} +1 -1
  54. package/frontend-dist/assets/Switch-DivrIFE3.js +1 -0
  55. package/frontend-dist/assets/TableHeader-Bn0bodWx.js +1 -0
  56. package/frontend-dist/assets/TabsContent-MWvOH_LJ.js +1 -0
  57. package/frontend-dist/assets/TabsTrigger-WKkUfO2M.js +1 -0
  58. package/frontend-dist/assets/Teleport-B0PNXZbP.js +3 -0
  59. package/frontend-dist/assets/UnifiedRequestDialog-B2nt8nLl.css +1 -0
  60. package/frontend-dist/assets/UnifiedRequestDialog-Ba2e7YuJ.js +3 -0
  61. package/frontend-dist/assets/{VisuallyHidden-ppNQPfSR.js → VisuallyHidden-BwwTtzb9.js} +1 -1
  62. package/frontend-dist/assets/{VisuallyHiddenInput-DOdhvOHf.js → VisuallyHiddenInput-EGZSP7s8.js} +1 -1
  63. package/frontend-dist/assets/alert-dialog-CS1yFhdV.js +1 -0
  64. package/frontend-dist/assets/{badge-CcgNasyU.js → badge-C-QcC5n2.js} +1 -1
  65. package/frontend-dist/assets/{button-BkhL20Qz.js → button-Dbz2Be22.js} +2 -2
  66. package/frontend-dist/assets/{createLucideIcon-D2A5NyBH.js → createLucideIcon-Biq59l_W.js} +1 -1
  67. package/frontend-dist/assets/dialog-Cr0YQlLW.js +1 -0
  68. package/frontend-dist/assets/{file-text-D-Vekfc6.js → file-text-DoRW0hQW.js} +1 -1
  69. package/frontend-dist/assets/index-0H2uCGbx.js +1 -0
  70. package/frontend-dist/assets/index-D-cdVNCb.css +1 -0
  71. package/frontend-dist/assets/{lib-BAWhmkfz.js → lib-B0lieqgg.js} +1 -1
  72. package/frontend-dist/assets/{ohash.D__AXeF1-B5oxGXRD.js → ohash.D__AXeF1-BGxYMs6k.js} +1 -1
  73. package/frontend-dist/assets/{useClipboard-D3mHD2V6.js → useClipboard-vaHkvJHw.js} +1 -1
  74. package/frontend-dist/assets/{useForwardExpose-Pf45y-q1.js → useForwardExpose-C2_ks3sW.js} +1 -1
  75. package/frontend-dist/assets/useLogRetention-Cs_fiKql.js +1 -0
  76. package/frontend-dist/assets/useNonce-C9do0jOI.js +1 -0
  77. package/frontend-dist/assets/x-BlTnH_0_.js +1 -0
  78. package/frontend-dist/index.html +8 -8
  79. package/package.json +1 -1
  80. package/frontend-dist/assets/Checkbox-CnvHc21k.js +0 -1
  81. package/frontend-dist/assets/CollapsibleTrigger-DG8JZQfm.js +0 -1
  82. package/frontend-dist/assets/Collection-B4yfvPwd.js +0 -3
  83. package/frontend-dist/assets/DialogTitle-C7Wl7fWN.js +0 -1
  84. package/frontend-dist/assets/Logs-CGzGzcDa.js +0 -1
  85. package/frontend-dist/assets/PopperContent-B48BBgdW.js +0 -1
  86. package/frontend-dist/assets/SelectValue-Ct22fmR7.js +0 -1
  87. package/frontend-dist/assets/Switch-ROnHu48E.js +0 -1
  88. package/frontend-dist/assets/TableHeader-R3y9M8qo.js +0 -1
  89. package/frontend-dist/assets/TabsContent-CtT2wglf.js +0 -1
  90. package/frontend-dist/assets/TabsTrigger-D2S77VwS.js +0 -1
  91. package/frontend-dist/assets/UnifiedRequestDialog-BhGWzOYY.js +0 -3
  92. package/frontend-dist/assets/UnifiedRequestDialog-CotaDyW7.css +0 -1
  93. package/frontend-dist/assets/alert-dialog-BbI_a1Xz.js +0 -1
  94. package/frontend-dist/assets/dialog-BeetdYQi.js +0 -1
  95. package/frontend-dist/assets/index-Bgqc9Lca.js +0 -1
  96. package/frontend-dist/assets/index-hzTjKE_2.css +0 -1
  97. package/frontend-dist/assets/useNonce-Dfbnp6Co.js +0 -1
  98. package/frontend-dist/assets/x-CrCHzImd.js +0 -1
  99. /package/frontend-dist/assets/{format-CPdJtjZ5.js → format-DOVIVsQC.js} +0 -0
package/dist/db/logs.js CHANGED
@@ -4,14 +4,14 @@ const LOG_LIST_SELECT = `rl.id, rl.api_type, rl.model, rl.provider_id, rl.status
4
4
  rl.is_stream, rl.error_message, rl.created_at, rl.is_retry, rl.is_failover, rl.original_request_id, rl.original_model,
5
5
  CASE WHEN rl.provider_id = 'router' THEN rl.upstream_request ELSE NULL END AS upstream_request,
6
6
  rl.input_tokens, rl.output_tokens, rl.cache_read_tokens, rl.ttft_ms, rl.tokens_per_second, rl.stop_reason,
7
- rl.backend_model, rl.metrics_complete,
7
+ rl.backend_model, rl.metrics_complete, rl.session_id,
8
8
  COALESCE(p.name, rl.provider_id) AS provider_name`;
9
9
  const LOG_LIST_JOIN = `LEFT JOIN providers p ON p.id = rl.provider_id`;
10
10
  export function insertRequestLog(db, log) {
11
11
  db.prepare(`INSERT INTO request_logs (id, api_type, model, provider_id, status_code, latency_ms,
12
12
  is_stream, error_message, created_at, client_request, upstream_request, upstream_response,
13
- is_retry, is_failover, original_request_id, router_key_id, original_model)
14
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(log.id, log.api_type, log.model, log.provider_id, log.status_code, log.latency_ms, log.is_stream, log.error_message, log.created_at, log.client_request ?? null, log.upstream_request ?? null, log.upstream_response ?? null, log.is_retry ?? 0, log.is_failover ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null);
13
+ is_retry, is_failover, original_request_id, router_key_id, original_model, session_id)
14
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(log.id, log.api_type, log.model, log.provider_id, log.status_code, log.latency_ms, log.is_stream, log.error_message, log.created_at, log.client_request ?? null, log.upstream_request ?? null, log.upstream_response ?? null, log.is_retry ?? 0, log.is_failover ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null, log.session_id ?? null);
15
15
  }
16
16
  function buildLogWhereClause(options, baseCondition) {
17
17
  let where = baseCondition;
@@ -55,7 +55,9 @@ export function getRequestLogs(db, options) {
55
55
  return { data, total };
56
56
  }
57
57
  export function getRequestLogById(db, id) {
58
- return db.prepare("SELECT * FROM request_logs WHERE id = ?").get(id);
58
+ return db.prepare(`SELECT rl.*, COALESCE(p.name, rl.provider_id) AS provider_name
59
+ FROM request_logs rl LEFT JOIN providers p ON p.id = rl.provider_id
60
+ WHERE rl.id = ?`).get(id);
59
61
  }
60
62
  /** 双写:collectTransportMetrics 写 request_metrics 的同时,更新 request_logs 的冗余列 */
61
63
  export function updateLogMetrics(db, logId, m) {
@@ -89,7 +91,55 @@ export function backfillMetricsFromRequestMetrics(db) {
89
91
  `).run().changes;
90
92
  }
91
93
  export function deleteLogsBefore(db, beforeDate) {
92
- return db.prepare("DELETE FROM request_logs WHERE created_at < ?").run(beforeDate).changes;
94
+ const changes = db.prepare("DELETE FROM request_logs WHERE created_at < ?").run(beforeDate).changes;
95
+ if (changes > 0) {
96
+ db.pragma("incremental_vacuum");
97
+ }
98
+ return changes;
99
+ }
100
+ /** 每行元数据(数字列+索引)的估算字节数 */
101
+ const ROW_METADATA_BYTES = 500;
102
+ /** 估算 request_logs 表占用字节数 */
103
+ export function estimateLogTableSize(db) {
104
+ const row = db.prepare(`
105
+ SELECT COALESCE(SUM(
106
+ COALESCE(length(client_request), 0) + COALESCE(length(upstream_request), 0) +
107
+ COALESCE(length(upstream_response), 0) + COALESCE(length(stream_text_content), 0) +
108
+ COALESCE(length(error_message), 0) + ?
109
+ ), 0) as size
110
+ FROM request_logs
111
+ `).get(ROW_METADATA_BYTES);
112
+ return row.size;
113
+ }
114
+ const DELETE_BATCH_SIZE = 1000;
115
+ /** 删除最旧的日志,保留 keepCount 条,返回实际删除条数。分批删除避免长时间锁表 */
116
+ export function deleteOldestLogs(db, keepCount) {
117
+ const total = db.prepare("SELECT count(*) as c FROM request_logs").get().c;
118
+ const toDelete = Math.max(0, total - keepCount);
119
+ if (toDelete === 0)
120
+ return 0;
121
+ let totalDeleted = 0;
122
+ const stmt = db.prepare(`
123
+ DELETE FROM request_logs
124
+ WHERE rowid IN (
125
+ SELECT rowid FROM request_logs ORDER BY created_at ASC LIMIT ?
126
+ )
127
+ `);
128
+ while (totalDeleted < toDelete) {
129
+ const batchSize = Math.min(DELETE_BATCH_SIZE, toDelete - totalDeleted);
130
+ const result = stmt.run(batchSize);
131
+ totalDeleted += result.changes;
132
+ if (result.changes < batchSize)
133
+ break;
134
+ }
135
+ if (totalDeleted > 0) {
136
+ db.pragma("incremental_vacuum");
137
+ }
138
+ return totalDeleted;
139
+ }
140
+ /** 获取 request_logs 总行数 */
141
+ export function getLogCount(db) {
142
+ return db.prepare("SELECT count(*) as c FROM request_logs").get().c;
93
143
  }
94
144
  /** 查询某条日志的子请求(retry/failover 关联),上限 100 条 */
95
145
  export function getRequestLogChildren(db, parentId) {
@@ -0,0 +1,5 @@
1
+ ALTER TABLE request_logs ADD COLUMN session_id TEXT;
2
+
3
+ -- NOTE: PRAGMA auto_vacuum only takes effect after VACUUM for existing databases.
4
+ -- Run scripts/vacuum-migrate.js for existing deployments.
5
+ PRAGMA auto_vacuum = 2;
@@ -4,3 +4,7 @@ export declare function setSetting(db: Database.Database, key: string, value: st
4
4
  export declare function isInitialized(db: Database.Database): boolean;
5
5
  export declare function getLogRetentionDays(db: Database.Database): number;
6
6
  export declare function setLogRetentionDays(db: Database.Database, days: number): void;
7
+ export declare function getDbMaxSizeMb(db: Database.Database): number;
8
+ export declare function setDbMaxSizeMb(db: Database.Database, mb: number): void;
9
+ export declare function getLogTableMaxSizeMb(db: Database.Database): number;
10
+ export declare function setLogTableMaxSizeMb(db: Database.Database, mb: number): void;
@@ -10,8 +10,25 @@ export function isInitialized(db) {
10
10
  }
11
11
  export function getLogRetentionDays(db) {
12
12
  const val = getSetting(db, "log_retention_days");
13
- return val ? parseInt(val, 10) : 3;
13
+ const DEFAULT_LOG_RETENTION_DAYS = 3;
14
+ return val ? parseInt(val, 10) : DEFAULT_LOG_RETENTION_DAYS;
14
15
  }
15
16
  export function setLogRetentionDays(db, days) {
16
17
  setSetting(db, "log_retention_days", String(days));
17
18
  }
19
+ const DEFAULT_DB_MAX_SIZE_MB = 1024;
20
+ const DEFAULT_LOG_TABLE_MAX_SIZE_MB = 800;
21
+ export function getDbMaxSizeMb(db) {
22
+ const val = getSetting(db, "db_max_size_mb");
23
+ return val ? parseInt(val, 10) : DEFAULT_DB_MAX_SIZE_MB;
24
+ }
25
+ export function setDbMaxSizeMb(db, mb) {
26
+ setSetting(db, "db_max_size_mb", String(mb));
27
+ }
28
+ export function getLogTableMaxSizeMb(db) {
29
+ const val = getSetting(db, "log_table_max_size_mb");
30
+ return val ? parseInt(val, 10) : DEFAULT_LOG_TABLE_MAX_SIZE_MB;
31
+ }
32
+ export function setLogTableMaxSizeMb(db, mb) {
33
+ setSetting(db, "log_table_max_size_mb", String(mb));
34
+ }
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ import { RequestTracker } from "./monitor/request-tracker.js";
33
33
  import { modelState } from "./proxy/model-state.js";
34
34
  import { UsageWindowTracker } from "./proxy/usage-window-tracker.js";
35
35
  import { scheduleLogCleanup } from "./db/log-cleaner.js";
36
+ import { scheduleDbSizeMonitor } from "./db/db-size-monitor.js";
36
37
  import fastifyStatic from "@fastify/static";
37
38
  export async function buildApp(options) {
38
39
  const config = options?.config ?? getBaseConfig();
@@ -184,12 +185,16 @@ export async function buildApp(options) {
184
185
  return { status: "ok" };
185
186
  });
186
187
  const logCleanup = scheduleLogCleanup(db, app.log);
188
+ const dbSizeMonitor = scheduleDbSizeMonitor(db, config.DB_PATH, {
189
+ log: app.log,
190
+ });
187
191
  return {
188
192
  app,
189
193
  db,
190
194
  usageWindowTracker,
191
195
  close: async () => {
192
196
  logCleanup.stop();
197
+ dbSizeMonitor.stop();
193
198
  tracker.stopPushInterval();
194
199
  await app.close();
195
200
  db.close();
@@ -92,6 +92,10 @@ export class MetricsExtractor {
92
92
  else if (type === "message_delta") {
93
93
  const msg = parsed;
94
94
  this.outputTokens = msg.usage?.output_tokens ?? null;
95
+ // 第三方 Anthropic 兼容 API(如 OpenRouter、智谱)可能将 input_tokens 放在 message_delta 而非 message_start
96
+ if (this.inputTokens === null && msg.usage?.input_tokens) {
97
+ this.inputTokens = msg.usage.input_tokens;
98
+ }
95
99
  this.stopReason = msg.delta?.stop_reason ?? null;
96
100
  this.streamEndTime = Date.now();
97
101
  }
@@ -34,6 +34,8 @@ export declare class RequestTracker {
34
34
  getActive(): ActiveRequest[];
35
35
  getRecent(limit?: number): ActiveRequest[];
36
36
  get(id: string): ActiveRequest | undefined;
37
+ /** Public alias for API endpoint use — returns full request data including clientRequest */
38
+ getRequestById(id: string): ActiveRequest | undefined;
37
39
  getStats(): StatsSnapshot;
38
40
  getConcurrency(): ProviderConcurrencySnapshot[];
39
41
  getRuntime(): RuntimeMetrics;
@@ -135,6 +135,10 @@ export class RequestTracker {
135
135
  get(id) {
136
136
  return this.activeMap.get(id) ?? this.recentCompleted.find((r) => r.id === id);
137
137
  }
138
+ /** Public alias for API endpoint use — returns full request data including clientRequest */
139
+ getRequestById(id) {
140
+ return this.get(id);
141
+ }
138
142
  // --- Stats / monitoring ---
139
143
  getStats() {
140
144
  return this.statsAggregator.getStats();
@@ -195,7 +199,22 @@ export class RequestTracker {
195
199
  }
196
200
  }
197
201
  broadcast(event, data) {
198
- const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
202
+ // Strip clientRequest from broadcasts to reduce bandwidth;
203
+ // full data available on-demand via API endpoint
204
+ let payload = data;
205
+ if (event === "request_update" && Array.isArray(data)) {
206
+ payload = data.map((req) => {
207
+ const copy = { ...req };
208
+ delete copy.clientRequest;
209
+ return copy;
210
+ });
211
+ }
212
+ else if (event === "request_complete" && data && typeof data === "object") {
213
+ const copy = { ...data };
214
+ delete copy.clientRequest;
215
+ payload = copy;
216
+ }
217
+ const msg = `event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`;
199
218
  const clientCount = this.clients.size;
200
219
  let sentCount = 0;
201
220
  for (const client of this.clients) {
@@ -28,6 +28,7 @@ export interface ActiveRequest {
28
28
  streamContent?: StreamContentSnapshot;
29
29
  clientIp?: string;
30
30
  sessionId?: string;
31
+ clientRequest?: string;
31
32
  completedAt?: number;
32
33
  }
33
34
  export interface AttemptSnapshot {
@@ -24,6 +24,7 @@ export interface RequestLogParams extends LogRetryMeta {
24
24
  upHdrs: Record<string, string>;
25
25
  routerKeyId?: string | null;
26
26
  originalModel?: string | null;
27
+ sessionId?: string | null;
27
28
  }
28
29
  /** 插入成功请求日志,供 openai/anthropic 插件共享 */
29
30
  export declare function insertSuccessLog(db: Database.Database, params: RequestLogParams): void;
@@ -41,6 +42,7 @@ export interface RejectedLogParams extends LogRetryMeta {
41
42
  clientHeaders: RawHeaders;
42
43
  providerId?: string | null;
43
44
  originalModel?: string | null;
45
+ sessionId?: string | null;
44
46
  }
45
47
  /** Log a request rejected before reaching upstream */
46
48
  export declare function insertRejectedLog(params: RejectedLogParams): void;
@@ -1,7 +1,7 @@
1
1
  import { insertRequestLog } from "../db/index.js";
2
2
  /** 插入成功请求日志,供 openai/anthropic 插件共享 */
3
3
  export function insertSuccessLog(db, params) {
4
- const { id: logId, apiType, model, provider, isStream, startTime, clientReq, upstreamReq, status, respBody, upHdrs, isRetry = false, isFailover = false, originalRequestId = null, routerKeyId = null, originalModel = null } = params;
4
+ const { id: logId, apiType, model, provider, isStream, startTime, clientReq, upstreamReq, status, respBody, upHdrs, isRetry = false, isFailover = false, originalRequestId = null, routerKeyId = null, originalModel = null, sessionId = null } = params;
5
5
  insertRequestLog(db, {
6
6
  id: logId, api_type: apiType, model, provider_id: provider.id,
7
7
  status_code: status, latency_ms: Date.now() - startTime,
@@ -11,11 +11,12 @@ export function insertSuccessLog(db, params) {
11
11
  upstream_response: JSON.stringify({ statusCode: status, headers: upHdrs, body: respBody }),
12
12
  is_retry: isRetry ? 1 : 0, is_failover: isFailover ? 1 : 0, original_request_id: originalRequestId,
13
13
  router_key_id: routerKeyId, original_model: originalModel,
14
+ session_id: sessionId,
14
15
  });
15
16
  }
16
17
  /** Log a request rejected before reaching upstream */
17
18
  export function insertRejectedLog(params) {
18
- const { db, logId, apiType, model, statusCode, errorMessage, startTime, isStream, routerKeyId, originalBody, clientHeaders, providerId = null, isFailover = false, originalRequestId = null, originalModel = null } = params;
19
+ const { db, logId, apiType, model, statusCode, errorMessage, startTime, isStream, routerKeyId, originalBody, clientHeaders, providerId = null, isFailover = false, originalRequestId = null, originalModel = null, sessionId = null } = params;
19
20
  insertRequestLog(db, {
20
21
  id: logId,
21
22
  api_type: apiType,
@@ -31,5 +32,6 @@ export function insertRejectedLog(params) {
31
32
  original_request_id: originalRequestId,
32
33
  router_key_id: routerKeyId,
33
34
  original_model: originalModel,
35
+ session_id: sessionId,
34
36
  });
35
37
  }
@@ -4,6 +4,8 @@ export declare class ModelStateManager {
4
4
  private db;
5
5
  /** 单例注入 DB 实例,启动时调用一次 */
6
6
  init(db: Database.Database): void;
7
+ /** 清空所有内存缓存(导入配置后调用) */
8
+ clearAll(): void;
7
9
  /** 构造内存 Map 的 key:有 sessionId 时用复合键 */
8
10
  buildKey(routerKeyId: string | null, sessionId?: string): string;
9
11
  /**
@@ -10,6 +10,10 @@ export class ModelStateManager {
10
10
  init(db) {
11
11
  this.db = db;
12
12
  }
13
+ /** 清空所有内存缓存(导入配置后调用) */
14
+ clearAll() {
15
+ this.store.clear();
16
+ }
13
17
  /** 构造内存 Map 的 key:有 sessionId 时用复合键 */
14
18
  buildKey(routerKeyId, sessionId) {
15
19
  if (sessionId) {
@@ -23,6 +23,8 @@ export interface OrchestratorConfig {
23
23
  trackerId?: string;
24
24
  /** Claude Code 的 session ID,从 x-claude-code-session-id 请求头获取 */
25
25
  sessionId?: string;
26
+ /** 客户端请求的 JSON 字符串(headers + body),用于 Monitor 实时查看 */
27
+ clientRequest?: string;
26
28
  }
27
29
  export interface HandleContext {
28
30
  streamTimeoutMs?: number;
@@ -50,6 +50,7 @@ export class ProxyOrchestrator {
50
50
  attempts: [],
51
51
  clientIp: request.ip,
52
52
  sessionId: config.sessionId,
53
+ clientRequest: config.clientRequest,
53
54
  };
54
55
  }
55
56
  createAbortSignal(request) {
@@ -14,15 +14,49 @@ import { updateLogStreamContent } from "../db/index.js";
14
14
  import { callNonStream, callStream } from "./transport.js";
15
15
  import { insertRejectedLog } from "./log-helpers.js";
16
16
  const HTTP_ERROR_THRESHOLD = 400;
17
- const STREAM_CONTENT_MAX_RAW = 8192;
18
- const STREAM_CONTENT_MAX_TEXT = 4096;
17
+ const STREAM_CONTENT_MAX_RAW = 131072;
18
+ const STREAM_CONTENT_MAX_TEXT = 65536;
19
+ /** 将 tracker blocks 序列化为前端 tryDirectParse 可解析的 JSON */
20
+ function serializeBlocksForStorage(blocks, apiType) {
21
+ if (!blocks || blocks.length === 0)
22
+ return "";
23
+ if (apiType === "anthropic") {
24
+ const content = blocks.map(b => {
25
+ if (b.type === "thinking")
26
+ return { type: "thinking", thinking: b.content };
27
+ if (b.type === "tool_use") {
28
+ let input = {};
29
+ try {
30
+ input = JSON.parse(b.content || "{}");
31
+ }
32
+ catch { /* eslint-disable-line taste/no-silent-catch -- tool_use content 非合法 JSON 时保留空对象 */ }
33
+ return { type: "tool_use", name: b.name ?? "", input };
34
+ }
35
+ return { type: "text", text: b.content };
36
+ });
37
+ return JSON.stringify({ content });
38
+ }
39
+ const text = blocks.filter(b => b.type === "text").map(b => b.content).join("");
40
+ return JSON.stringify({ choices: [{ message: { content: text } }] });
41
+ }
42
+ function toStreamMetrics(m) {
43
+ return {
44
+ inputTokens: m.input_tokens,
45
+ outputTokens: m.output_tokens,
46
+ cacheReadTokens: m.cache_read_tokens,
47
+ ttftMs: m.ttft_ms,
48
+ tokensPerSecond: m.tokens_per_second,
49
+ stopReason: m.stop_reason,
50
+ isComplete: m.is_complete === 1,
51
+ };
52
+ }
19
53
  export async function handleProxyRequest(request, reply, apiType, upstreamPath, errors, deps, options) {
20
54
  request.raw.socket.on("error", (err) => request.log.debug({ err }, "client socket error"));
21
55
  const clientModel = request.body.model || "unknown";
22
56
  const sessionId = request.headers["x-claude-code-session-id"];
23
57
  const { effectiveModel, originalModel, interceptResponse } = applyEnhancement(deps.db, request, clientModel, sessionId);
24
58
  if (interceptResponse)
25
- return handleIntercept(deps.db, apiType, request, reply, interceptResponse, clientModel);
59
+ return handleIntercept(deps.db, apiType, request, reply, interceptResponse, clientModel, sessionId);
26
60
  const group = getMappingGroup(deps.db, effectiveModel);
27
61
  const isFailover = group?.strategy === "failover";
28
62
  const excludeTargets = [];
@@ -47,7 +81,7 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
47
81
  db: deps.db, logId, apiType, model: effectiveModel, statusCode: e.statusCode,
48
82
  errorMessage: `All failover targets exhausted (${excludeTargets.length} attempted)`,
49
83
  startTime, isStream, routerKeyId, originalBody, clientHeaders: cliHdrs, originalModel,
50
- isFailover: true, originalRequestId: rootLogId,
84
+ isFailover: true, originalRequestId: rootLogId, sessionId,
51
85
  });
52
86
  return reply.status(e.statusCode).send(e.body);
53
87
  }
@@ -56,7 +90,7 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
56
90
  db: deps.db, logId, apiType, model: effectiveModel, statusCode: e.statusCode,
57
91
  errorMessage: `No mapping found for model '${effectiveModel}'`, startTime, isStream,
58
92
  routerKeyId, originalBody, clientHeaders: cliHdrs, originalModel,
59
- isFailover: isFailoverIteration, originalRequestId: isFailoverIteration ? rootLogId : null,
93
+ isFailover: isFailoverIteration, originalRequestId: isFailoverIteration ? rootLogId : null, sessionId,
60
94
  });
61
95
  return reply.status(e.statusCode).send(e.body);
62
96
  }
@@ -71,7 +105,7 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
71
105
  db: deps.db, logId, apiType, model: effectiveModel, statusCode: e.statusCode,
72
106
  errorMessage: `Model '${resolved.backend_model}' not allowed`, startTime, isStream, routerKeyId,
73
107
  originalBody, clientHeaders: cliHdrs, providerId: resolved.provider_id, originalModel,
74
- isFailover: isFailoverIteration, originalRequestId: isFailoverIteration ? rootLogId : null,
108
+ isFailover: isFailoverIteration, originalRequestId: isFailoverIteration ? rootLogId : null, sessionId,
75
109
  });
76
110
  return reply.status(e.statusCode).send(e.body);
77
111
  }
@@ -88,7 +122,7 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
88
122
  db: deps.db, logId, apiType, model: effectiveModel, statusCode: e.statusCode,
89
123
  errorMessage: `Provider '${resolved.provider_id}' unavailable`, startTime, isStream, routerKeyId,
90
124
  originalBody, clientHeaders: cliHdrs, providerId: resolved.provider_id, originalModel,
91
- isFailover: isFailoverIteration, originalRequestId: isFailoverIteration ? rootLogId : null,
125
+ isFailover: isFailoverIteration, originalRequestId: isFailoverIteration ? rootLogId : null, sessionId,
92
126
  });
93
127
  return reply.status(e.statusCode).send(e.body);
94
128
  }
@@ -98,7 +132,7 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
98
132
  db: deps.db, logId, apiType, model: effectiveModel, statusCode: e.statusCode,
99
133
  errorMessage: `API type mismatch: expected '${apiType}'`, startTime, isStream, routerKeyId,
100
134
  originalBody, clientHeaders: cliHdrs, providerId: resolved.provider_id, originalModel,
101
- isFailover: isFailoverIteration, originalRequestId: isFailoverIteration ? rootLogId : null,
135
+ isFailover: isFailoverIteration, originalRequestId: isFailoverIteration ? rootLogId : null, sessionId,
102
136
  });
103
137
  return reply.status(e.statusCode).send(e.body);
104
138
  }
@@ -119,14 +153,7 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
119
153
  if (isStream) {
120
154
  const metricsTransform = new SSEMetricsTransform(apiType, startTime, {
121
155
  onMetrics: (m) => {
122
- deps.tracker?.update(logId, {
123
- streamMetrics: {
124
- inputTokens: m.input_tokens, outputTokens: m.output_tokens,
125
- cacheReadTokens: m.cache_read_tokens,
126
- ttftMs: m.ttft_ms, tokensPerSecond: m.tokens_per_second,
127
- stopReason: m.stop_reason, isComplete: m.is_complete === 1,
128
- },
129
- });
156
+ deps.tracker?.update(logId, { streamMetrics: toStreamMetrics(m) });
130
157
  },
131
158
  onChunk: (rawLine) => {
132
159
  deps.tracker?.appendStreamChunk(logId, rawLine, apiType, STREAM_CONTENT_MAX_RAW, STREAM_CONTENT_MAX_TEXT);
@@ -135,21 +162,19 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
135
162
  const checkEarlyError = deps.matcher
136
163
  ? (data) => deps.matcher.test(UPSTREAM_SUCCESS, data)
137
164
  : undefined;
138
- return callStream(provider, apiKey, body, cliHdrs, reply, deps.streamTimeoutMs, upstreamPath, buildUpstreamHeaders, metricsTransform, checkEarlyError);
165
+ const streamResult = await callStream(provider, apiKey, body, cliHdrs, reply, deps.streamTimeoutMs, upstreamPath, buildUpstreamHeaders, metricsTransform, checkEarlyError);
166
+ const m = (streamResult.kind === "stream_success" || streamResult.kind === "stream_abort")
167
+ ? streamResult.metrics : undefined;
168
+ if (m)
169
+ deps.tracker?.update(logId, { streamMetrics: toStreamMetrics(m) });
170
+ return streamResult;
139
171
  }
140
172
  const result = await callNonStream(provider, apiKey, body, cliHdrs, upstreamPath, buildUpstreamHeaders);
141
173
  // 非流式请求:从响应体提取指标并更新 tracker
142
174
  if (result.kind === "success") {
143
175
  const mr = MetricsExtractor.fromNonStreamResponse(apiType, result.body);
144
176
  if (mr) {
145
- deps.tracker?.update(logId, {
146
- streamMetrics: {
147
- inputTokens: mr.input_tokens, outputTokens: mr.output_tokens,
148
- cacheReadTokens: mr.cache_read_tokens,
149
- ttftMs: mr.ttft_ms, tokensPerSecond: mr.tokens_per_second,
150
- stopReason: mr.stop_reason, isComplete: mr.is_complete === 1,
151
- },
152
- });
177
+ deps.tracker?.update(logId, { streamMetrics: toStreamMetrics(mr) });
153
178
  }
154
179
  }
155
180
  // 非流式响应注入模型信息标签(模型映射场景)
@@ -168,19 +193,25 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
168
193
  return result;
169
194
  };
170
195
  try {
171
- const resilienceResult = await deps.orchestrator.handle(request, reply, apiType, { resolved, provider, clientModel: effectiveModel, isStream, trackerId: logId, sessionId }, { retryMaxAttempts: deps.retryMaxAttempts, retryBaseDelayMs: deps.retryBaseDelayMs, isFailover, ruleMatcher: deps.matcher, transportFn });
196
+ const resilienceResult = await deps.orchestrator.handle(request, reply, apiType, { resolved, provider, clientModel: effectiveModel, isStream, trackerId: logId, sessionId, clientRequest: clientReq }, { retryMaxAttempts: deps.retryMaxAttempts, retryBaseDelayMs: deps.retryBaseDelayMs, isFailover, ruleMatcher: deps.matcher, transportFn });
172
197
  const lastLogId = logResilienceResult(deps.db, {
173
198
  apiType, model: effectiveModel, providerId: provider.id, isStream,
174
- clientReq, upstreamReqBase, logId, routerKeyId, originalModel,
199
+ clientReq, upstreamReqBase, logId, routerKeyId, originalModel, sessionId,
175
200
  failover: { isFailoverIteration, rootLogId: rootLogId },
176
201
  }, resilienceResult.attempts, resilienceResult.result, startTime);
177
202
  collectTransportMetrics(deps.db, apiType, resilienceResult.result, isStream, lastLogId, provider.id, resolved.backend_model, request);
178
- // 流式请求:将 tracker 中累积的文本内容持久化到日志
203
+ // 流式请求:将 tracker 中累积的内容持久化到日志
204
+ // blocks 含非 text 类型时(thinking/tool_use)必须序列化为 JSON 以保留结构
205
+ // 注意:tracker 在原 logId 下累积内容,lastLogId 可能因 resilience 重试而指向不同记录
179
206
  if (isStream && deps.tracker) {
180
- const req = deps.tracker.get(lastLogId);
181
- const text = req?.streamContent?.textContent;
182
- if (text)
183
- updateLogStreamContent(deps.db, lastLogId, text);
207
+ const sc = deps.tracker.get(logId)?.streamContent;
208
+ const blocks = sc?.blocks;
209
+ const hasStructured = blocks && blocks.length > 0 && blocks.some(b => b.type !== "text");
210
+ const content = hasStructured
211
+ ? serializeBlocksForStorage(blocks, apiType)
212
+ : (sc?.textContent || "");
213
+ if (content)
214
+ updateLogStreamContent(deps.db, lastLogId, content);
184
215
  }
185
216
  // Failover: 单 provider 内重试已耗尽但仍失败,尝试下一个 target
186
217
  if (isFailover && !reply.raw.headersSent) {
@@ -216,7 +247,7 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
216
247
  errorMessage: `Concurrency queue full for provider '${provider.id}'`,
217
248
  startTime, isStream, routerKeyId, originalBody, clientHeaders: cliHdrs,
218
249
  providerId: provider.id, originalModel,
219
- isFailover: isFailoverIteration, originalRequestId: isFailoverIteration ? rootLogId : null,
250
+ isFailover: isFailoverIteration, originalRequestId: isFailoverIteration ? rootLogId : null, sessionId,
220
251
  });
221
252
  return reply.status(err.statusCode).send(err.body);
222
253
  }
@@ -227,7 +258,7 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
227
258
  errorMessage: `Concurrency wait timeout for provider '${provider.id}' (${e.timeoutMs}ms)`,
228
259
  startTime, isStream, routerKeyId, originalBody, clientHeaders: cliHdrs,
229
260
  providerId: provider.id, originalModel,
230
- isFailover: isFailoverIteration, originalRequestId: isFailoverIteration ? rootLogId : null,
261
+ isFailover: isFailoverIteration, originalRequestId: isFailoverIteration ? rootLogId : null, sessionId,
231
262
  });
232
263
  return reply.status(err.statusCode).send(err.body);
233
264
  }
@@ -240,6 +271,7 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
240
271
  client_request: clientReq, upstream_request: upstreamReqBase,
241
272
  is_failover: isFailoverIteration ? 1 : 0, original_request_id: isFailoverIteration ? rootLogId : null,
242
273
  router_key_id: routerKeyId, original_model: originalModel,
274
+ session_id: sessionId,
243
275
  });
244
276
  const err = errors.upstreamConnectionFailed();
245
277
  return reply.status(err.statusCode).send(err.body);
@@ -11,7 +11,7 @@ export declare function handleIntercept(db: Database.Database, apiType: "openai"
11
11
  statusCode: number;
12
12
  body: unknown;
13
13
  meta?: unknown;
14
- }, clientModel: string): import("fastify").FastifyReply;
14
+ }, clientModel: string, sessionId?: string): import("fastify").FastifyReply;
15
15
  export declare function logResilienceResult(db: Database.Database, params: {
16
16
  apiType: "openai" | "anthropic";
17
17
  model: string;
@@ -22,6 +22,7 @@ export declare function logResilienceResult(db: Database.Database, params: {
22
22
  logId: string;
23
23
  routerKeyId: string | null;
24
24
  originalModel: string | null;
25
+ sessionId?: string | null;
25
26
  failover?: FailoverContext;
26
27
  }, attempts: ResilienceAttempt[], result: TransportResult, startTime: number): string;
27
28
  export declare function collectTransportMetrics(db: Database.Database, apiType: "openai" | "anthropic", result: TransportResult, isStream: boolean, lastSuccessLogId: string, providerId: string, backendModel: string, request: FastifyRequest): void;
@@ -18,7 +18,7 @@ export function sanitizeHeadersForLog(headers) {
18
18
  return sanitized;
19
19
  }
20
20
  // ---------- Logging helpers (extracted from proxy-core) ----------
21
- export function handleIntercept(db, apiType, request, reply, interceptResponse, clientModel) {
21
+ export function handleIntercept(db, apiType, request, reply, interceptResponse, clientModel, sessionId) {
22
22
  const logId = randomUUID();
23
23
  const isStream = request.body.stream === true;
24
24
  const respBody = JSON.stringify(interceptResponse.body);
@@ -32,6 +32,7 @@ export function handleIntercept(db, apiType, request, reply, interceptResponse,
32
32
  upstream_response: JSON.stringify({ statusCode: interceptResponse.statusCode, body: respBody }),
33
33
  is_retry: 0, is_failover: 0, original_request_id: null,
34
34
  router_key_id: request.routerKey?.id ?? null, original_model: null,
35
+ session_id: sessionId,
35
36
  });
36
37
  return reply.status(interceptResponse.statusCode).send(interceptResponse.body);
37
38
  }
@@ -56,6 +57,7 @@ export function logResilienceResult(db, params, attempts, result, startTime) {
56
57
  is_retry: isOriginal ? 0 : 1, is_failover: isFailoverLog ? 1 : 0,
57
58
  original_request_id: parentId,
58
59
  router_key_id: params.routerKeyId, original_model: params.originalModel,
60
+ session_id: params.sessionId,
59
61
  });
60
62
  }
61
63
  else if (attempt.statusCode !== UPSTREAM_SUCCESS) {
@@ -70,6 +72,7 @@ export function logResilienceResult(db, params, attempts, result, startTime) {
70
72
  is_retry: isOriginal ? 0 : 1, is_failover: isFailoverLog ? 1 : 0,
71
73
  original_request_id: parentId,
72
74
  router_key_id: params.routerKeyId, original_model: params.originalModel,
75
+ session_id: params.sessionId,
73
76
  });
74
77
  }
75
78
  else {
@@ -87,6 +90,7 @@ export function logResilienceResult(db, params, attempts, result, startTime) {
87
90
  isRetry: !isOriginal, isFailover: isFailoverLog,
88
91
  originalRequestId: parentId,
89
92
  routerKeyId: params.routerKeyId, originalModel: params.originalModel,
93
+ sessionId: params.sessionId,
90
94
  });
91
95
  lastSuccessLogId = attemptLogId;
92
96
  }
@@ -30,5 +30,7 @@ export declare class ProviderSemaphoreManager {
30
30
  queued: number;
31
31
  };
32
32
  remove(providerId: string): void;
33
+ /** 清除所有 provider 的信号量配置(导入配置后调用) */
34
+ removeAll(): void;
33
35
  }
34
36
  export {};
@@ -149,4 +149,15 @@ export class ProviderSemaphoreManager {
149
149
  }
150
150
  this.entries.delete(providerId);
151
151
  }
152
+ /** 清除所有 provider 的信号量配置(导入配置后调用) */
153
+ removeAll() {
154
+ for (const [, entry] of this.entries) {
155
+ for (const e of entry.queue) {
156
+ if (e.timer)
157
+ clearTimeout(e.timer);
158
+ e.reject(new Error("Provider removed"));
159
+ }
160
+ }
161
+ this.entries.clear();
162
+ }
152
163
  }
@@ -1 +1 @@
1
- import{It as e,K as t,Pt as n,U as r,at as i,ct as a,r as o}from"./button-BkhL20Qz.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:e(n(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:e(n(o)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[a(t.$slots,`default`)],2))}});export{c as n,l as t};
1
+ import{It as e,K as t,Pt as n,U as r,at as i,ct as a,r as o}from"./button-Dbz2Be22.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:e(n(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:e(n(o)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[a(t.$slots,`default`)],2))}});export{c as n,l as t};
@@ -1 +1 @@
1
- import{It as e,K as t,Pt as n,U as r,at as i,ct as a,r as o}from"./button-BkhL20Qz.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:e(n(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))}}),c=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:e(n(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 n,c as t};
1
+ import{It as e,K as t,Pt as n,U as r,at as i,ct as a,r as o}from"./button-Dbz2Be22.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:e(n(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))}}),c=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:e(n(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 n,c as t};
@@ -0,0 +1 @@
1
+ import{$ as e,F as t,G as n,H as r,I as i,J as a,K as o,Lt as s,Pt 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,vt as _,z as v}from"./button-Dbz2Be22.js";import{n as y,t as b}from"./ohash.D__AXeF1-BGxYMs6k.js";import{g as x,o as S,u as C,y as w}from"./Teleport-B0PNXZbP.js";import{n as T}from"./VisuallyHidden-BwwTtzb9.js";import{t as E}from"./useForwardExpose-C2_ks3sW.js";import{t as D}from"./VisuallyHiddenInput-EGZSP7s8.js";import{s as O}from"./TableHeader-Bn0bodWx.js";function k(e,t){return x(e)?!1:Array.isArray(e)?e.some(e=>b(e,t)):b(e,t)}var[A,j]=w(`CheckboxGroupRoot`);function M(e){return e===`indeterminate`}function N(e){return M(e)?`indeterminate`:e?`checked`:`unchecked`}var[P,F]=w(`CheckboxRoot`),I=o({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(n,{emit:a}){let o=n,s=a,{forwardRef:d,currentElement:h}=E(),y=A(null),S=m(o,`modelValue`,s,{defaultValue:o.defaultValue??o.falseValue,passive:o.modelValue===void 0}),C=v(()=>y?.disabled.value||o.disabled),w=v(()=>b(S.value,o.trueValue)),j=v(()=>x(y?.modelValue.value)?S.value===`indeterminate`?`indeterminate`:w.value:k(y.modelValue.value,o.value));function P(){if(x(y?.modelValue.value))S.value===`indeterminate`?S.value=o.trueValue:S.value=w.value?o.falseValue:o.trueValue;else{let e=[...y.modelValue.value||[]];if(k(e,o.value)){let t=e.findIndex(e=>b(e,o.value));e.splice(t,1)}else e.push(o.value);y.modelValue.value=e}}let I=T(h),L=v(()=>o.id&&h.value?document.querySelector(`[for="${o.id}"]`)?.innerText:void 0);return F({disabled:C,state:j}),(n,a)=>(u(),l(g(c(y)?.rovingFocus.value?c(O):c(p)),e(n.$attrs,{id:n.id,ref:c(d),role:`checkbox`,"as-child":n.asChild,as:n.as,type:n.as===`button`?`button`:void 0,"aria-checked":c(M)(j.value)?`mixed`:j.value,"aria-required":n.required,"aria-label":n.$attrs[`aria-label`]||L.value,"data-state":c(N)(j.value),"data-disabled":C.value?``:void 0,disabled:C.value,focusable:c(y)?.rovingFocus.value?!C.value:void 0,onKeydown:t(i(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:_(()=>[f(n.$slots,`default`,{modelValue:c(S),state:j.value}),c(I)&&n.name&&!c(y)?(u(),l(c(D),{key:0,type:`checkbox`,checked:!!j.value,name:n.name,value:n.value,disabled:C.value,required:n.required},null,8,[`checked`,`name`,`value`,`disabled`,`required`])):r(`v-if`,!0)]),_:3},16,[`id`,`as-child`,`as`,`type`,`aria-checked`,`aria-required`,`aria-label`,`data-state`,`data-disabled`,`disabled`,`focusable`,`onKeydown`]))}}),L=o({__name:`CheckboxIndicator`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`span`}},setup(t){let{forwardRef:r}=E(),i=P();return(t,a)=>(u(),l(c(S),{present:t.forceMount||c(M)(c(i).state.value)||c(i).state.value===!0},{default:_(()=>[n(c(p),e({ref:c(r),"data-state":c(N)(c(i).state.value),"data-disabled":c(i).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=o({__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:r}){let i=t,o=r,p=C(d(i,`class`),o);return(t,r)=>(u(),l(c(I),e({"data-slot":`checkbox`},c(p),{class:c(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`,i.class)}),{default:_(e=>[n(c(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:_(()=>[f(t.$slots,`default`,s(a(e)),()=>[n(c(y))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};
@@ -0,0 +1 @@
1
+ import{$ as e,Et as t,G as n,H as r,J as i,K as a,Lt as o,Pt as s,V as c,at as l,ct as u,et as d,ht as f,i as p,jt as m,nt as h,p as g,u as _,vt as v,z as y}from"./button-Dbz2Be22.js";import{c as b,o as x,u as S,y as C}from"./Teleport-B0PNXZbP.js";import{t as w}from"./useForwardExpose-C2_ks3sW.js";var[T,E]=C(`CollapsibleRoot`),D=a({__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:n}){let r=e,i=g(r,`open`,n,{defaultValue:r.defaultOpen,passive:r.open===void 0}),{disabled:a,unmountOnHide:o}=m(r);return E({contentId:``,disabled:a,open:i,unmountOnHide:o,onOpenToggle:()=>{a.value||(i.value=!i.value)}}),t({open:i}),w(),(e,t)=>(l(),c(s(p),{as:e.as,"as-child":r.asChild,"data-state":s(i)?`open`:`closed`,"data-disabled":s(a)?``:void 0},{default:v(()=>[u(e.$slots,`default`,{open:s(i)})]),_:3},8,[`as`,`as-child`,`data-state`,`data-disabled`]))}}),O=a({inheritAttrs:!1,__name:`CollapsibleContent`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`contentFound`],setup(i,{emit:a}){let o=i,m=a,g=T();g.contentId||=b(void 0,`reka-collapsible-content`);let S=t(),{forwardRef:C,currentElement:E}=w(),D=t(0),O=t(0),k=y(()=>g.open.value),A=t(k.value),j=t();f(()=>[k.value,S.value?.present],async()=>{await d();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(),m(`contentFound`)})}),(t,i)=>(l(),c(s(x),{ref_key:`presentRef`,ref:S,present:t.forceMount||s(g).open.value,"force-mount":!0},{default:v(({present:i})=>[n(s(p),e(t.$attrs,{id:s(g).contentId,ref:s(C),"as-child":o.asChild,as:t.as,hidden:i?void 0:s(g).unmountOnHide.value?``:`until-found`,"data-state":M.value?void 0:s(g).open.value?`open`:`closed`,"data-disabled":s(g).disabled?.value?``:void 0,style:{"--reka-collapsible-content-height":`${O.value}px`,"--reka-collapsible-content-width":`${D.value}px`}}),{default:v(()=>[!s(g).unmountOnHide.value||i?u(t.$slots,`default`,{key:0}):r(`v-if`,!0)]),_:2},1040,[`id`,`as-child`,`as`,`hidden`,`data-state`,`data-disabled`,`style`])]),_:3},8,[`present`]))}}),k=a({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`}},setup(e){let t=e;w();let n=T();return(e,r)=>(l(),c(s(p),{type:e.as===`button`?`button`:void 0,as:e.as,"as-child":t.asChild,"aria-controls":s(n).contentId,"aria-expanded":s(n).open.value,"data-state":s(n).open.value?`open`:`closed`,"data-disabled":s(n).disabled?.value?``:void 0,disabled:s(n).disabled?.value,onClick:s(n).onOpenToggle},{default:v(()=>[u(e.$slots,`default`)]),_:3},8,[`type`,`as`,`as-child`,`aria-controls`,`aria-expanded`,`data-state`,`data-disabled`,`disabled`,`onClick`]))}}),A=a({__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:n}){let r=S(t,n);return(t,n)=>(l(),c(s(D),e({"data-slot":`collapsible`},s(r)),{default:v(e=>[u(t.$slots,`default`,o(i(e)))]),_:3},16))}}),j=a({__name:`CollapsibleContent`,props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{}},setup(t){let n=t;return(t,r)=>(l(),c(s(O),e({"data-slot":`collapsible-content`},n),{default:v(()=>[u(t.$slots,`default`)]),_:3},16))}}),M=a({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean},as:{}},setup(t){let n=t;return(t,r)=>(l(),c(s(k),e({"data-slot":`collapsible-trigger`},n),{default:v(()=>[u(t.$slots,`default`)]),_:3},16))}});export{j as n,A as r,M as t};