llm-simple-router 0.11.13 → 0.11.15

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 (80) hide show
  1. package/dist/core/types.d.ts +5 -0
  2. package/dist/db/logs.d.ts +8 -0
  3. package/dist/db/logs.js +4 -2
  4. package/dist/db/migrations/048_add_diagnostic_columns.sql +9 -0
  5. package/dist/proxy/handler/failover-loop.js +25 -7
  6. package/dist/proxy/log-helpers.d.ts +16 -0
  7. package/dist/proxy/log-helpers.js +18 -2
  8. package/dist/proxy/orchestration/resilience.d.ts +2 -0
  9. package/dist/proxy/orchestration/resilience.js +7 -2
  10. package/dist/proxy/proxy-logging.d.ts +4 -0
  11. package/dist/proxy/proxy-logging.js +26 -1
  12. package/dist/proxy/transport/stream.js +5 -6
  13. package/frontend-dist/assets/CardContent-CjG9la6t.js +1 -0
  14. package/frontend-dist/assets/CardTitle-DLg3AzYe.js +1 -0
  15. package/frontend-dist/assets/CascadingModelSelect-uI71MquC.js +1 -0
  16. package/frontend-dist/assets/Checkbox-BN-RZ6gV.js +1 -0
  17. package/frontend-dist/assets/CollapsibleContent-1Jnv6xUL.js +1 -0
  18. package/frontend-dist/assets/CollapsibleTrigger-efrWnbjX.js +1 -0
  19. package/frontend-dist/assets/Dashboard-CvOVaMuA.js +3 -0
  20. package/frontend-dist/assets/Input-B7SStAr9.js +1 -0
  21. package/frontend-dist/assets/Label-pcY5ONLi.js +1 -0
  22. package/frontend-dist/assets/Login-B7X1YFxm.js +1 -0
  23. package/frontend-dist/assets/Logs-DUS2AiMd.js +1 -0
  24. package/frontend-dist/assets/MappingEntryEditor-trK_pdk8.js +1 -0
  25. package/frontend-dist/assets/ModelMappings-hXKpwXVH.js +1 -0
  26. package/frontend-dist/assets/Monitor-BYeg4vZ3.js +1 -0
  27. package/frontend-dist/assets/Providers-DTIpcRW3.js +1 -0
  28. package/frontend-dist/assets/ProxyEnhancement-Cmti88q4.js +1 -0
  29. package/frontend-dist/assets/QuickSetup-g860Hl8u.js +1 -0
  30. package/frontend-dist/assets/RetryRules-DZwp2UG4.js +1 -0
  31. package/frontend-dist/assets/RouterKeys-DfdjoM2L.js +1 -0
  32. package/frontend-dist/assets/{RovingFocusItem-Dg-GYaYf.js → RovingFocusItem-CPyYVYtN.js} +1 -1
  33. package/frontend-dist/assets/Schedules-B7LliotC.js +1 -0
  34. package/frontend-dist/assets/Settings-DIyvniYc.js +6 -0
  35. package/frontend-dist/assets/Setup-C2737yJ7.js +1 -0
  36. package/frontend-dist/assets/{Switch-CqF_csS2.js → Switch-BlOIfUMO.js} +1 -1
  37. package/frontend-dist/assets/TooltipTrigger-DF06pXkC.js +1 -0
  38. package/frontend-dist/assets/TransformRulesForm-DJZim24h.js +1 -0
  39. package/frontend-dist/assets/UnifiedRequestDialog-BFHeYqYY.js +3 -0
  40. package/frontend-dist/assets/VisuallyHiddenInput-C8GAHZzN.js +1 -0
  41. package/frontend-dist/assets/{button-BaNRw0cF.js → button-DsVDTV_5.js} +2 -2
  42. package/frontend-dist/assets/{copy-Dbl7J0Cg.js → copy-B9-1OTfs.js} +1 -1
  43. package/frontend-dist/assets/dialog-C3OF45pf.js +1 -0
  44. package/frontend-dist/assets/index-DVY081yH.js +3 -0
  45. package/frontend-dist/assets/model-patches-D7N5lB_e.js +1 -0
  46. package/frontend-dist/assets/{sparkles-D3u86oss.js → sparkles-BPDHpMYW.js} +1 -1
  47. package/frontend-dist/assets/{trash-2-DvEWr8UB.js → trash-2-DmnTuMRP.js} +1 -1
  48. package/frontend-dist/assets/{useClipboard-D_jpOvnE.js → useClipboard-DTVDIfen.js} +1 -1
  49. package/frontend-dist/assets/{useLogRetention-DnSxnecs.js → useLogRetention-Dz4ZFJD0.js} +1 -1
  50. package/frontend-dist/index.html +2 -2
  51. package/package.json +1 -1
  52. package/frontend-dist/assets/CardContent-POdM9knm.js +0 -1
  53. package/frontend-dist/assets/CardTitle-mi00cvHk.js +0 -1
  54. package/frontend-dist/assets/CascadingModelSelect-CubFNL5Z.js +0 -1
  55. package/frontend-dist/assets/Checkbox-BcnVK5BE.js +0 -1
  56. package/frontend-dist/assets/CollapsibleContent-CMuY_0ps.js +0 -1
  57. package/frontend-dist/assets/CollapsibleTrigger-KRXgNeSC.js +0 -1
  58. package/frontend-dist/assets/Dashboard-Bh4nXWLM.js +0 -3
  59. package/frontend-dist/assets/Input-CQAFlthI.js +0 -1
  60. package/frontend-dist/assets/Label-_o2ZnCRD.js +0 -1
  61. package/frontend-dist/assets/Login-BPqzTIaZ.js +0 -1
  62. package/frontend-dist/assets/Logs-ZJszhjOb.js +0 -1
  63. package/frontend-dist/assets/MappingEntryEditor--Icmf_he.js +0 -1
  64. package/frontend-dist/assets/ModelMappings-Bj32YLQZ.js +0 -1
  65. package/frontend-dist/assets/Monitor-SBlmRPTQ.js +0 -1
  66. package/frontend-dist/assets/Providers-Biuxv2W9.js +0 -1
  67. package/frontend-dist/assets/ProxyEnhancement-BTUqx-hj.js +0 -1
  68. package/frontend-dist/assets/QuickSetup-B8-u2sZa.js +0 -1
  69. package/frontend-dist/assets/RetryRules-C8mGFYBh.js +0 -1
  70. package/frontend-dist/assets/RouterKeys-DgzXJsL8.js +0 -1
  71. package/frontend-dist/assets/Schedules-CQQX55LC.js +0 -1
  72. package/frontend-dist/assets/Settings-B7-f_Pei.js +0 -6
  73. package/frontend-dist/assets/Setup-aJfZU8RD.js +0 -1
  74. package/frontend-dist/assets/TooltipTrigger-CpHXgQxI.js +0 -1
  75. package/frontend-dist/assets/TransformRulesForm-C0elmJO6.js +0 -1
  76. package/frontend-dist/assets/UnifiedRequestDialog-DWUXdfWY.js +0 -3
  77. package/frontend-dist/assets/VisuallyHiddenInput-HnGHuxTe.js +0 -1
  78. package/frontend-dist/assets/dialog-D_OkVkI9.js +0 -1
  79. package/frontend-dist/assets/index-COoySiAr.js +0 -3
  80. package/frontend-dist/assets/model-patches-D5Tyous2.js +0 -1
@@ -89,6 +89,7 @@ export type TransportResult = {
89
89
  providerId: string;
90
90
  };
91
91
  timeoutMs?: number;
92
+ abortReason?: "idle_timeout" | "client_disconnect" | "loop_detection";
92
93
  } | {
93
94
  kind: "error";
94
95
  statusCode: number;
@@ -113,6 +114,10 @@ export interface ResilienceAttempt {
113
114
  responseHeaders: Record<string, string> | null;
114
115
  /** TransportResult.kind,用于区分 stream_error 等特殊类型 */
115
116
  resultKind: TransportResult["kind"];
117
+ /** error.code(如 ETIMEDOUT / ECONNRESET / ECONNREFUSED),仅 throw 时有值 */
118
+ error_code?: string | null;
119
+ /** response headers 是否已发送,影响重试/failover 决策 */
120
+ headers_sent?: boolean | null;
116
121
  }
117
122
  /** 流式传输阶段状态 */
118
123
  export type StreamState = "BUFFERING" | "STREAMING" | "COMPLETED" | "EARLY_ERROR" | "ABORTED";
package/dist/db/logs.d.ts CHANGED
@@ -48,6 +48,14 @@ export interface RequestLogInsert {
48
48
  session_id?: string | null;
49
49
  client_status_code?: number | null;
50
50
  pipeline_snapshot?: string | null;
51
+ transport_kind?: string | null;
52
+ abort_reason?: string | null;
53
+ error_code?: string | null;
54
+ headers_sent?: number | null;
55
+ resilience_action?: string | null;
56
+ resilience_reason?: string | null;
57
+ mapping_reason?: string | null;
58
+ failover_trigger?: string | null;
51
59
  }
52
60
  export interface LogWriteContext {
53
61
  matcher?: RetryMatcher | null;
package/dist/db/logs.js CHANGED
@@ -16,8 +16,10 @@ function rawInsertRequestLog(db, log, writeContext) {
16
16
  const preserveDetail = shouldPreserveDetail(log.status_code, writeContext?.responseBody ?? null, writeContext?.matcher ?? null, !!writeContext?.logFileWriter);
17
17
  getCachedStmt(db, `INSERT INTO request_logs (id, api_type, model, provider_id, status_code, client_status_code, latency_ms,
18
18
  is_stream, error_message, created_at, client_request, upstream_request, upstream_response,
19
- is_retry, is_failover, original_request_id, router_key_id, original_model, session_id, pipeline_snapshot)
20
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(log.id, log.api_type, log.model, log.provider_id, log.status_code, log.client_status_code ?? null, log.latency_ms, log.is_stream, log.error_message, log.created_at, preserveDetail ? (log.client_request ?? null) : null, preserveDetail ? (log.upstream_request ?? null) : null, preserveDetail ? (log.upstream_response ?? null) : null, log.is_retry ?? 0, log.is_failover ?? 0, log.original_request_id ?? null, log.router_key_id ?? null, log.original_model ?? null, log.session_id ?? null, log.pipeline_snapshot ?? null);
19
+ is_retry, is_failover, original_request_id, router_key_id, original_model, session_id, pipeline_snapshot,
20
+ transport_kind, abort_reason, error_code, headers_sent, resilience_action, resilience_reason, mapping_reason, failover_trigger)
21
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
22
+ ?, ?, ?, ?, ?, ?, ?, ?)`).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, log.transport_kind ?? null, log.abort_reason ?? null, log.error_code ?? null, log.headers_sent ?? null, log.resilience_action ?? null, log.resilience_reason ?? null, log.mapping_reason ?? null, log.failover_trigger ?? null);
21
23
  }
22
24
  export function insertRequestLog(db, log, writeContext) {
23
25
  // 文件写入:始终同步调用(WriteStream 内部异步,不阻塞事件循环)
@@ -0,0 +1,9 @@
1
+ -- 048: Add diagnostic columns to request_logs for runtime observability
2
+ ALTER TABLE request_logs ADD COLUMN transport_kind TEXT;
3
+ ALTER TABLE request_logs ADD COLUMN abort_reason TEXT;
4
+ ALTER TABLE request_logs ADD COLUMN error_code TEXT;
5
+ ALTER TABLE request_logs ADD COLUMN headers_sent INTEGER;
6
+ ALTER TABLE request_logs ADD COLUMN resilience_action TEXT;
7
+ ALTER TABLE request_logs ADD COLUMN resilience_reason TEXT;
8
+ ALTER TABLE request_logs ADD COLUMN mapping_reason TEXT;
9
+ ALTER TABLE request_logs ADD COLUMN failover_trigger TEXT;
@@ -63,6 +63,7 @@ function rejectAndReply(reply, params, error, errorMessage, providerId, afterLog
63
63
  isFailover: params.isFailover, originalRequestId: params.originalRequestId,
64
64
  sessionId: params.sessionId, pipelineSnapshot: params.pipelineSnapshot,
65
65
  matcher: params.matcher, logFileWriter: params.logFileWriter,
66
+ mapping_reason: params.mappingReason ?? null,
66
67
  });
67
68
  try {
68
69
  afterLog?.();
@@ -184,6 +185,7 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
184
185
  }
185
186
  // === while(true):纯执行循环 ===
186
187
  let failoverIteration = 0;
188
+ let lastFailoverTrigger = null;
187
189
  while (true) {
188
190
  // 请求被 kill 后 reply 已销毁,直接退出避免浪费 failover 迭代
189
191
  if (reply.raw.destroyed)
@@ -218,6 +220,14 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
218
220
  }
219
221
  const resolved = filtered[0];
220
222
  const isFailover = cachedTargets.length > 1;
223
+ // effectiveMappingReason: 首次迭代用 resolveResult.reason,溢出时覆盖
224
+ let effectiveMappingReason = isFailoverIteration ? "failover_retry" : resolveResult.mappingReason;
225
+ // 只有当前 target 是 overflow 扩展产生的才标记
226
+ const resolvedIdx = cachedTargets.findIndex(t => t.provider_id === resolved.provider_id && t.backend_model === resolved.backend_model);
227
+ if (overflowIndices.has(resolvedIdx))
228
+ effectiveMappingReason = "overflow_redirect";
229
+ // 将 mappingReason 注入 rCtx,使后续 rejectAndReply 能写入诊断字段
230
+ rCtx.mappingReason = effectiveMappingReason;
221
231
  const provider = getProviderById(db, resolved.provider_id);
222
232
  if (!provider || !provider.is_active) {
223
233
  return rejectAndReply(reply, rCtx, errors.providerUnavailable(), `Provider '${resolved.provider_id}' unavailable`, resolved.provider_id);
@@ -230,12 +240,6 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
230
240
  const effectiveApiType = resolvedPath.effectiveApiType;
231
241
  const effectiveUpstreamPath = resolvedPath.effectiveUpstreamPath;
232
242
  const needsTransform = resolvedPath.needsTransform;
233
- // effectiveMappingReason: 首次迭代用 resolveResult.reason,溢出时覆盖
234
- let effectiveMappingReason = isFailoverIteration ? "failover_retry" : resolveResult.mappingReason;
235
- // 只有当前 target 是 overflow 扩展产生的才标记
236
- const resolvedIdx = cachedTargets.findIndex(t => t.provider_id === resolved.provider_id && t.backend_model === resolved.backend_model);
237
- if (overflowIndices.has(resolvedIdx))
238
- effectiveMappingReason = "overflow_redirect";
239
243
  // --- routing ---
240
244
  currentBody = { ...currentBody, model: resolved.backend_model };
241
245
  iterationSnapshot.add({ stage: "routing", client_model: clientModel, backend_model: resolved.backend_model, provider_id: resolved.provider_id, strategy: cachedTargets.length > 1 ? "failover" : "scheduled", mapping_reason: effectiveMappingReason });
@@ -325,6 +329,12 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
325
329
  failover: { isFailoverIteration, rootLogId: rootLogId },
326
330
  pipelineSnapshot,
327
331
  matcher, logFileWriter,
332
+ resilienceAction: resilienceResult.finalDecision?.action,
333
+ resilienceReason: resilienceResult.finalDecision?.action === "abort"
334
+ ? resilienceResult.finalDecision.reason
335
+ : null,
336
+ mappingReason: effectiveMappingReason,
337
+ failoverTrigger: lastFailoverTrigger,
328
338
  }, resilienceResult.attempts, resilienceResult.result, startTime);
329
339
  collectTransportMetrics(db, clientApiType, resilienceResult.result, isStream, lastLogId, provider.id, resolved.backend_model, request, routerKeyId, getTransportStatusCode(resilienceResult.result), ctx.metadata.get("client_type"), ctx.metadata.get("session_id"), tracker, ctx.metadata);
330
340
  // flush tool errors
@@ -366,6 +376,7 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
366
376
  const failed = tr.kind === "throw"
367
377
  || ("statusCode" in tr && tr.statusCode >= HTTP_ERROR_THRESHOLD);
368
378
  if (failed) {
379
+ lastFailoverTrigger = tr.kind === "throw" ? "throw" : `status_${("statusCode" in tr ? tr.statusCode : 0)}`;
369
380
  excludeTargets.push(resolved);
370
381
  continue;
371
382
  }
@@ -402,9 +413,14 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
402
413
  failover: { isFailoverIteration, rootLogId: rootLogId },
403
414
  pipelineSnapshot,
404
415
  matcher, logFileWriter,
416
+ resilienceAction: "failover",
417
+ resilienceReason: "provider_switch_needed",
418
+ mappingReason: effectiveMappingReason,
419
+ failoverTrigger: e.constructor.name,
405
420
  }, e.attempts, fakeResult, startTime);
406
421
  }
407
422
  flushCurrentErrors();
423
+ lastFailoverTrigger = e.constructor.name;
408
424
  excludeTargets.push(resolved);
409
425
  continue;
410
426
  }
@@ -422,7 +438,7 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
422
438
  const errMsg = e instanceof Error ? e.message : JSON.stringify(e);
423
439
  request.log.debug({ logId, error: errMsg, action: "upstream_error" });
424
440
  insertRequestLog(db, {
425
- id: randomUUID(), api_type: clientApiType,
441
+ id: logId, api_type: clientApiType,
426
442
  model: clientModel, provider_id: provider.id,
427
443
  status_code: UPSTREAM_ERROR_STATUS, latency_ms: Date.now() - startTime, is_stream: isStream ? 1 : 0,
428
444
  error_message: errMsg || "Upstream connection failed", created_at: new Date().toISOString(),
@@ -431,6 +447,8 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
431
447
  router_key_id: routerKeyId, original_model: null,
432
448
  session_id: ctx.metadata.get("session_id"),
433
449
  pipeline_snapshot: pipelineSnapshot,
450
+ transport_kind: "throw",
451
+ mapping_reason: rCtx.mappingReason ?? null,
434
452
  }, (matcher || logFileWriter) ? {
435
453
  matcher, logFileWriter, responseBody: null,
436
454
  } : undefined);
@@ -30,6 +30,14 @@ export interface RequestLogParams extends LogRetryMeta {
30
30
  pipelineSnapshot?: string | null;
31
31
  matcher?: RetryMatcher | null;
32
32
  logFileWriter?: LogFileWriter | null;
33
+ transport_kind?: string | null;
34
+ abort_reason?: string | null;
35
+ error_code?: string | null;
36
+ headers_sent?: number | null;
37
+ resilience_action?: string | null;
38
+ resilience_reason?: string | null;
39
+ mapping_reason?: string | null;
40
+ failover_trigger?: string | null;
33
41
  }
34
42
  /** 插入成功请求日志,供 openai/anthropic 插件共享 */
35
43
  export declare function insertSuccessLog(db: Database.Database, params: RequestLogParams): void;
@@ -51,6 +59,14 @@ export interface RejectedLogParams extends LogRetryMeta {
51
59
  pipelineSnapshot?: string | null;
52
60
  matcher?: RetryMatcher | null;
53
61
  logFileWriter?: LogFileWriter | null;
62
+ mapping_reason?: string | null;
63
+ transport_kind?: string | null;
64
+ abort_reason?: string | null;
65
+ error_code?: string | null;
66
+ headers_sent?: number | null;
67
+ resilience_action?: string | null;
68
+ resilience_reason?: string | null;
69
+ failover_trigger?: string | null;
54
70
  }
55
71
  /** Log a request rejected before reaching upstream */
56
72
  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, sessionId = null, pipelineSnapshot = null, matcher, logFileWriter } = 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, pipelineSnapshot = null, matcher, logFileWriter, transport_kind, abort_reason, error_code, headers_sent, resilience_action, resilience_reason, mapping_reason, failover_trigger } = params;
5
5
  const writeContext = (matcher || logFileWriter) ? {
6
6
  matcher,
7
7
  logFileWriter,
@@ -18,11 +18,19 @@ export function insertSuccessLog(db, params) {
18
18
  router_key_id: routerKeyId, original_model: originalModel,
19
19
  session_id: sessionId,
20
20
  pipeline_snapshot: pipelineSnapshot ?? null,
21
+ transport_kind: transport_kind ?? null,
22
+ abort_reason: abort_reason ?? null,
23
+ error_code: error_code ?? null,
24
+ headers_sent: headers_sent ?? null,
25
+ resilience_action: resilience_action ?? null,
26
+ resilience_reason: resilience_reason ?? null,
27
+ mapping_reason: mapping_reason ?? null,
28
+ failover_trigger: failover_trigger ?? null,
21
29
  }, writeContext);
22
30
  }
23
31
  /** Log a request rejected before reaching upstream */
24
32
  export function insertRejectedLog(params) {
25
- const { db, logId, apiType, model, statusCode, errorMessage, startTime, isStream, routerKeyId, originalBody, clientHeaders, providerId = null, isFailover = false, originalRequestId = null, originalModel = null, sessionId = null, pipelineSnapshot = null, matcher, logFileWriter } = params;
33
+ const { db, logId, apiType, model, statusCode, errorMessage, startTime, isStream, routerKeyId, originalBody, clientHeaders, providerId = null, isFailover = false, originalRequestId = null, originalModel = null, sessionId = null, pipelineSnapshot = null, matcher, logFileWriter, mapping_reason, transport_kind, abort_reason, error_code, headers_sent, resilience_action, resilience_reason, failover_trigger } = params;
26
34
  const writeContext = (matcher || logFileWriter) ? {
27
35
  matcher,
28
36
  logFileWriter,
@@ -45,5 +53,13 @@ export function insertRejectedLog(params) {
45
53
  original_model: originalModel,
46
54
  session_id: sessionId,
47
55
  pipeline_snapshot: pipelineSnapshot ?? null,
56
+ transport_kind: transport_kind ?? null,
57
+ abort_reason: abort_reason ?? null,
58
+ error_code: error_code ?? null,
59
+ headers_sent: headers_sent ?? null,
60
+ resilience_action: resilience_action ?? null,
61
+ resilience_reason: resilience_reason ?? null,
62
+ mapping_reason: mapping_reason ?? null,
63
+ failover_trigger: failover_trigger ?? null,
48
64
  }, writeContext);
49
65
  }
@@ -32,6 +32,8 @@ export interface ResilienceResult {
32
32
  result: TransportResult;
33
33
  attempts: ResilienceAttempt[];
34
34
  excludedTargets: Target[];
35
+ /** 最终 resilience 决策,用于诊断日志 */
36
+ finalDecision?: ResilienceDecision;
35
37
  }
36
38
  export type ResilienceDecision = {
37
39
  action: "done";
@@ -153,6 +153,7 @@ export class ResilienceLayer {
153
153
  result: lastResult ?? { kind: "error", statusCode: 502, body: "Iteration cap exceeded", headers: {}, sentHeaders: {}, sentBody: "" },
154
154
  attempts: allAttempts,
155
155
  excludedTargets,
156
+ finalDecision: { action: "abort", reason: "iteration_cap_exceeded" },
156
157
  };
157
158
  }
158
159
  const excludedSet = new Set(excludedTargets.map(e => `${e.provider_id}:${e.backend_model}`));
@@ -162,6 +163,7 @@ export class ResilienceLayer {
162
163
  result: lastResult ?? { kind: "error", statusCode: 502, body: "All targets exhausted", headers: {}, sentHeaders: {}, sentBody: "" },
163
164
  attempts: allAttempts,
164
165
  excludedTargets,
166
+ finalDecision: { action: "abort", reason: "all_targets_exhausted" },
165
167
  };
166
168
  }
167
169
  const currentTarget = available[0];
@@ -183,6 +185,8 @@ export class ResilienceLayer {
183
185
  statusCode: null, error: (throwErr instanceof Error ? throwErr.message : JSON.stringify(throwErr)),
184
186
  latencyMs: Date.now() - start, responseBody: null,
185
187
  responseHeaders: null, resultKind: transportResult.kind,
188
+ error_code: (throwErr instanceof Error && 'code' in throwErr) ? throwErr.code ?? null : null,
189
+ headers_sent: transportResult.headersSent ?? null,
186
190
  });
187
191
  }
188
192
  else {
@@ -192,6 +196,7 @@ export class ResilienceLayer {
192
196
  latencyMs: Date.now() - start, responseBody: extractBody(transportResult),
193
197
  responseHeaders: extractHeaders(transportResult) ?? null,
194
198
  resultKind: transportResult.kind,
199
+ headers_sent: transportResult.kind === "stream_error" ? transportResult.headersSent ?? null : null,
195
200
  });
196
201
  }
197
202
  const state = {
@@ -202,7 +207,7 @@ export class ResilienceLayer {
202
207
  const decision = this.decide(transportResult, state, config);
203
208
  switch (decision.action) {
204
209
  case "done":
205
- return { result: transportResult, attempts: allAttempts, excludedTargets };
210
+ return { result: transportResult, attempts: allAttempts, excludedTargets, finalDecision: decision };
206
211
  case "retry":
207
212
  globalAttemptIndex++;
208
213
  await sleep(decision.delayMs);
@@ -218,7 +223,7 @@ export class ResilienceLayer {
218
223
  }
219
224
  continue;
220
225
  case "abort":
221
- return { result: transportResult, attempts: allAttempts, excludedTargets };
226
+ return { result: transportResult, attempts: allAttempts, excludedTargets, finalDecision: decision };
222
227
  }
223
228
  }
224
229
  }
@@ -24,5 +24,9 @@ export declare function logResilienceResult(db: Database.Database, params: {
24
24
  test: (statusCode: number, body: string) => boolean;
25
25
  } | null;
26
26
  logFileWriter?: LogFileWriter | null;
27
+ resilienceAction?: string | null;
28
+ resilienceReason?: string | null;
29
+ mappingReason?: string | null;
30
+ failoverTrigger?: string | null;
27
31
  }, attempts: ResilienceAttempt[], result: TransportResult, startTime: number): string;
28
32
  export declare function collectTransportMetrics(db: Database.Database, apiType: "openai" | "openai-responses" | "anthropic", result: TransportResult, isStream: boolean, lastSuccessLogId: string, providerId: string, backendModel: string, request: FastifyRequest, routerKeyId?: string | null, statusCode?: number | null, clientType?: string, sessionId?: string, tracker?: RequestTracker, metadata?: Map<string, unknown>): void;
@@ -53,11 +53,32 @@ export function logResilienceResult(db, params, attempts, result, startTime) {
53
53
  const isFailoverIteration = params.failover?.isFailoverIteration ?? false;
54
54
  const rootLogId = params.failover?.rootLogId ?? params.logId;
55
55
  let lastSuccessLogId = params.logId;
56
- for (const attempt of attempts) {
56
+ for (let attemptIdx = 0; attemptIdx < attempts.length; attemptIdx++) {
57
+ const attempt = attempts[attemptIdx];
57
58
  const isOriginal = attempt.attemptIndex === 0;
58
59
  const attemptLogId = isOriginal ? params.logId : randomUUID();
59
60
  const isFailoverLog = isOriginal && isFailoverIteration;
60
61
  const parentId = isOriginal ? (isFailoverIteration ? rootLogId : null) : params.logId;
62
+ // 中间 attempt 的 resilience_action 为 "retry"(否则不会继续尝试)
63
+ const isLastAttempt = attemptIdx === attempts.length - 1;
64
+ const attemptResilienceAction = isLastAttempt
65
+ ? (params.resilienceAction ?? null)
66
+ : "retry";
67
+ // 中间 attempt 的 resilience_reason 为 null(retry 不携带 reason,只有 abort 才有)
68
+ const attemptResilienceReason = isLastAttempt
69
+ ? (params.resilienceReason ?? null)
70
+ : null;
71
+ // 诊断字段:每次 attempt 通用的计算
72
+ const diagnosticFields = {
73
+ transport_kind: attempt.resultKind,
74
+ abort_reason: attempt.resultKind === "stream_abort" && result.kind === "stream_abort" ? result.abortReason ?? null : null,
75
+ error_code: attempt.error_code ?? null,
76
+ headers_sent: attempt.headers_sent != null ? (attempt.headers_sent ? 1 : 0) : null,
77
+ resilience_action: attemptResilienceAction,
78
+ resilience_reason: attemptResilienceReason,
79
+ mapping_reason: params.mappingReason ?? null,
80
+ failover_trigger: params.failoverTrigger ?? null,
81
+ };
61
82
  // 构建 writeContext(所有路径共享,error/stream_error 路径 status >= 400 所以 preserveDetail=true,但文件写入仍需执行)
62
83
  const attemptWriteContext = (params.matcher || params.logFileWriter) ? {
63
84
  matcher: params.matcher,
@@ -81,6 +102,7 @@ export function logResilienceResult(db, params, attempts, result, startTime) {
81
102
  router_key_id: params.routerKeyId, original_model: params.originalModel,
82
103
  session_id: params.sessionId,
83
104
  pipeline_snapshot: params.pipelineSnapshot ?? null,
105
+ ...diagnosticFields,
84
106
  }, attemptWriteContext);
85
107
  }
86
108
  else if (attempt.error) {
@@ -99,6 +121,7 @@ export function logResilienceResult(db, params, attempts, result, startTime) {
99
121
  router_key_id: params.routerKeyId, original_model: params.originalModel,
100
122
  session_id: params.sessionId,
101
123
  pipeline_snapshot: params.pipelineSnapshot ?? null,
124
+ ...diagnosticFields,
102
125
  }, attemptWriteContext);
103
126
  }
104
127
  else if (attempt.statusCode !== UPSTREAM_SUCCESS) {
@@ -115,6 +138,7 @@ export function logResilienceResult(db, params, attempts, result, startTime) {
115
138
  router_key_id: params.routerKeyId, original_model: params.originalModel,
116
139
  session_id: params.sessionId,
117
140
  pipeline_snapshot: params.pipelineSnapshot ?? null,
141
+ ...diagnosticFields,
118
142
  }, attemptWriteContext);
119
143
  }
120
144
  else {
@@ -136,6 +160,7 @@ export function logResilienceResult(db, params, attempts, result, startTime) {
136
160
  pipelineSnapshot: params.pipelineSnapshot,
137
161
  matcher: params.matcher,
138
162
  logFileWriter: params.logFileWriter,
163
+ ...diagnosticFields,
139
164
  });
140
165
  lastSuccessLogId = attemptLogId;
141
166
  }
@@ -85,7 +85,7 @@ class StreamProxy {
85
85
  result = { kind: "stream_error", ...base, body: extra.body, headers: this.sseHeaders, headersSent: this.headersSent || undefined };
86
86
  break;
87
87
  case "stream_abort":
88
- result = { kind: "stream_abort", ...base, metrics: extra.metrics, timeoutContext: extra.timeoutContext, timeoutMs: extra.timeoutMs };
88
+ result = { kind: "stream_abort", ...base, metrics: extra.metrics, timeoutContext: extra.timeoutContext, timeoutMs: extra.timeoutMs, abortReason: extra.abortReason };
89
89
  break;
90
90
  }
91
91
  // deferred 模式:先 resolve 让 handler 链路(日志写入等)在 microtask 中执行,
@@ -155,7 +155,7 @@ class StreamProxy {
155
155
  }
156
156
  catch { /* reply may be destroyed */ } // eslint-disable-line taste/no-silent-catch
157
157
  }
158
- this.terminal("stream_abort", { metrics: this.collectMetrics(false), timeoutContext: this.timeoutContext, timeoutMs: this.timeoutMs });
158
+ this.terminal("stream_abort", { metrics: this.collectMetrics(false), timeoutContext: this.timeoutContext, timeoutMs: this.timeoutMs, abortReason: "idle_timeout" });
159
159
  }, this.timeoutMs);
160
160
  }
161
161
  startStreaming() {
@@ -175,7 +175,7 @@ class StreamProxy {
175
175
  }
176
176
  catch {
177
177
  // 客户端在 state transition 和 writeHead 之间断连,可安全忽略
178
- this.terminal("stream_abort");
178
+ this.terminal("stream_abort", { abortReason: "client_disconnect" });
179
179
  return;
180
180
  }
181
181
  if (this.metricsTransform) {
@@ -209,7 +209,7 @@ class StreamProxy {
209
209
  if (this.state === "BUFFERING" || this.state === "STREAMING") {
210
210
  this.transition("ABORTED");
211
211
  }
212
- this.terminal("stream_abort", { metrics: this.collectMetrics(false) });
212
+ this.terminal("stream_abort", { metrics: this.collectMetrics(false), abortReason: "client_disconnect" });
213
213
  });
214
214
  }
215
215
  onData(chunk) {
@@ -279,8 +279,7 @@ class StreamProxy {
279
279
  }
280
280
  this.pipeEntry.write(chunk);
281
281
  if (this.loopGuard?.isTriggered()) {
282
- this.terminal("stream_abort", { metrics: this.collectMetrics(false) });
283
- return;
282
+ this.terminal("stream_abort", { metrics: this.collectMetrics(false), abortReason: "loop_detection" });
284
283
  }
285
284
  }
286
285
  onEnd() {
@@ -0,0 +1 @@
1
+ import{Gt as e,et as t,gt as n,it as r,qt as i,r as a,yt as o}from"./button-DsVDTV_5.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)=>(n(),t(`div`,{"data-slot":`card`,"data-size":r.size,class:i(e(a)(`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))},[o(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)=>(n(),t(`div`,{"data-slot":`card-content`,class:i(e(a)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[o(r.$slots,`default`)],2))}});export{c as n,l as t};
@@ -0,0 +1 @@
1
+ import{Gt as e,et as t,gt as n,it as r,qt as i,r as a,yt as o}from"./button-DsVDTV_5.js";var s=r({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(r){let s=r;return(r,c)=>(n(),t(`div`,{"data-slot":`card-header`,class:i(e(a)(`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))},[o(r.$slots,`default`)],2))}}),c=r({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(r){let s=r;return(r,c)=>(n(),t(`div`,{"data-slot":`card-title`,class:i(e(a)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[o(r.$slots,`default`)],2))}});export{s as n,c as t};
@@ -0,0 +1 @@
1
+ import{$ as e,Gt as t,H as n,J as r,L as i,Lt as a,Q as o,X as s,Xt as c,Z as l,et as u,gt as d,it as f,kt as p,qt as m,rt as h,vt as g}from"./button-DsVDTV_5.js";import{b as _,ft as v,ht as y,v as b,y as x}from"./index-DVY081yH.js";var S=i(`chevron-right`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),C=i(`plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),w=[`onMouseenter`],T={class:`truncate max-w-40`},E={key:0,class:`ml-1 text-[10px] px-1 py-px rounded bg-emerald-500/15 text-emerald-400 shrink-0`},D=[`onMouseenter`],O=[`onClick`],k={class:`truncate`},A={key:0,class:`shrink-0 text-xs text-muted-foreground`},j={key:0,class:`px-2 py-1.5 text-sm text-muted-foreground`},M=f({__name:`CascadingSelect`,props:{groups:{},modelValue:{},placeholder:{default:``},compact:{type:Boolean,default:!1}},emits:[`update:modelValue`],setup(i,{emit:f}){let{t:y}=n(),C=i,M=s(()=>C.placeholder||y(`common.selectPlaceholder`)),N=f,P=a(!1),F=a(null),I=s(()=>{if(!C.modelValue)return``;let e=C.groups.find(e=>e.key===C.modelValue.groupKey);if(!e)return``;let t=e.options.find(e=>e.value===C.modelValue.value);return t?`${e.label} / ${t.label}`:``});function L(e,t){N(`update:modelValue`,{groupKey:e,value:t}),P.value=!1}function R(e){P.value=e,e||(F.value=null)}return(n,a)=>(d(),o(t(_),{open:P.value,"onUpdate:open":R},{default:p(()=>[h(t(b),{"as-child":``},{default:p(()=>[l(`div`,{class:m([`flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer hover:bg-accent hover:text-accent-foreground`,[i.compact?`h-8 text-xs px-2.5 py-1`:`h-10 text-sm px-3 py-2`,{"ring-2 ring-ring ring-offset-2":P.value}]])},[l(`span`,{class:m([`truncate`,i.modelValue?`text-foreground`:`text-muted-foreground`])},c(I.value||M.value),3),h(t(v),{class:`h-4 w-4 shrink-0 opacity-50`})],2)]),_:1}),h(t(x),{align:`start`,"side-offset":4,class:`z-[200] w-auto min-w-56 overflow-visible p-1`},{default:p(()=>[(d(!0),u(r,null,g(i.groups,n=>(d(),u(`div`,{key:n.key,class:m([`relative flex cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground`,{"bg-accent text-accent-foreground z-10":F.value===n.key}]),onMouseenter:e=>F.value=n.key},[l(`span`,T,c(n.label),1),n.badge?(d(),u(`span`,E,c(n.badge),1)):e(``,!0),h(t(S),{class:`ml-1 h-4 w-4 shrink-0 opacity-50`}),F.value===n.key&&n.options.length>0?(d(),u(`div`,{key:1,class:`absolute left-full top-0 ml-0.5 min-w-48 rounded-md border bg-popover p-1 text-popover-foreground shadow-md`,onMouseenter:e=>F.value=n.key},[(d(!0),u(r,null,g(n.options,t=>(d(),u(`div`,{key:t.value,class:m([`flex cursor-pointer items-center justify-between gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground`,{"bg-accent text-accent-foreground":i.modelValue?.groupKey===n.key&&i.modelValue?.value===t.value}]),onClick:e=>L(n.key,t.value)},[l(`span`,k,c(t.label),1),t.tag?(d(),u(`span`,A,c(t.tag),1)):e(``,!0)],10,O))),128))],40,D)):e(``,!0)],42,w))),128)),i.groups.length===0?(d(),u(`div`,j,c(t(y)(`common.noOptions`)),1)):e(``,!0)]),_:1})]),_:1},8,[`open`]))}}),N=f({__name:`CascadingModelSelect`,props:{providers:{},modelValue:{},placeholder:{default:``},compact:{type:Boolean}},emits:[`update:modelValue`],setup(e,{emit:t}){let{t:r}=n(),i=e,a=s(()=>i.placeholder||r(`mappings.selectProviderModel`)),c=t,l=s(()=>i.providers.map(e=>({key:e.provider.id,label:e.provider.name,badge:e.isNew?r(`common.new`):void 0,options:e.models.map(e=>({value:e.name,label:e.name,tag:y(e.contextWindow)}))}))),u=s(()=>i.modelValue?{groupKey:i.modelValue.provider_id,value:i.modelValue.model}:void 0);function f(e){c(`update:modelValue`,{provider_id:e.groupKey,model:e.value})}return(t,n)=>(d(),o(M,{groups:l.value,"model-value":u.value,placeholder:a.value,compact:e.compact,"onUpdate:modelValue":f},null,8,[`groups`,`model-value`,`placeholder`,`compact`]))}});export{C as n,N as t};
@@ -0,0 +1 @@
1
+ import{$ as e,Gt as t,Jt as n,K as r,Q as i,X as a,dt as o,gt as s,i as c,it as l,kt as u,m as d,o as f,ot as p,q as m,r as h,rt as g,x as _,xt as v,yt as y}from"./button-DsVDTV_5.js";import{t as b}from"./VisuallyHiddenInput-C8GAHZzN.js";import{t as x}from"./RovingFocusItem-CPyYVYtN.js";import{J as S,K as C,R as w,U as T,V as E,X as D,pt as O}from"./index-DVY081yH.js";function k(e,t){return C(e)?!1:Array.isArray(e)?e.some(e=>D(e,t)):D(e,t)}var[A,j]=S(`CheckboxGroupRoot`);function M(e){return e===`indeterminate`}function N(e){return M(e)?`indeterminate`:e?`checked`:`unchecked`}var[P,F]=S(`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(n,{emit:l}){let p=n,h=l,{forwardRef:g,currentElement:_}=f(),S=A(null),w=d(p,`modelValue`,h,{defaultValue:p.defaultValue??p.falseValue,passive:p.modelValue===void 0}),E=a(()=>S?.disabled.value||p.disabled),O=a(()=>D(w.value,p.trueValue)),j=a(()=>C(S?.modelValue.value)?w.value===`indeterminate`?`indeterminate`:O.value:k(S.modelValue.value,p.value));function P(){if(C(S?.modelValue.value))w.value===`indeterminate`?w.value=p.trueValue:w.value=O.value?p.falseValue:p.trueValue;else{let e=[...S.modelValue.value||[]];if(k(e,p.value)){let t=e.findIndex(e=>D(e,p.value));e.splice(t,1)}else e.push(p.value);S.modelValue.value=e}}let I=T(_),L=a(()=>p.id&&_.value?document.querySelector(`[for="${p.id}"]`)?.innerText:void 0);return F({disabled:E,state:j}),(n,a)=>(s(),i(v(t(S)?.rovingFocus.value?t(x):t(c)),o(n.$attrs,{id:n.id,ref:t(g),role:`checkbox`,"as-child":n.asChild,as:n.as,type:n.as===`button`?`button`:void 0,"aria-checked":t(M)(j.value)?`mixed`:j.value,"aria-required":n.required,"aria-label":n.$attrs[`aria-label`]||L.value,"data-state":t(N)(j.value),"data-disabled":E.value?``:void 0,disabled:E.value,focusable:t(S)?.rovingFocus.value?!E.value:void 0,onKeydown:r(m(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:u(()=>[y(n.$slots,`default`,{modelValue:t(w),state:j.value}),t(I)&&n.name&&!t(S)?(s(),i(t(b),{key:0,type:`checkbox`,checked:!!j.value,name:n.name,value:n.value,disabled:E.value,required:n.required},null,8,[`checked`,`name`,`value`,`disabled`,`required`])):e(`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(e){let{forwardRef:n}=f(),r=P();return(e,a)=>(s(),i(t(w),{present:e.forceMount||t(M)(t(r).state.value)||t(r).state.value===!0},{default:u(()=>[g(t(c),o({ref:t(n),"data-state":t(N)(t(r).state.value),"data-disabled":t(r).disabled.value?``:void 0,style:{pointerEvents:`none`},"as-child":e.asChild,as:e.as},e.$attrs),{default:u(()=>[y(e.$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(e,{emit:r}){let a=e,c=r,l=E(_(a,`class`),c);return(e,r)=>(s(),i(t(I),o({"data-slot":`checkbox`},t(l),{class:t(h)(`border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-md border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50`,a.class)}),{default:u(r=>[g(t(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:u(()=>[y(e.$slots,`default`,n(p(r)),()=>[g(t(O))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};
@@ -0,0 +1 @@
1
+ import{$ as e,Et as t,Gt as n,Ht as r,Jt as i,Lt as a,Q as o,X as s,d as c,dt as l,ft as u,gt as d,i as f,it as p,kt as m,m as h,mt as g,o as _,ot as v,rt as y,yt as b}from"./button-DsVDTV_5.js";import{B as x,J as S,R as C,V as w}from"./index-DVY081yH.js";var[T,E]=S(`CollapsibleRoot`),D=p({__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:i}){let a=e,s=h(a,`open`,i,{defaultValue:a.defaultOpen,passive:a.open===void 0}),{disabled:c,unmountOnHide:l}=r(a);return E({contentId:``,disabled:c,open:s,unmountOnHide:l,onOpenToggle:()=>{c.value||(s.value=!s.value)}}),t({open:s}),_(),(e,t)=>(d(),o(n(f),{as:e.as,"as-child":a.asChild,"data-state":n(s)?`open`:`closed`,"data-disabled":n(c)?``:void 0},{default:m(()=>[b(e.$slots,`default`,{open:n(s)})]),_:3},8,[`as`,`as-child`,`data-state`,`data-disabled`]))}}),O=p({inheritAttrs:!1,__name:`CollapsibleContent`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`contentFound`],setup(r,{emit:i}){let p=r,h=i,v=T();v.contentId||=x(void 0,`reka-collapsible-content`);let S=a(),{forwardRef:w,currentElement:E}=_(),D=a(0),O=a(0),k=s(()=>v.open.value),A=a(k.value),j=a();t(()=>[k.value,S.value?.present],async()=>{await u();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=s(()=>A.value&&v.open.value);return g(()=>{requestAnimationFrame(()=>{A.value=!1})}),c(E,`beforematch`,e=>{requestAnimationFrame(()=>{v.onOpenToggle(),h(`contentFound`)})}),(t,r)=>(d(),o(n(C),{ref_key:`presentRef`,ref:S,present:t.forceMount||n(v).open.value,"force-mount":!0},{default:m(({present:r})=>[y(n(f),l(t.$attrs,{id:n(v).contentId,ref:n(w),"as-child":p.asChild,as:t.as,hidden:r?void 0:n(v).unmountOnHide.value?``:`until-found`,"data-state":M.value?void 0:n(v).open.value?`open`:`closed`,"data-disabled":n(v).disabled?.value?``:void 0,style:{"--reka-collapsible-content-height":`${O.value}px`,"--reka-collapsible-content-width":`${D.value}px`}}),{default:m(()=>[!n(v).unmountOnHide.value||r?b(t.$slots,`default`,{key:0}):e(`v-if`,!0)]),_:2},1040,[`id`,`as-child`,`as`,`hidden`,`data-state`,`data-disabled`,`style`])]),_:3},8,[`present`]))}}),k=p({__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 r=w(e,t);return(e,t)=>(d(),o(n(D),l({"data-slot":`collapsible`},n(r)),{default:m(t=>[b(e.$slots,`default`,i(v(t)))]),_:3},16))}}),A=p({__name:`CollapsibleContent`,props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{}},setup(e){let t=e;return(e,r)=>(d(),o(n(O),l({"data-slot":`collapsible-content`},t),{default:m(()=>[b(e.$slots,`default`)]),_:3},16))}});export{k as n,T as r,A as t};
@@ -0,0 +1 @@
1
+ import{Gt as e,Q as t,dt as n,gt as r,i,it as a,kt as o,o as s,yt as c}from"./button-DsVDTV_5.js";import{r as l}from"./CollapsibleContent-1Jnv6xUL.js";var u=a({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`}},setup(n){let a=n;s();let u=l();return(n,s)=>(r(),t(e(i),{type:n.as===`button`?`button`:void 0,as:n.as,"as-child":a.asChild,"aria-controls":e(u).contentId,"aria-expanded":e(u).open.value,"data-state":e(u).open.value?`open`:`closed`,"data-disabled":e(u).disabled?.value?``:void 0,disabled:e(u).disabled?.value,onClick:e(u).onOpenToggle},{default:o(()=>[c(n.$slots,`default`)]),_:3},8,[`type`,`as`,`as-child`,`aria-controls`,`aria-expanded`,`data-state`,`data-disabled`,`disabled`,`onClick`]))}}),d=a({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean},as:{}},setup(i){let a=i;return(i,s)=>(r(),t(e(u),n({"data-slot":`collapsible-trigger`},a),{default:o(()=>[c(i.$slots,`default`)]),_:3},16))}});export{d as t};