llm-simple-router 0.9.19 → 0.9.20

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 (55) hide show
  1. package/config/recommended-retry-rules.json +1 -0
  2. package/dist/admin/providers.js +22 -6
  3. package/dist/config/model-context.d.ts +2 -0
  4. package/dist/config/model-context.js +14 -6
  5. package/dist/core/types.d.ts +5 -0
  6. package/dist/db/index.js +45 -0
  7. package/dist/db/migrations/040_models_object_format.sql +4 -0
  8. package/dist/db/providers.d.ts +4 -0
  9. package/dist/db/providers.js +31 -0
  10. package/dist/proxy/handler/proxy-handler.js +19 -1
  11. package/dist/proxy/transport/stream.d.ts +4 -1
  12. package/dist/proxy/transport/stream.js +21 -6
  13. package/dist/proxy/transport/transport-fn.d.ts +4 -0
  14. package/dist/proxy/transport/transport-fn.js +1 -1
  15. package/frontend-dist/assets/{CardContent-DpdNrtkG.js → CardContent-uWoTAlVi.js} +1 -1
  16. package/frontend-dist/assets/{CardTitle-mrTD9vua.js → CardTitle-Cr8POTL5.js} +1 -1
  17. package/frontend-dist/assets/{Checkbox-1rtzAXdv.js → Checkbox-D2Z17AP8.js} +1 -1
  18. package/frontend-dist/assets/{CollapsibleContent-Gd8G1WPI.js → CollapsibleContent-BqCmqNIs.js} +1 -1
  19. package/frontend-dist/assets/{CollapsibleTrigger-DEs_2Gam.js → CollapsibleTrigger-B--uk4iR.js} +1 -1
  20. package/frontend-dist/assets/{Dashboard-yhOvjTlI.js → Dashboard-niqNN72b.js} +1 -1
  21. package/frontend-dist/assets/{Input-1iFstr5N.js → Input--tQLJqtT.js} +1 -1
  22. package/frontend-dist/assets/{Label-CRw4oQf2.js → Label-C4V5wJj3.js} +1 -1
  23. package/frontend-dist/assets/{Login-BJLsCn5J.js → Login-EWJo401l.js} +1 -1
  24. package/frontend-dist/assets/{Logs-DhCuj7rp.js → Logs-Pvhkbrof.js} +1 -1
  25. package/frontend-dist/assets/MappingEntryEditor-Ch7ahw0-.js +1 -0
  26. package/frontend-dist/assets/{ModelCard-PiwXmas-.js → ModelCard-BzFBBbml.js} +1 -1
  27. package/frontend-dist/assets/{ModelMappings-8u8jBFoA.js → ModelMappings-DeNasUhB.js} +1 -1
  28. package/frontend-dist/assets/{Monitor-UFRtv7Oi.js → Monitor-C5FdCHhI.js} +1 -1
  29. package/frontend-dist/assets/Providers-TZCIlKs9.js +1 -0
  30. package/frontend-dist/assets/{ProxyEnhancement-BxZCYxBL.js → ProxyEnhancement-D-pHVAHL.js} +1 -1
  31. package/frontend-dist/assets/{QuickSetup-CYVEtfPZ.js → QuickSetup-CdKq6YXW.js} +1 -1
  32. package/frontend-dist/assets/{RetryRules-DXU-NS1-.js → RetryRules-Dm0WmD9Z.js} +1 -1
  33. package/frontend-dist/assets/{RouterKeys-CUa64rMC.js → RouterKeys-CVFtDedl.js} +1 -1
  34. package/frontend-dist/assets/{RovingFocusItem-WFfF7xTx.js → RovingFocusItem-CP9lqnFp.js} +1 -1
  35. package/frontend-dist/assets/{Schedules-DJboHZQe.js → Schedules-BB2cufFc.js} +1 -1
  36. package/frontend-dist/assets/{Settings-C00E7rLh.js → Settings-Dc8uJ5Il.js} +1 -1
  37. package/frontend-dist/assets/{Setup-Bdg3g-7K.js → Setup-15RN4xTV.js} +1 -1
  38. package/frontend-dist/assets/{Switch-DXVDTQ8F.js → Switch-GUFZnjj9.js} +1 -1
  39. package/frontend-dist/assets/{TooltipTrigger-BcM0p009.js → TooltipTrigger-DhZJrVvO.js} +1 -1
  40. package/frontend-dist/assets/{TransformRulesForm-BhiHKW55.js → TransformRulesForm-D0BEkLYX.js} +1 -1
  41. package/frontend-dist/assets/{UnifiedRequestDialog-DniIXKEa.js → UnifiedRequestDialog-txL71pYo.js} +1 -1
  42. package/frontend-dist/assets/{VisuallyHiddenInput-Bae_wVf3.js → VisuallyHiddenInput-Itdmiinr.js} +1 -1
  43. package/frontend-dist/assets/{button-DMmB7tDy.js → button-CDY3J7p5.js} +2 -2
  44. package/frontend-dist/assets/{copy-3gKHS1GQ.js → copy-DyhMBltz.js} +1 -1
  45. package/frontend-dist/assets/{dialog-B68sJflZ.js → dialog-B3bc_Iev.js} +1 -1
  46. package/frontend-dist/assets/{index-QPBBCV_W.js → index-BZ7-9gPE.js} +2 -2
  47. package/frontend-dist/assets/index-iEMoIOdZ.css +1 -0
  48. package/frontend-dist/assets/{trash-2-C8JRtoE6.js → trash-2-BvFFjoxL.js} +1 -1
  49. package/frontend-dist/assets/{useClipboard-Df2zt0gp.js → useClipboard-BJeJ8Sah.js} +1 -1
  50. package/frontend-dist/assets/{useLogRetention-D79yrt_t.js → useLogRetention-BIEahe9Z.js} +1 -1
  51. package/frontend-dist/index.html +3 -3
  52. package/package.json +1 -1
  53. package/frontend-dist/assets/MappingEntryEditor-B7cc69AQ.js +0 -1
  54. package/frontend-dist/assets/Providers-CyUi2DVb.js +0 -1
  55. package/frontend-dist/assets/index-BdA9t4qo.css +0 -1
@@ -7,5 +7,6 @@
7
7
  { "name": "ZAI 速率限制 (HTTP 200, code 1302)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"1302\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000, "providers": ["智谱"] },
8
8
  { "name": "ZAI SSE 错误 (HTTP 200, code 500)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"500\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000, "providers": ["智谱"] },
9
9
  { "name": "ZAI SSE 错误 (HTTP 200, code 1234)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"1234\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000, "providers": ["智谱"] },
10
+ { "name": "ZAI 模型过载 (HTTP 200, code 1305)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"1305\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000, "providers": ["智谱"] },
10
11
  { "name": "KIMI 401 认证错误", "status_code": 401, "body_pattern": ".*authentication_error.*", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 3, "max_delay_ms": 60000, "providers": ["月之暗面"] }
11
12
  ]
@@ -58,10 +58,24 @@ function cascadeProviderDisable(db, providerId) {
58
58
  return result;
59
59
  }
60
60
  function extractModelOverrides(models) {
61
- const entries = models.map(m => typeof m === "string"
62
- ? { name: m, patches: [] }
63
- : { name: m.name, context_window: m.context_window, patches: m.patches ?? [] });
64
- const overrides = models.filter((m) => typeof m !== "string" && m.context_window != null);
61
+ const entries = [];
62
+ const overrides = [];
63
+ for (const m of models) {
64
+ if (typeof m === "string") {
65
+ entries.push({ name: m, patches: [] });
66
+ continue;
67
+ }
68
+ const name = m.name ?? m.id;
69
+ if (!name)
70
+ continue;
71
+ const entry = { name, patches: m.patches ?? [] };
72
+ if (m.stream_timeout_ms != null)
73
+ entry.stream_timeout_ms = m.stream_timeout_ms;
74
+ entries.push(entry);
75
+ if (m.name != null && m.context_window != null) {
76
+ overrides.push({ name: m.name, context_window: m.context_window });
77
+ }
78
+ }
65
79
  return { entries, overrides };
66
80
  }
67
81
  const API_KEY_PREVIEW_PREFIX_LEN = 4;
@@ -74,7 +88,8 @@ const CreateProviderSchema = Type.Object({
74
88
  api_key: Type.String({ minLength: 1 }),
75
89
  models: Type.Optional(Type.Array(Type.Union([
76
90
  Type.String(),
77
- Type.Object({ name: Type.String(), context_window: Type.Optional(Type.Number()), patches: Type.Optional(Type.Array(Type.String())) })
91
+ Type.Object({ name: Type.String(), context_window: Type.Optional(Type.Number()), patches: Type.Optional(Type.Array(Type.String())), stream_timeout_ms: Type.Optional(Type.Number({ minimum: 0, maximum: 86_400_000 })) }),
92
+ Type.Object({ id: Type.String(), stream_timeout_ms: Type.Optional(Type.Number({ minimum: 0, maximum: 86_400_000 })) })
78
93
  ]))),
79
94
  is_active: Type.Optional(Type.Number()),
80
95
  max_concurrency: Type.Optional(Type.Integer({ minimum: 0 })),
@@ -90,7 +105,8 @@ const UpdateProviderSchema = Type.Object({
90
105
  api_key: Type.Optional(Type.String({ minLength: 1 })),
91
106
  models: Type.Optional(Type.Array(Type.Union([
92
107
  Type.String(),
93
- Type.Object({ name: Type.String(), context_window: Type.Optional(Type.Number()), patches: Type.Optional(Type.Array(Type.String())) })
108
+ Type.Object({ name: Type.String(), context_window: Type.Optional(Type.Number()), patches: Type.Optional(Type.Array(Type.String())), stream_timeout_ms: Type.Optional(Type.Number({ minimum: 0, maximum: 86_400_000 })) }),
109
+ Type.Object({ id: Type.String(), stream_timeout_ms: Type.Optional(Type.Number({ minimum: 0, maximum: 86_400_000 })) })
94
110
  ]))),
95
111
  is_active: Type.Optional(Type.Number()),
96
112
  max_concurrency: Type.Optional(Type.Integer({ minimum: 0 })),
@@ -2,11 +2,13 @@ export interface ModelInfo {
2
2
  name: string;
3
3
  context_window: number | null;
4
4
  patches: string[];
5
+ stream_timeout_ms?: number;
5
6
  }
6
7
  export interface ModelEntry {
7
8
  name: string;
8
9
  context_window?: number;
9
10
  patches?: string[];
11
+ stream_timeout_ms?: number;
10
12
  }
11
13
  export declare const MODEL_CONTEXT_WINDOWS: Record<string, number>;
12
14
  export declare const DEFAULT_CONTEXT_WINDOW = 200000;
@@ -102,10 +102,13 @@ export function parseModels(raw) {
102
102
  const obj = item;
103
103
  if (!obj || !obj.name)
104
104
  return null;
105
- return {
105
+ const result = {
106
106
  name: obj.name,
107
107
  patches: (obj.patches ?? []).map(normalizePatchName),
108
108
  };
109
+ if (obj.stream_timeout_ms != null)
110
+ result.stream_timeout_ms = obj.stream_timeout_ms;
111
+ return result;
109
112
  }).filter((e) => e !== null);
110
113
  }
111
114
  catch {
@@ -113,9 +116,14 @@ export function parseModels(raw) {
113
116
  }
114
117
  }
115
118
  export function buildModelInfoList(modelEntries, overrides) {
116
- return modelEntries.map(entry => ({
117
- name: entry.name,
118
- context_window: overrides.get(entry.name) ?? lookupContextWindow(entry.name),
119
- patches: entry.patches ?? [],
120
- }));
119
+ return modelEntries.map(entry => {
120
+ const info = {
121
+ name: entry.name,
122
+ context_window: overrides.get(entry.name) ?? lookupContextWindow(entry.name),
123
+ patches: entry.patches ?? [],
124
+ };
125
+ if (entry.stream_timeout_ms != null)
126
+ info.stream_timeout_ms = entry.stream_timeout_ms;
127
+ return info;
128
+ });
121
129
  }
@@ -71,6 +71,11 @@ export type TransportResult = {
71
71
  metrics?: MetricsResult;
72
72
  upstreamResponseHeaders?: Record<string, string>;
73
73
  sentHeaders: Record<string, string>;
74
+ timeoutContext?: {
75
+ modelId: string;
76
+ providerId: string;
77
+ };
78
+ timeoutMs?: number;
74
79
  } | {
75
80
  kind: "error";
76
81
  statusCode: number;
package/dist/db/index.js CHANGED
@@ -84,8 +84,53 @@ export function initDatabase(dbPath) {
84
84
  throw err;
85
85
  }
86
86
  }
87
+ // 应用层迁移:SQL 无法安全处理的转换
88
+ runApplicationMigrations(db);
87
89
  return db;
88
90
  }
91
+ /**
92
+ * 应用层迁移:需要 Node.js 逻辑处理的 DB 转换。
93
+ * 在 SQL migration 执行完毕后运行。
94
+ */
95
+ function runApplicationMigrations(db) {
96
+ // 040: providers.models 从字符串数组转为对象数组
97
+ // ["glm-5.1"] → [{"id":"glm-5.1"}]
98
+ // 已有对象数组({name, patches})→ 补充 id 字段
99
+ const markerKey = "app_migration_040_models_object_format";
100
+ const done = db.prepare("SELECT value FROM settings WHERE key = ?").get(markerKey);
101
+ if (done)
102
+ return;
103
+ const providers = db.prepare("SELECT id, models FROM providers").all();
104
+ const update = db.prepare("UPDATE providers SET models = ? WHERE id = ?");
105
+ db.transaction(() => {
106
+ for (const p of providers) {
107
+ try {
108
+ const raw = JSON.parse(p.models);
109
+ if (!Array.isArray(raw) || raw.length === 0)
110
+ continue;
111
+ // 已是对象数组且每个元素都有 id → 无需转换
112
+ if (raw.every((m) => typeof m === "object" && m !== null && "id" in m))
113
+ continue;
114
+ const converted = raw.map((m) => {
115
+ if (typeof m === "string")
116
+ return { id: m };
117
+ const obj = m;
118
+ if (typeof obj !== "object" || obj === null)
119
+ return null;
120
+ // 已有 id → 保留;有 name 无 id → 用 name 作 id
121
+ if ("id" in obj)
122
+ return obj;
123
+ if ("name" in obj)
124
+ return { id: obj.name, ...obj };
125
+ return obj;
126
+ }).filter((m) => m !== null);
127
+ update.run(JSON.stringify(converted), p.id);
128
+ }
129
+ catch { /* JSON parse failed — skip this provider's models conversion */ } // eslint-disable-line taste/no-silent-catch
130
+ }
131
+ db.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)").run(markerKey, "done");
132
+ })();
133
+ }
89
134
  // --- Re-export from per-table modules ---
90
135
  export { getActiveProviders, getAllProviders, getProviderById, getActiveProviderByName, getActiveProvidersWithModels, createProvider, updateProvider, deleteProvider, PROVIDER_CONCURRENCY_DEFAULTS, } from "./providers.js";
91
136
  export { getMappingGroup, getMappingGroupById, getAllMappingGroups, createMappingGroup, updateMappingGroup, deleteMappingGroup, getActiveProviderModels, resolveByProviderModel, } from "./mappings.js";
@@ -0,0 +1,4 @@
1
+ -- 040_models_object_format.sql
2
+ -- No-op: string[] → object[] conversion is handled in application code (initDatabase)
3
+ -- See: router/src/db/index.ts runApplicationMigrations()
4
+ SELECT 1;
@@ -16,6 +16,10 @@ export interface Provider {
16
16
  created_at: string;
17
17
  updated_at: string;
18
18
  }
19
+ /** 默认流式超时 10 分钟 */
20
+ export declare const DEFAULT_STREAM_TIMEOUT_MS = 600000;
21
+ /** 从 provider 的 models JSON 中查找指定模型的超时值 */
22
+ export declare function getModelStreamTimeout(provider: Provider, backendModel: string): number;
19
23
  export declare const PROVIDER_CONCURRENCY_DEFAULTS: {
20
24
  readonly max_concurrency: 0;
21
25
  readonly queue_timeout_ms: 0;
@@ -1,5 +1,36 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import { buildUpdateQuery, deleteById } from "./helpers.js";
3
+ /** 默认流式超时 10 分钟 */
4
+ export const DEFAULT_STREAM_TIMEOUT_MS = 600_000;
5
+ /** 从 provider 的 models JSON 中查找指定模型的超时值 */
6
+ export function getModelStreamTimeout(provider, backendModel) {
7
+ try {
8
+ const raw = JSON.parse(provider.models);
9
+ if (!Array.isArray(raw))
10
+ return DEFAULT_STREAM_TIMEOUT_MS;
11
+ for (const m of raw) {
12
+ if (typeof m === "string") {
13
+ if (m === backendModel)
14
+ return DEFAULT_STREAM_TIMEOUT_MS;
15
+ continue;
16
+ }
17
+ const obj = m;
18
+ if (!obj || typeof obj !== "object")
19
+ continue;
20
+ const modelId = (obj.name ?? obj.id);
21
+ if (modelId === backendModel) {
22
+ const timeout = obj.stream_timeout_ms;
23
+ // stream_timeout_ms: 0 表示禁用超时,返回 Infinity;
24
+ // undefined/null/未设置 表示使用默认值
25
+ if (timeout === 0)
26
+ return Number.POSITIVE_INFINITY;
27
+ return timeout ?? DEFAULT_STREAM_TIMEOUT_MS;
28
+ }
29
+ }
30
+ }
31
+ catch { /* ignore parse errors — models field may be empty or invalid */ } // eslint-disable-line taste/no-silent-catch
32
+ return DEFAULT_STREAM_TIMEOUT_MS;
33
+ }
3
34
  export const PROVIDER_CONCURRENCY_DEFAULTS = {
4
35
  max_concurrency: 0,
5
36
  queue_timeout_ms: 0,
@@ -3,6 +3,7 @@ import { HTTP_UNPROCESSABLE_ENTITY } from "../../core/constants.js";
3
3
  import { getProviderById, insertRequestLog, updateLogStreamContent, updateLogClientStatus } from "../../db/index.js";
4
4
  import { decrypt } from "../../utils/crypto.js";
5
5
  import { getSetting } from "../../db/settings.js";
6
+ import { getModelStreamTimeout } from "../../db/providers.js";
6
7
  import { resolveMapping } from "../routing/mapping-resolver.js";
7
8
  import { SemaphoreQueueFullError, SemaphoreTimeoutError } from "@llm-router/core";
8
9
  import { logResilienceResult, collectTransportMetrics, sanitizeHeadersForLog, } from "../proxy-logging.js";
@@ -279,8 +280,9 @@ async function executeFailoverLoop(ctx) {
279
280
  const transportFn = buildTransportFn({
280
281
  provider, apiKey, body: patchedBody, cliHdrs, reply, upstreamPath: effectiveUpstreamPath, apiType: effectiveApiType,
281
282
  isStream, startTime, logId, effectiveModel,
282
- streamTimeoutMs: config.STREAM_TIMEOUT_MS, tracker, matcher, request,
283
+ streamTimeoutMs: getModelStreamTimeout(provider, resolved.backend_model), tracker, matcher, request,
283
284
  streamLoopEnabled, formatTransform, responseTransform, injectedHeaders,
285
+ timeoutContext: { modelId: resolved.backend_model, providerId: provider.id },
284
286
  });
285
287
  const pipelineSnapshot = iterationSnapshot.toJSON();
286
288
  try {
@@ -293,6 +295,22 @@ async function executeFailoverLoop(ctx) {
293
295
  matcher, logFileWriter,
294
296
  }, resilienceResult.attempts, resilienceResult.result, startTime);
295
297
  collectTransportMetrics(deps.db, apiType, resilienceResult.result, isStream, lastLogId, provider.id, resolved.backend_model, request, routerKeyId, getTransportStatusCode(resilienceResult.result));
298
+ // Stream timeout: send 408 error with API-specific body to client
299
+ if (resilienceResult.result.kind === "stream_abort" && resilienceResult.result.timeoutContext) {
300
+ const { modelId, providerId } = resilienceResult.result.timeoutContext;
301
+ const msg = `Stream timeout: no data received for ${resilienceResult.result.timeoutMs}ms (model: ${modelId}, provider: ${providerId})`;
302
+ const errBody = apiType === "anthropic"
303
+ ? { type: "error", error: { type: "api_error", message: msg } }
304
+ : { error: { message: msg, type: "server_error", code: "stream_timeout" } };
305
+ try {
306
+ reply.raw.write(`data: ${JSON.stringify(errBody)}\n\n`);
307
+ }
308
+ catch { /* client disconnected */ } // eslint-disable-line taste/no-silent-catch
309
+ try {
310
+ reply.raw.end();
311
+ }
312
+ catch { /* client disconnected */ } // eslint-disable-line taste/no-silent-catch
313
+ }
296
314
  const tr = resilienceResult.result;
297
315
  const succeeded = tr.kind === "success" || tr.kind === "stream_success" || tr.kind === "stream_abort";
298
316
  if (succeeded)
@@ -5,4 +5,7 @@ import type { StreamLoopGuard } from "@llm-router/core/loop-prevention";
5
5
  import { type BuildHeadersFn } from "./http.js";
6
6
  export declare function callStream(backend: {
7
7
  base_url: string;
8
- }, apiKey: string, body: Record<string, unknown>, clientHeaders: RawHeaders, reply: FastifyReply, timeoutMs: number, upstreamPath: string, buildHeaders: BuildHeadersFn, metricsTransform?: SSEMetricsTransform, checkEarlyError?: (bufferedData: string) => boolean, compatResolve?: (result: TransportResult) => void, loopGuard?: StreamLoopGuard, formatTransform?: import("stream").Transform): Promise<TransportResult>;
8
+ }, apiKey: string, body: Record<string, unknown>, clientHeaders: RawHeaders, reply: FastifyReply, timeoutMs: number, upstreamPath: string, buildHeaders: BuildHeadersFn, metricsTransform?: SSEMetricsTransform, checkEarlyError?: (bufferedData: string) => boolean, compatResolve?: (result: TransportResult) => void, loopGuard?: StreamLoopGuard, formatTransform?: import("stream").Transform, timeoutContext?: {
9
+ modelId: string;
10
+ providerId: string;
11
+ }, onTimeoutAbort?: () => void): Promise<TransportResult>;
@@ -12,6 +12,8 @@ class StreamProxy {
12
12
  checkEarlyError;
13
13
  timeoutMs;
14
14
  loopGuard;
15
+ timeoutContext;
16
+ onTimeoutAbort;
15
17
  state = "BUFFERING";
16
18
  resolved = false;
17
19
  resolveFn = null;
@@ -28,7 +30,7 @@ class StreamProxy {
28
30
  // 流式阶段 SSE error 扫描缓冲(跨 chunk 边界匹配)
29
31
  sseScanBuffer = "";
30
32
  static SSE_SCAN_MAX = 8 * 1024; // eslint-disable-line no-magic-numbers -- 8KB scan buffer
31
- constructor(statusCode, rawUpstreamHeaders, sentUpstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs, loopGuard, formatTransform) {
33
+ constructor(statusCode, rawUpstreamHeaders, sentUpstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs, loopGuard, formatTransform, timeoutContext, onTimeoutAbort) {
32
34
  this.statusCode = statusCode;
33
35
  this.sentUpstreamHeaders = sentUpstreamHeaders;
34
36
  this.reply = reply;
@@ -36,6 +38,8 @@ class StreamProxy {
36
38
  this.checkEarlyError = checkEarlyError;
37
39
  this.timeoutMs = timeoutMs;
38
40
  this.loopGuard = loopGuard;
41
+ this.timeoutContext = timeoutContext;
42
+ this.onTimeoutAbort = onTimeoutAbort;
39
43
  this.formatTransform = formatTransform;
40
44
  this.sseHeaders = filterHeaders(rawUpstreamHeaders);
41
45
  this.sseHeaders["Content-Type"] = "text/event-stream";
@@ -79,7 +83,7 @@ class StreamProxy {
79
83
  result = { kind: "stream_error", ...base, body: extra.body, headers: this.sseHeaders, headersSent: this.headersSent || undefined };
80
84
  break;
81
85
  case "stream_abort":
82
- result = { kind: "stream_abort", ...base, metrics: extra.metrics };
86
+ result = { kind: "stream_abort", ...base, metrics: extra.metrics, timeoutContext: extra.timeoutContext, timeoutMs: extra.timeoutMs };
83
87
  break;
84
88
  }
85
89
  // deferred 模式:先 resolve 让 handler 链路(日志写入等)在 microtask 中执行,
@@ -94,7 +98,8 @@ class StreamProxy {
94
98
  }
95
99
  else {
96
100
  // stream_abort 且 headers 已发送时,必须 end reply 避免客户端挂起
97
- if (kind === "stream_abort" && this.headersSent) {
101
+ // 但如果有 timeoutContext,让 handler 层负责写入错误 SSE 后再 end
102
+ if (kind === "stream_abort" && this.headersSent && !extra.timeoutContext) {
98
103
  // eslint-disable-next-line taste/no-silent-catch -- reply may already be destroyed, warn is sufficient
99
104
  try {
100
105
  this.reply.raw.end();
@@ -132,10 +137,20 @@ class StreamProxy {
132
137
  resetIdleTimer() {
133
138
  if (this.idleTimer)
134
139
  clearTimeout(this.idleTimer);
140
+ if (!isFinite(this.timeoutMs) || this.timeoutMs <= 0)
141
+ return; // 0 或 Infinity 表示禁用超时
135
142
  this.idleTimer = setTimeout(() => {
136
143
  if (this.resolved)
137
144
  return;
138
- this.terminal("stream_abort", { metrics: this.collectMetrics(false) });
145
+ // 在 terminal() 调用 reply.raw.end() 之前,同步写入超时错误 SSE
146
+ // 必须同步执行,确保 inject() 能正确收集响应体
147
+ if (this.onTimeoutAbort) {
148
+ try {
149
+ this.onTimeoutAbort();
150
+ }
151
+ catch { /* reply may be destroyed */ } // eslint-disable-line taste/no-silent-catch
152
+ }
153
+ this.terminal("stream_abort", { metrics: this.collectMetrics(false), timeoutContext: this.timeoutContext, timeoutMs: this.timeoutMs });
139
154
  }, this.timeoutMs);
140
155
  }
141
156
  startStreaming() {
@@ -292,7 +307,7 @@ class StreamProxy {
292
307
  }
293
308
  }
294
309
  // ---------- callStream ----------
295
- export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutMs, upstreamPath, buildHeaders, metricsTransform, checkEarlyError, compatResolve, loopGuard, formatTransform) {
310
+ export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutMs, upstreamPath, buildHeaders, metricsTransform, checkEarlyError, compatResolve, loopGuard, formatTransform, timeoutContext, onTimeoutAbort) {
296
311
  return new Promise((resolve) => {
297
312
  const effectiveResolve = compatResolve ?? resolve;
298
313
  const url = new URL(buildUpstreamUrl(backend.base_url, upstreamPath));
@@ -316,7 +331,7 @@ export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutM
316
331
  });
317
332
  return;
318
333
  }
319
- const proxy = new StreamProxy(statusCode, upstreamRes.headers, upstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs, loopGuard, formatTransform);
334
+ const proxy = new StreamProxy(statusCode, upstreamRes.headers, upstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs, loopGuard, formatTransform, timeoutContext, onTimeoutAbort);
320
335
  proxy.bindResolve(effectiveResolve);
321
336
  proxy.registerCloseHandler();
322
337
  // 无 early error checker 时直接开始流式传输
@@ -24,5 +24,9 @@ export interface TransportFnParams {
24
24
  formatTransform?: import("stream").Transform;
25
25
  responseTransform?: (body: string) => string;
26
26
  injectedHeaders?: Record<string, string>;
27
+ timeoutContext?: {
28
+ modelId: string;
29
+ providerId: string;
30
+ };
27
31
  }
28
32
  export declare function buildTransportFn(p: TransportFnParams): (target: Target) => Promise<TransportResult>;
@@ -52,7 +52,7 @@ export function buildTransportFn(p) {
52
52
  onContentDelta: streamLoopGuard ? (text) => streamLoopGuard.feed(text) : undefined,
53
53
  });
54
54
  const checkEarlyError = p.matcher ? (data) => p.matcher.test(UPSTREAM_SUCCESS, data) : undefined;
55
- const streamResult = await callStream(p.provider, p.apiKey, p.body, p.cliHdrs, p.reply, p.streamTimeoutMs, p.upstreamPath, buildHeaders, metricsTransform, checkEarlyError, undefined, streamLoopGuard, p.formatTransform);
55
+ const streamResult = await callStream(p.provider, p.apiKey, p.body, p.cliHdrs, p.reply, p.streamTimeoutMs, p.upstreamPath, buildHeaders, metricsTransform, checkEarlyError, undefined, streamLoopGuard, p.formatTransform, p.timeoutContext);
56
56
  const m = (streamResult.kind === "stream_success" || streamResult.kind === "stream_abort")
57
57
  ? streamResult.metrics : undefined;
58
58
  if (m)
@@ -1 +1 @@
1
- import{Bt as e,Q as t,Rt as n,Y as r,pt as i,r as a,ut as o}from"./button-DMmB7tDy.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)=>(o(),r(`div`,{"data-slot":`card`,"data-size":t.size,class:e(n(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))},[i(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)=>(o(),r(`div`,{"data-slot":`card-content`,class:e(n(a)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[i(t.$slots,`default`)],2))}});export{c as n,l as t};
1
+ import{Bt as e,Q as t,Rt as n,Y as r,pt as i,r as a,ut as o}from"./button-CDY3J7p5.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)=>(o(),r(`div`,{"data-slot":`card`,"data-size":t.size,class:e(n(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))},[i(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)=>(o(),r(`div`,{"data-slot":`card-content`,class:e(n(a)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[i(t.$slots,`default`)],2))}});export{c as n,l as t};
@@ -1 +1 @@
1
- import{Bt as e,Q as t,Rt as n,Y as r,pt as i,r as a,ut as o}from"./button-DMmB7tDy.js";var s=t({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(t){let s=t;return(t,c)=>(o(),r(`div`,{"data-slot":`card-header`,class:e(n(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))},[i(t.$slots,`default`)],2))}}),c=t({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(t){let s=t;return(t,c)=>(o(),r(`div`,{"data-slot":`card-title`,class:e(n(a)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[i(t.$slots,`default`)],2))}});export{s as n,c as t};
1
+ import{Bt as e,Q as t,Rt as n,Y as r,pt as i,r as a,ut as o}from"./button-CDY3J7p5.js";var s=t({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(t){let s=t;return(t,c)=>(o(),r(`div`,{"data-slot":`card-header`,class:e(n(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))},[i(t.$slots,`default`)],2))}}),c=t({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(t){let s=t;return(t,c)=>(o(),r(`div`,{"data-slot":`card-title`,class:e(n(a)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[i(t.$slots,`default`)],2))}});export{s as n,c as t};
@@ -1 +1 @@
1
- import{Ct as e,G as t,H as n,J as r,Q as i,Rt as a,V as o,Vt as s,Z as c,at as l,et as u,ht as d,i as f,m as p,o as m,pt as h,q as g,r as _,ut as v,x as y}from"./button-DMmB7tDy.js";import{t as b}from"./VisuallyHiddenInput-Bae_wVf3.js";import{t as x}from"./RovingFocusItem-WFfF7xTx.js";import{B as S,G as C,H as w,L as T,Y as E,nt as D,q as O}from"./index-QPBBCV_W.js";function k(e,t){return C(e)?!1:Array.isArray(e)?e.some(e=>E(e,t)):E(e,t)}var[A,j]=O(`CheckboxGroupRoot`);function M(e){return e===`indeterminate`}function N(e){return M(e)?`indeterminate`:e?`checked`:`unchecked`}var[P,F]=O(`CheckboxRoot`),I=i({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(i,{emit:s}){let c=i,u=s,{forwardRef:_,currentElement:y}=m(),S=A(null),T=p(c,`modelValue`,u,{defaultValue:c.defaultValue??c.falseValue,passive:c.modelValue===void 0}),D=t(()=>S?.disabled.value||c.disabled),O=t(()=>E(T.value,c.trueValue)),j=t(()=>C(S?.modelValue.value)?T.value===`indeterminate`?`indeterminate`:O.value:k(S.modelValue.value,c.value));function P(){if(C(S?.modelValue.value))T.value===`indeterminate`?T.value=c.trueValue:T.value=O.value?c.falseValue:c.trueValue;else{let e=[...S.modelValue.value||[]];if(k(e,c.value)){let t=e.findIndex(e=>E(e,c.value));e.splice(t,1)}else e.push(c.value);S.modelValue.value=e}}let I=w(y),L=t(()=>c.id&&y.value?document.querySelector(`[for="${c.id}"]`)?.innerText:void 0);return F({disabled:D,state:j}),(t,i)=>(v(),g(d(a(S)?.rovingFocus.value?a(x):a(f)),l(t.$attrs,{id:t.id,ref:a(_),role:`checkbox`,"as-child":t.asChild,as:t.as,type:t.as===`button`?`button`:void 0,"aria-checked":a(M)(j.value)?`mixed`:j.value,"aria-required":t.required,"aria-label":t.$attrs[`aria-label`]||L.value,"data-state":a(N)(j.value),"data-disabled":D.value?``:void 0,disabled:D.value,focusable:a(S)?.rovingFocus.value?!D.value:void 0,onKeydown:o(n(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:e(()=>[h(t.$slots,`default`,{modelValue:a(T),state:j.value}),a(I)&&t.name&&!a(S)?(v(),g(a(b),{key:0,type:`checkbox`,checked:!!j.value,name:t.name,value:t.value,disabled:D.value,required:t.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=i({__name:`CheckboxIndicator`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`span`}},setup(t){let{forwardRef:n}=m(),r=P();return(t,i)=>(v(),g(a(T),{present:t.forceMount||a(M)(a(r).state.value)||a(r).state.value===!0},{default:e(()=>[c(a(f),l({ref:a(n),"data-state":a(N)(a(r).state.value),"data-disabled":a(r).disabled.value?``:void 0,style:{pointerEvents:`none`},"as-child":t.asChild,as:t.as},t.$attrs),{default:e(()=>[h(t.$slots,`default`)]),_:3},16,[`data-state`,`data-disabled`,`as-child`,`as`])]),_:3},8,[`present`]))}}),R=i({__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:n}){let r=t,i=n,o=S(y(r,`class`),i);return(t,n)=>(v(),g(a(I),l({"data-slot":`checkbox`},a(o),{class:a(_)(`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`,r.class)}),{default:e(n=>[c(a(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:e(()=>[h(t.$slots,`default`,s(u(n)),()=>[c(a(D))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};
1
+ import{Ct as e,G as t,H as n,J as r,Q as i,Rt as a,V as o,Vt as s,Z as c,at as l,et as u,ht as d,i as f,m as p,o as m,pt as h,q as g,r as _,ut as v,x as y}from"./button-CDY3J7p5.js";import{t as b}from"./VisuallyHiddenInput-Itdmiinr.js";import{t as x}from"./RovingFocusItem-CP9lqnFp.js";import{B as S,G as C,H as w,L as T,Y as E,nt as D,q as O}from"./index-BZ7-9gPE.js";function k(e,t){return C(e)?!1:Array.isArray(e)?e.some(e=>E(e,t)):E(e,t)}var[A,j]=O(`CheckboxGroupRoot`);function M(e){return e===`indeterminate`}function N(e){return M(e)?`indeterminate`:e?`checked`:`unchecked`}var[P,F]=O(`CheckboxRoot`),I=i({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(i,{emit:s}){let c=i,u=s,{forwardRef:_,currentElement:y}=m(),S=A(null),T=p(c,`modelValue`,u,{defaultValue:c.defaultValue??c.falseValue,passive:c.modelValue===void 0}),D=t(()=>S?.disabled.value||c.disabled),O=t(()=>E(T.value,c.trueValue)),j=t(()=>C(S?.modelValue.value)?T.value===`indeterminate`?`indeterminate`:O.value:k(S.modelValue.value,c.value));function P(){if(C(S?.modelValue.value))T.value===`indeterminate`?T.value=c.trueValue:T.value=O.value?c.falseValue:c.trueValue;else{let e=[...S.modelValue.value||[]];if(k(e,c.value)){let t=e.findIndex(e=>E(e,c.value));e.splice(t,1)}else e.push(c.value);S.modelValue.value=e}}let I=w(y),L=t(()=>c.id&&y.value?document.querySelector(`[for="${c.id}"]`)?.innerText:void 0);return F({disabled:D,state:j}),(t,i)=>(v(),g(d(a(S)?.rovingFocus.value?a(x):a(f)),l(t.$attrs,{id:t.id,ref:a(_),role:`checkbox`,"as-child":t.asChild,as:t.as,type:t.as===`button`?`button`:void 0,"aria-checked":a(M)(j.value)?`mixed`:j.value,"aria-required":t.required,"aria-label":t.$attrs[`aria-label`]||L.value,"data-state":a(N)(j.value),"data-disabled":D.value?``:void 0,disabled:D.value,focusable:a(S)?.rovingFocus.value?!D.value:void 0,onKeydown:o(n(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:e(()=>[h(t.$slots,`default`,{modelValue:a(T),state:j.value}),a(I)&&t.name&&!a(S)?(v(),g(a(b),{key:0,type:`checkbox`,checked:!!j.value,name:t.name,value:t.value,disabled:D.value,required:t.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=i({__name:`CheckboxIndicator`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`span`}},setup(t){let{forwardRef:n}=m(),r=P();return(t,i)=>(v(),g(a(T),{present:t.forceMount||a(M)(a(r).state.value)||a(r).state.value===!0},{default:e(()=>[c(a(f),l({ref:a(n),"data-state":a(N)(a(r).state.value),"data-disabled":a(r).disabled.value?``:void 0,style:{pointerEvents:`none`},"as-child":t.asChild,as:t.as},t.$attrs),{default:e(()=>[h(t.$slots,`default`)]),_:3},16,[`data-state`,`data-disabled`,`as-child`,`as`])]),_:3},8,[`present`]))}}),R=i({__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:n}){let r=t,i=n,o=S(y(r,`class`),i);return(t,n)=>(v(),g(a(I),l({"data-slot":`checkbox`},a(o),{class:a(_)(`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`,r.class)}),{default:e(n=>[c(a(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:e(()=>[h(t.$slots,`default`,s(u(n)),()=>[c(a(D))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};
@@ -1 +1 @@
1
- import{Ct as e,G as t,It as n,J as r,Q as i,Rt as a,Vt as o,Z as s,at as c,bt as l,ct as u,d,et as f,i as p,jt as m,m as h,o as g,ot as _,pt as v,q as y,ut as b}from"./button-DMmB7tDy.js";import{B as x,L as S,q as C,z as w}from"./index-QPBBCV_W.js";var[T,E]=C(`CollapsibleRoot`),D=i({__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(t,{expose:r,emit:i}){let o=t,s=h(o,`open`,i,{defaultValue:o.defaultOpen,passive:o.open===void 0}),{disabled:c,unmountOnHide:l}=n(o);return E({contentId:``,disabled:c,open:s,unmountOnHide:l,onOpenToggle:()=>{c.value||(s.value=!s.value)}}),r({open:s}),g(),(t,n)=>(b(),y(a(p),{as:t.as,"as-child":o.asChild,"data-state":a(s)?`open`:`closed`,"data-disabled":a(c)?``:void 0},{default:e(()=>[v(t.$slots,`default`,{open:a(s)})]),_:3},8,[`as`,`as-child`,`data-state`,`data-disabled`]))}}),O=i({inheritAttrs:!1,__name:`CollapsibleContent`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`contentFound`],setup(n,{emit:i}){let o=n,f=i,h=T();h.contentId||=w(void 0,`reka-collapsible-content`);let x=m(),{forwardRef:C,currentElement:E}=g(),D=m(0),O=m(0),k=t(()=>h.open.value),A=m(k.value),j=m();l(()=>[k.value,x.value?.present],async()=>{await _();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=t(()=>A.value&&h.open.value);return u(()=>{requestAnimationFrame(()=>{A.value=!1})}),d(E,`beforematch`,e=>{requestAnimationFrame(()=>{h.onOpenToggle(),f(`contentFound`)})}),(t,n)=>(b(),y(a(S),{ref_key:`presentRef`,ref:x,present:t.forceMount||a(h).open.value,"force-mount":!0},{default:e(({present:n})=>[s(a(p),c(t.$attrs,{id:a(h).contentId,ref:a(C),"as-child":o.asChild,as:t.as,hidden:n?void 0:a(h).unmountOnHide.value?``:`until-found`,"data-state":M.value?void 0:a(h).open.value?`open`:`closed`,"data-disabled":a(h).disabled?.value?``:void 0,style:{"--reka-collapsible-content-height":`${O.value}px`,"--reka-collapsible-content-width":`${D.value}px`}}),{default:e(()=>[!a(h).unmountOnHide.value||n?v(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=i({__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=x(t,n);return(t,n)=>(b(),y(a(D),c({"data-slot":`collapsible`},a(r)),{default:e(e=>[v(t.$slots,`default`,o(f(e)))]),_:3},16))}}),A=i({__name:`CollapsibleContent`,props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{}},setup(t){let n=t;return(t,r)=>(b(),y(a(O),c({"data-slot":`collapsible-content`},n),{default:e(()=>[v(t.$slots,`default`)]),_:3},16))}});export{k as n,T as r,A as t};
1
+ import{Ct as e,G as t,It as n,J as r,Q as i,Rt as a,Vt as o,Z as s,at as c,bt as l,ct as u,d,et as f,i as p,jt as m,m as h,o as g,ot as _,pt as v,q as y,ut as b}from"./button-CDY3J7p5.js";import{B as x,L as S,q as C,z as w}from"./index-BZ7-9gPE.js";var[T,E]=C(`CollapsibleRoot`),D=i({__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(t,{expose:r,emit:i}){let o=t,s=h(o,`open`,i,{defaultValue:o.defaultOpen,passive:o.open===void 0}),{disabled:c,unmountOnHide:l}=n(o);return E({contentId:``,disabled:c,open:s,unmountOnHide:l,onOpenToggle:()=>{c.value||(s.value=!s.value)}}),r({open:s}),g(),(t,n)=>(b(),y(a(p),{as:t.as,"as-child":o.asChild,"data-state":a(s)?`open`:`closed`,"data-disabled":a(c)?``:void 0},{default:e(()=>[v(t.$slots,`default`,{open:a(s)})]),_:3},8,[`as`,`as-child`,`data-state`,`data-disabled`]))}}),O=i({inheritAttrs:!1,__name:`CollapsibleContent`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`contentFound`],setup(n,{emit:i}){let o=n,f=i,h=T();h.contentId||=w(void 0,`reka-collapsible-content`);let x=m(),{forwardRef:C,currentElement:E}=g(),D=m(0),O=m(0),k=t(()=>h.open.value),A=m(k.value),j=m();l(()=>[k.value,x.value?.present],async()=>{await _();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=t(()=>A.value&&h.open.value);return u(()=>{requestAnimationFrame(()=>{A.value=!1})}),d(E,`beforematch`,e=>{requestAnimationFrame(()=>{h.onOpenToggle(),f(`contentFound`)})}),(t,n)=>(b(),y(a(S),{ref_key:`presentRef`,ref:x,present:t.forceMount||a(h).open.value,"force-mount":!0},{default:e(({present:n})=>[s(a(p),c(t.$attrs,{id:a(h).contentId,ref:a(C),"as-child":o.asChild,as:t.as,hidden:n?void 0:a(h).unmountOnHide.value?``:`until-found`,"data-state":M.value?void 0:a(h).open.value?`open`:`closed`,"data-disabled":a(h).disabled?.value?``:void 0,style:{"--reka-collapsible-content-height":`${O.value}px`,"--reka-collapsible-content-width":`${D.value}px`}}),{default:e(()=>[!a(h).unmountOnHide.value||n?v(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=i({__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=x(t,n);return(t,n)=>(b(),y(a(D),c({"data-slot":`collapsible`},a(r)),{default:e(e=>[v(t.$slots,`default`,o(f(e)))]),_:3},16))}}),A=i({__name:`CollapsibleContent`,props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{}},setup(t){let n=t;return(t,r)=>(b(),y(a(O),c({"data-slot":`collapsible-content`},n),{default:e(()=>[v(t.$slots,`default`)]),_:3},16))}});export{k as n,T as r,A as t};
@@ -1 +1 @@
1
- import{Ct as e,Q as t,Rt as n,at as r,i,o as a,pt as o,q as s,ut as c}from"./button-DMmB7tDy.js";import{r as l}from"./CollapsibleContent-Gd8G1WPI.js";var u=t({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`}},setup(t){let r=t;a();let u=l();return(t,a)=>(c(),s(n(i),{type:t.as===`button`?`button`:void 0,as:t.as,"as-child":r.asChild,"aria-controls":n(u).contentId,"aria-expanded":n(u).open.value,"data-state":n(u).open.value?`open`:`closed`,"data-disabled":n(u).disabled?.value?``:void 0,disabled:n(u).disabled?.value,onClick:n(u).onOpenToggle},{default:e(()=>[o(t.$slots,`default`)]),_:3},8,[`type`,`as`,`as-child`,`aria-controls`,`aria-expanded`,`data-state`,`data-disabled`,`disabled`,`onClick`]))}}),d=t({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean},as:{}},setup(t){let i=t;return(t,a)=>(c(),s(n(u),r({"data-slot":`collapsible-trigger`},i),{default:e(()=>[o(t.$slots,`default`)]),_:3},16))}});export{d as t};
1
+ import{Ct as e,Q as t,Rt as n,at as r,i,o as a,pt as o,q as s,ut as c}from"./button-CDY3J7p5.js";import{r as l}from"./CollapsibleContent-BqCmqNIs.js";var u=t({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`}},setup(t){let r=t;a();let u=l();return(t,a)=>(c(),s(n(i),{type:t.as===`button`?`button`:void 0,as:t.as,"as-child":r.asChild,"aria-controls":n(u).contentId,"aria-expanded":n(u).open.value,"data-state":n(u).open.value?`open`:`closed`,"data-disabled":n(u).disabled?.value?``:void 0,disabled:n(u).disabled?.value,onClick:n(u).onOpenToggle},{default:e(()=>[o(t.$slots,`default`)]),_:3},8,[`type`,`as`,`as-child`,`aria-controls`,`aria-expanded`,`data-state`,`data-disabled`,`disabled`,`onClick`]))}}),d=t({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean},as:{}},setup(t){let i=t;return(t,a)=>(c(),s(n(u),r({"data-slot":`collapsible-trigger`},i),{default:e(()=>[o(t.$slots,`default`)]),_:3},16))}});export{d as t};