llm-simple-router 1.1.2 → 1.1.3

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 (127) hide show
  1. package/dist/admin/providers.js +6 -4
  2. package/dist/admin/quick-setup.js +3 -1
  3. package/dist/app/register-routes.js +2 -0
  4. package/dist/config/model-context.d.ts +2 -0
  5. package/dist/config/model-context.js +4 -0
  6. package/dist/core/concurrency/semaphore.d.ts +15 -2
  7. package/dist/core/concurrency/semaphore.js +36 -4
  8. package/dist/core/constants.d.ts +1 -0
  9. package/dist/core/constants.js +3 -0
  10. package/dist/core/monitor/request-tracker.d.ts +6 -0
  11. package/dist/core/monitor/request-tracker.js +15 -0
  12. package/dist/core/types.d.ts +1 -1
  13. package/dist/db/providers.d.ts +16 -3
  14. package/dist/db/providers.js +27 -12
  15. package/dist/index.js +2 -0
  16. package/dist/proxy/handler/iteration-setup.js +7 -3
  17. package/dist/proxy/orchestration/orchestrator.d.ts +1 -1
  18. package/dist/proxy/orchestration/orchestrator.js +37 -12
  19. package/dist/proxy/orchestration/resilience.d.ts +3 -1
  20. package/dist/proxy/orchestration/resilience.js +17 -2
  21. package/dist/proxy/orchestration/scope.d.ts +1 -1
  22. package/dist/proxy/orchestration/scope.js +2 -2
  23. package/dist/proxy/proxy-core.d.ts +0 -11
  24. package/dist/proxy/proxy-core.js +2 -63
  25. package/dist/proxy/transport/http.d.ts +11 -13
  26. package/dist/proxy/transport/http.js +40 -30
  27. package/dist/proxy/transport/shared.d.ts +23 -0
  28. package/dist/proxy/transport/shared.js +58 -0
  29. package/dist/proxy/transport/stream.d.ts +56 -3
  30. package/dist/proxy/transport/stream.js +128 -49
  31. package/dist/proxy/transport/transport-fn.d.ts +2 -1
  32. package/dist/proxy/transport/transport-fn.js +3 -3
  33. package/frontend-dist/assets/AuthLayout-DgrURhUg.js +1 -0
  34. package/frontend-dist/assets/Card-C_9nHSrd.js +1 -0
  35. package/frontend-dist/assets/CardContent-i69eqvwn.js +1 -0
  36. package/frontend-dist/assets/CardTitle-BG8tDKg7.js +1 -0
  37. package/frontend-dist/assets/CascadingModelSelect-DSBog9EX.js +1 -0
  38. package/frontend-dist/assets/Checkbox-DIo_zvR8.js +1 -0
  39. package/frontend-dist/assets/CollapsibleContent-vZSsPw6t.js +1 -0
  40. package/frontend-dist/assets/CollapsibleTrigger-BBdH_cCm.js +1 -0
  41. package/frontend-dist/assets/ConcurrencyControl-hsDQfbVn.js +1 -0
  42. package/frontend-dist/assets/Dashboard-BndYfuJw.js +3 -0
  43. package/frontend-dist/assets/{Input-DMDfXTXB.js → Input-CCoAeW8B.js} +1 -1
  44. package/frontend-dist/assets/Label-C7Hesw43.js +1 -0
  45. package/frontend-dist/assets/Login-C8ZPAsdy.js +1 -0
  46. package/frontend-dist/assets/Logs-BS0OalCQ.js +1 -0
  47. package/frontend-dist/assets/ModelMappings-Ck6Oddbz.js +1 -0
  48. package/frontend-dist/assets/Monitor-D-yoFX7E.js +1 -0
  49. package/frontend-dist/assets/Providers-CXFMJpnM.js +1 -0
  50. package/frontend-dist/assets/ProxyEnhancement-BY66st8t.js +1 -0
  51. package/frontend-dist/assets/QuickSetup-GgmYoihs.js +1 -0
  52. package/frontend-dist/assets/RetryRules-C_dY-UdI.js +1 -0
  53. package/frontend-dist/assets/RouterKeys-l9RxPss_.js +1 -0
  54. package/frontend-dist/assets/{RovingFocusItem-5H5eE6G2.js → RovingFocusItem-8-Pjjqen.js} +1 -1
  55. package/frontend-dist/assets/Schedules-B0TVDH59.js +1 -0
  56. package/frontend-dist/assets/Separator-D6RzArbH.js +1 -0
  57. package/frontend-dist/assets/Settings-BkSAw8ff.js +6 -0
  58. package/frontend-dist/assets/Setup-38qAFwc9.js +1 -0
  59. package/frontend-dist/assets/Skeleton-BpwFXs9Z.js +1 -0
  60. package/frontend-dist/assets/Switch-BVug67mH.js +1 -0
  61. package/frontend-dist/assets/TableHeader-CARoN-zc.js +1 -0
  62. package/frontend-dist/assets/TabsTrigger-t8wD-SZ9.js +1 -0
  63. package/frontend-dist/assets/UnifiedRequestDialog-h4e7P7Id.js +3 -0
  64. package/frontend-dist/assets/{VisuallyHiddenInput-DrNFhnVL.js → VisuallyHiddenInput-CzjlCU82.js} +1 -1
  65. package/frontend-dist/assets/arrow-down-P_02O0zN.js +1 -0
  66. package/frontend-dist/assets/badge-CxTrJsDs.js +1 -0
  67. package/frontend-dist/assets/{button-BBiWml8B.js → button-CEjoKdKq.js} +2 -2
  68. package/frontend-dist/assets/chevron-right-Bvw4z2Yf.js +1 -0
  69. package/frontend-dist/assets/dialog-DPvcfIzg.js +1 -0
  70. package/frontend-dist/assets/{image-zYdpUIEA.js → image-DOH8COxk.js} +1 -1
  71. package/frontend-dist/assets/index-DE_N-dT0.js +58 -0
  72. package/frontend-dist/assets/{index-DyQ39g4W.css → index-DGJSS9jI.css} +1 -1
  73. package/frontend-dist/assets/model-patches-BVZz8hmE.js +1 -0
  74. package/frontend-dist/assets/{pencil-C3-MFg-d.js → pencil-BERHubJ1.js} +1 -1
  75. package/frontend-dist/assets/plus-B9mBymA8.js +1 -0
  76. package/frontend-dist/assets/quickSetup-jgJgPUcH.js +1 -0
  77. package/frontend-dist/assets/quickSetup-qTjp3Z6J.js +1 -0
  78. package/frontend-dist/assets/search-uTHGe-LF.js +1 -0
  79. package/frontend-dist/assets/{sparkles-B5RWZZuf.js → sparkles-gPb_SItQ.js} +1 -1
  80. package/frontend-dist/assets/transform-domain-C42QlTbk.js +1 -0
  81. package/frontend-dist/assets/{trash-2-Dn3T5-Z1.js → trash-2-m7vH2ViZ.js} +1 -1
  82. package/frontend-dist/assets/{useClipboard-Bx3CrPal.js → useClipboard-Bzk0ItE3.js} +1 -1
  83. package/frontend-dist/assets/useLogRetention-I_TUncV5.js +1 -0
  84. package/frontend-dist/assets/{useProviderGroups-Og5FpCPe.js → useProviderGroups-bpCq-vsG.js} +1 -1
  85. package/frontend-dist/index.html +3 -3
  86. package/package.json +1 -1
  87. package/frontend-dist/assets/AuthLayout-jELzICkx.js +0 -1
  88. package/frontend-dist/assets/Card-uC_v0CEa.js +0 -1
  89. package/frontend-dist/assets/CardContent-CP3OiCj4.js +0 -1
  90. package/frontend-dist/assets/CardTitle-DGxuW5DZ.js +0 -1
  91. package/frontend-dist/assets/CascadingModelSelect-Dzk7rxIN.js +0 -1
  92. package/frontend-dist/assets/Checkbox-C1aVqGdC.js +0 -1
  93. package/frontend-dist/assets/CollapsibleContent-pBG4UkLo.js +0 -1
  94. package/frontend-dist/assets/CollapsibleTrigger-D5TkgXmz.js +0 -1
  95. package/frontend-dist/assets/ConcurrencyControl-5GweS-rY.js +0 -1
  96. package/frontend-dist/assets/Dashboard-wjd3d3qk.js +0 -3
  97. package/frontend-dist/assets/Label-BQXea0mo.js +0 -1
  98. package/frontend-dist/assets/Login-DNpCjxrY.js +0 -1
  99. package/frontend-dist/assets/Logs-BuL2Z0sF.js +0 -1
  100. package/frontend-dist/assets/ModelMappings-DeRhu-2N.js +0 -1
  101. package/frontend-dist/assets/Monitor-V30dnACo.js +0 -1
  102. package/frontend-dist/assets/Providers-BkkQhSTb.js +0 -1
  103. package/frontend-dist/assets/ProxyEnhancement-DjgebwfU.js +0 -1
  104. package/frontend-dist/assets/QuickSetup-BTVjEiU7.js +0 -1
  105. package/frontend-dist/assets/RetryRules-DFBHBG-B.js +0 -1
  106. package/frontend-dist/assets/RouterKeys-DZADvMfh.js +0 -1
  107. package/frontend-dist/assets/Schedules-DUubZ2uN.js +0 -1
  108. package/frontend-dist/assets/Separator-BqIs_Dy3.js +0 -1
  109. package/frontend-dist/assets/Settings-CO59WLOZ.js +0 -6
  110. package/frontend-dist/assets/Setup-Br7JZKNp.js +0 -1
  111. package/frontend-dist/assets/Skeleton-gVyjaP-y.js +0 -1
  112. package/frontend-dist/assets/Switch-D1ER0j6H.js +0 -1
  113. package/frontend-dist/assets/TableHeader-MqyrNSsx.js +0 -1
  114. package/frontend-dist/assets/TabsTrigger-DdjWJbUq.js +0 -1
  115. package/frontend-dist/assets/UnifiedRequestDialog-DRthlI6j.js +0 -3
  116. package/frontend-dist/assets/arrow-down-BFgGYafs.js +0 -1
  117. package/frontend-dist/assets/badge-Db4OYMEf.js +0 -1
  118. package/frontend-dist/assets/chevron-right-DYwStkJr.js +0 -1
  119. package/frontend-dist/assets/dialog-DRYeWncC.js +0 -1
  120. package/frontend-dist/assets/index-DTujoAWx.js +0 -58
  121. package/frontend-dist/assets/model-patches-DIy-rFuq.js +0 -1
  122. package/frontend-dist/assets/plus-xmIDnujf.js +0 -1
  123. package/frontend-dist/assets/quickSetup-CqxQRMCR.js +0 -1
  124. package/frontend-dist/assets/quickSetup-DplqYrvf.js +0 -1
  125. package/frontend-dist/assets/search-BxNrTsG8.js +0 -1
  126. package/frontend-dist/assets/transform-domain-KBixlLXR.js +0 -1
  127. package/frontend-dist/assets/useLogRetention-CdccNhYN.js +0 -1
@@ -1,11 +1,14 @@
1
1
  import { PassThrough } from "stream";
2
2
  import { UPSTREAM_SUCCESS, filterHeaders } from "../types.js";
3
- import { buildUpstreamUrl } from "../proxy-core.js";
4
- import { _transportInternals, buildRequestOptions, } from "./http.js";
3
+ import { buildUpstreamUrl, _transportInternals, buildRequestOptions, } from "./shared.js";
5
4
  const UPSTREAM_BAD_GATEWAY = 502;
6
5
  const BUFFER_SIZE_LIMIT = 4096;
7
6
  const END_REPLY_TIMEOUT_MS = 1000;
8
- class StreamProxy {
7
+ /**
8
+ * SSE 流式代理状态机。导出仅为单元测试,非公开 API。
9
+ * 负责 buffering/streaming 状态转换、idle 超时、上游资源销毁。
10
+ */
11
+ export class StreamProxy {
9
12
  statusCode;
10
13
  sentUpstreamHeaders;
11
14
  reply;
@@ -15,6 +18,8 @@ class StreamProxy {
15
18
  loopGuard;
16
19
  timeoutContext;
17
20
  onTimeoutAbort;
21
+ upstreamRes;
22
+ upstreamReq;
18
23
  state = "BUFFERING";
19
24
  resolved = false;
20
25
  resolveFn = null;
@@ -29,10 +34,10 @@ class StreamProxy {
29
34
  passThrough = new PassThrough();
30
35
  pipeEntry;
31
36
  formatTransform;
32
- // 流式阶段 SSE error 扫描缓冲(跨 chunk 边界匹配)
37
+ // 流式阶段 SSE error 扫描缓冲(跨 chunk 边界匹配)
33
38
  sseScanBuffer = "";
34
39
  static SSE_SCAN_MAX = 8 * 1024; // eslint-disable-line no-magic-numbers -- 8KB scan buffer
35
- constructor(statusCode, rawUpstreamHeaders, sentUpstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs, loopGuard, formatTransform, timeoutContext, onTimeoutAbort) {
40
+ constructor(statusCode, rawUpstreamHeaders, sentUpstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs, loopGuard, formatTransform, timeoutContext, onTimeoutAbort, upstreamRes, upstreamReq) {
36
41
  this.statusCode = statusCode;
37
42
  this.sentUpstreamHeaders = sentUpstreamHeaders;
38
43
  this.reply = reply;
@@ -42,6 +47,8 @@ class StreamProxy {
42
47
  this.loopGuard = loopGuard;
43
48
  this.timeoutContext = timeoutContext;
44
49
  this.onTimeoutAbort = onTimeoutAbort;
50
+ this.upstreamRes = upstreamRes;
51
+ this.upstreamReq = upstreamReq;
45
52
  this.formatTransform = formatTransform;
46
53
  this.sseHeaders = filterHeaders(rawUpstreamHeaders);
47
54
  this.sseHeaders["Content-Type"] = "text/event-stream";
@@ -88,8 +95,8 @@ class StreamProxy {
88
95
  result = { kind: "stream_abort", ...base, metrics: extra.metrics, timeoutContext: extra.timeoutContext, timeoutMs: extra.timeoutMs, abortReason: extra.abortReason };
89
96
  break;
90
97
  }
91
- // deferred 模式:先 resolve 让 handler 链路(日志写入等)在 microtask 中执行,
92
- // cleanup 由调用方在 setImmediatemacrotask)中处理。
98
+ // deferred 模式:先 resolve 让 handler 链路(日志写入等)在 microtask 中执行,
99
+ // cleanup 由调用方在 setImmediate(macrotask)中处理。
93
100
  if (deferred) {
94
101
  if (this.resolveFn) {
95
102
  this.resolveFn(result);
@@ -99,8 +106,8 @@ class StreamProxy {
99
106
  }
100
107
  }
101
108
  else {
102
- // stream_abort(非 deferred)且 headers 已发送时,立即 end reply 避免客户端挂起
103
- // idle_timeout 使用 deferred 模式,由 resetIdleTimer 的 setImmediate 负责 end reply
109
+ // stream_abort(非 deferred)且 headers 已发送时,立即 end reply 避免客户端挂起
110
+ // idle_timeout 使用 deferred 模式,由 resetIdleTimer 的 setImmediate 负责 end reply
104
111
  if (kind === "stream_abort" && this.headersSent) {
105
112
  try {
106
113
  this.reply.raw.end();
@@ -109,7 +116,10 @@ class StreamProxy {
109
116
  console.warn("[stream-proxy] reply.raw.end() failed, likely already destroyed");
110
117
  }
111
118
  }
112
- this.cleanup();
119
+ // cleanup 透传 cause:让上游 destroy(err) emit 'error' 事件链,与 CLAUDE.md
120
+ // 「destroy 必须 error 参数」规范一致。无 cause 时 destroy(undefined) 等价 destroy(),
121
+ // 不 emit 'error',自然结束路径(onEnd/stream_success)行为不变。
122
+ this.cleanup(extra.error);
113
123
  if (this.resolveFn) {
114
124
  this.resolveFn(result);
115
125
  }
@@ -118,10 +128,16 @@ class StreamProxy {
118
128
  }
119
129
  }
120
130
  }
121
- cleanup() {
131
+ cleanup(cause) {
122
132
  if (this.idleTimer)
123
133
  clearTimeout(this.idleTimer);
124
134
  this.idleTimer = null;
135
+ // 销毁上游资源,避免连接泄漏(幂等:destroyed 标志保护重复调用)。
136
+ // 传 cause 时 destroy(err) 会 emit 'error',触发 onUpstreamError(resolved 守卫保护下为 noop)。
137
+ if (this.upstreamRes && !this.upstreamRes.destroyed && typeof this.upstreamRes.destroy === "function")
138
+ this.upstreamRes.destroy(cause);
139
+ if (this.upstreamReq && !this.upstreamReq.destroyed && typeof this.upstreamReq.destroy === "function")
140
+ this.upstreamReq.destroy(cause);
125
141
  if (this.formatTransform && !this.formatTransform.destroyed)
126
142
  this.formatTransform.destroy();
127
143
  if (!this.passThrough.destroyed)
@@ -132,8 +148,8 @@ class StreamProxy {
132
148
  collectMetrics(isComplete) {
133
149
  if (!this.metricsTransform)
134
150
  return undefined;
135
- // 在读取 metrics 之前 flush SSE parser 缓冲区,确保 [DONE] / message_stop
136
- // 等末尾事件已被处理。否则 extractor.complete 可能为 false,导致 is_complete=0。
151
+ // 在读取 metrics 之前 flush SSE parser 缓冲区,确保 [DONE] / message_stop
152
+ // 等末尾事件已被处理。否则 extractor.complete 可能为 false,导致 is_complete=0。
137
153
  this.metricsTransform.flushParser();
138
154
  const result = this.metricsTransform.getExtractor().getMetrics();
139
155
  return isComplete ? result : { ...result, is_complete: 0 };
@@ -146,15 +162,15 @@ class StreamProxy {
146
162
  this.idleTimer = setTimeout(() => {
147
163
  if (this.resolved)
148
164
  return;
149
- // 在 terminal() 之前同步写入超时错误 SSE,确保 inject() 能正确收集响应体
165
+ // 在 terminal() 之前同步写入超时错误 SSE,确保 inject() 能正确收集响应体
150
166
  if (this.onTimeoutAbort) {
151
167
  try {
152
168
  this.onTimeoutAbort(this.timeoutMs);
153
169
  }
154
170
  catch { /* reply may be destroyed */ } // eslint-disable-line taste/no-silent-catch
155
171
  }
156
- // deferred 模式:先 resolve 让 handler 链路(日志写入等)在 microtask 中完成,
157
- // reply.raw.end() 延迟到 setImmediatemacrotask),保证 inject() 返回时日志已写入。
172
+ // deferred 模式:先 resolve 让 handler 链路(日志写入等)在 microtask 中完成,
173
+ // reply.raw.end() 延迟到 setImmediate(macrotask),保证 inject() 返回时日志已写入。
158
174
  this.terminal("stream_abort", { metrics: this.collectMetrics(false), timeoutContext: this.timeoutContext, timeoutMs: this.timeoutMs, abortReason: "idle_timeout" }, true);
159
175
  setImmediate(() => {
160
176
  if (this.headersSent) {
@@ -166,12 +182,13 @@ class StreamProxy {
166
182
  this.cleanup();
167
183
  });
168
184
  }, this.timeoutMs);
185
+ this.idleTimer.unref();
169
186
  }
170
187
  startStreaming() {
171
188
  if (this.headersSent)
172
189
  return;
173
190
  if (this.reply.raw.headersSent) {
174
- // headers 已由其他代码路径(如前一次 retry 的 StreamProxy)发送,
191
+ // headers 已由其他代码路径(如前一次 retry 的 StreamProxy)发送,
175
192
  // 仅更新状态机避免 BUFFERING 阶段的重复逻辑
176
193
  this.transition("STREAMING");
177
194
  this.headersSent = true;
@@ -183,7 +200,7 @@ class StreamProxy {
183
200
  this.reply.raw.writeHead(this.statusCode, this.sseHeaders);
184
201
  }
185
202
  catch {
186
- // 客户端在 state transition 和 writeHead 之间断连,可安全忽略
203
+ // 客户端在 state transition 和 writeHead 之间断连,可安全忽略
187
204
  this.terminal("stream_abort", { abortReason: "client_disconnect" });
188
205
  return;
189
206
  }
@@ -193,17 +210,26 @@ class StreamProxy {
193
210
  if (this.formatTransform) {
194
211
  this.formatTransform.pipe(this.passThrough, { end: true });
195
212
  }
196
- // 手动转发而非 pipe,避免 Node.js 在 dest 上自动注册 close/finish handler
213
+ // 手动转发而非 pipe,避免 Node.js 在 dest 上自动注册 close/finish handler
197
214
  this.passThrough.on("data", (chunk) => {
198
215
  try {
199
216
  this.reply.raw.write(chunk);
200
217
  }
201
218
  catch { // eslint-disable-line taste/no-silent-catch
202
- // 客户端已断开,写已销毁的 socket 会抛出异常,可安全忽略
219
+ // 客户端已断开,写已销毁的 socket 会抛出异常,可安全忽略
203
220
  }
204
221
  });
205
- // 不在 passThrough end 事件中调用 reply.raw.end()
222
+ // 不在 passThrough end 事件中调用 reply.raw.end(),
206
223
  // 因为 onEnd() 统一管理响应结束时机,确保日志在 reply end 之前写入
224
+ // passThrough 异常若不处理会冒泡 uncaughtException;此处直接走 terminal 保证 Promise resolve,
225
+ // 避免 callStream 永挂(cleanup 的 upstreamRes.destroy() 无 error 时不 emit 'error',
226
+ // onUpstreamError 兜底不触发,是旧实现的隐藏 bug)。
227
+ this.passThrough.on("error", (err) => {
228
+ console.warn("[stream-proxy] passThrough error:", err.message);
229
+ if (!this.resolved) {
230
+ this.terminal("stream_abort", { abortReason: "pipe_error", error: err });
231
+ }
232
+ });
207
233
  for (const c of this.bufferChunks)
208
234
  this.pipeEntry.write(c);
209
235
  this.bufferChunks.length = 0;
@@ -212,10 +238,10 @@ class StreamProxy {
212
238
  if (this.closeHandlerRegistered)
213
239
  return;
214
240
  this.closeHandlerRegistered = true;
215
- // EPIPE 从底层 socket 的 WriteWrap.onWriteComplete emitreply.raw.on("error") 无法拦截。
241
+ // EPIPE 从底层 socket 的 WriteWrap.onWriteComplete emit,reply.raw.on("error") 无法拦截。
216
242
  // 必须直接在 socket 上注册 error handler 防止冒泡到 process uncaughtException。
217
- // Node.js HTTP server 内置的 socketOnError 只处理第一次 socket error
218
- // 如果第一次被其他错误消耗,后续 EPIPE 可能无 handler 导致进程崩溃。
243
+ // Node.js HTTP server 内置的 socketOnError 只处理第一次 socket error,
244
+ // 如果第一次被其他错误消耗,后续 EPIPE 可能无 handler 导致进程崩溃。
219
245
  const sock = this.reply.raw.socket;
220
246
  let sockErrorHandler;
221
247
  if (sock) {
@@ -246,7 +272,7 @@ class StreamProxy {
246
272
  if (this.state === "BUFFERING") {
247
273
  this.bufferChunks.push(chunk);
248
274
  this.totalBuffered += chunk.length;
249
- // 快速路径:检查大小限制(无需 concat
275
+ // 快速路径:检查大小限制(无需 concat)
250
276
  if (this.totalBuffered >= BUFFER_SIZE_LIMIT) {
251
277
  const buf = Buffer.concat(this.bufferChunks);
252
278
  const text = buf.toString("utf-8");
@@ -258,10 +284,10 @@ class StreamProxy {
258
284
  this.startStreaming();
259
285
  return;
260
286
  }
261
- // 快速路径:检查当前 chunk 是否包含 \n\n
287
+ // 快速路径:检查当前 chunk 是否包含 \n\n
262
288
  const chunkStr = chunk.toString("utf-8");
263
289
  const hasNewlinePair = chunkStr.includes("\n\n");
264
- // 跨 chunk 边界检测:上一个 chunk 以 \n 结尾 + 当前以 \n 开头
290
+ // 跨 chunk 边界检测:上一个 chunk 以 \n 结尾 + 当前以 \n 开头
265
291
  const crossBoundary = this.lastChunkEndedWithNewline && chunkStr.startsWith("\n");
266
292
  this.lastChunkEndedWithNewline = chunkStr.endsWith("\n");
267
293
  if (hasNewlinePair || crossBoundary) {
@@ -278,25 +304,25 @@ class StreamProxy {
278
304
  }
279
305
  return;
280
306
  }
281
- // STREAMING 阶段:扫描 SSE error event(处理跨 chunk 边界)
307
+ // STREAMING 阶段:扫描 SSE error event(处理跨 chunk 边界)
282
308
  if (this.state === "STREAMING" && this.checkEarlyError) {
283
309
  this.sseScanBuffer += chunk.toString("utf-8");
284
- // 保留最近 SSE_SCAN_MAX 字符,避免无限增长
310
+ // 保留最近 SSE_SCAN_MAX 字符,避免无限增长
285
311
  if (this.sseScanBuffer.length > StreamProxy.SSE_SCAN_MAX) {
286
312
  this.sseScanBuffer = this.sseScanBuffer.slice(-StreamProxy.SSE_SCAN_MAX);
287
313
  }
288
- // 快速启发式:只在扫描窗口出现 SSE error 标记时才执行正则匹配
314
+ // 快速启发式:只在扫描窗口出现 SSE error 标记时才执行正则匹配
289
315
  if (this.sseScanBuffer.includes("event: error") || this.sseScanBuffer.includes('"type":"error"')) {
290
316
  if (this.checkEarlyError(this.sseScanBuffer)) {
291
317
  this.terminal("stream_error", { body: this.sseScanBuffer });
292
- // headers 已发送:必须结束 reply 避免 client hang
318
+ // headers 已发送:必须结束 reply 避免 client hang
293
319
  if (this.headersSent) {
294
320
  setImmediate(() => {
295
321
  try {
296
322
  this.reply.raw.end();
297
323
  }
298
324
  catch { // eslint-disable-line taste/no-silent-catch
299
- // reply 可能已 destroyed,安全忽略
325
+ // reply 可能已 destroyed,安全忽略
300
326
  }
301
327
  });
302
328
  }
@@ -326,19 +352,19 @@ class StreamProxy {
326
352
  if (this.state === "STREAMING") {
327
353
  this.transition("COMPLETED");
328
354
  }
329
- // 通过 terminal 的 deferred 模式统一 resolve
330
- // 先 resolve Promise,让 handler 链路(日志写入等)在 microtask 中执行。
331
- // reply.raw.end() 延迟到 setImmediatemacrotask),确保 microtask 先完成。
332
- // light-my-request 监听 reply.raw 的 end 事件判定响应完成,
355
+ // 通过 terminal 的 deferred 模式统一 resolve:
356
+ // 先 resolve Promise,让 handler 链路(日志写入等)在 microtask 中执行。
357
+ // reply.raw.end() 延迟到 setImmediate(macrotask),确保 microtask 先完成。
358
+ // light-my-request 监听 reply.raw 的 end 事件判定响应完成,
333
359
  // 这保证了 inject() 返回时日志已经写入 DB。
334
360
  const metrics = this.collectMetrics(true);
335
361
  this.terminal("stream_success", { metrics }, true);
336
- // 延迟结束管道和响应,属于 reply 层面操作,不属于 StreamProxy 状态管理。
362
+ // 延迟结束管道和响应,属于 reply 层面操作,不属于 StreamProxy 状态管理。
337
363
  //
338
- // 当 formatTransform 存在时(如 OpenAI→Anthropic 转换),Transform 链的 flush
339
- // 通过 process.nextTick 异步传播。pipeEntry.end() 后不能同步调用 reply.raw.end()
340
- // 否则 formatTransform._flush() 中的 ensureTerminated()(发送 message_stop
341
- // 尚未执行,连接就被关闭,导致客户端收到 "stream ended before message_stop"。
364
+ // 当 formatTransform 存在时(如 OpenAI→Anthropic 转换),Transform 链的 flush
365
+ // 通过 process.nextTick 异步传播。pipeEntry.end() 后不能同步调用 reply.raw.end(),
366
+ // 否则 formatTransform._flush() 中的 ensureTerminated()(发送 message_stop)
367
+ // 尚未执行,连接就被关闭,导致客户端收到 "stream ended before message_stop"。
342
368
  setImmediate(() => {
343
369
  const endReply = () => {
344
370
  if (this.headersSent) {
@@ -346,16 +372,16 @@ class StreamProxy {
346
372
  this.reply.raw.end();
347
373
  }
348
374
  catch { // eslint-disable-line taste/no-silent-catch
349
- // reply 可能已 destroyed,安全忽略
375
+ // reply 可能已 destroyed,安全忽略
350
376
  }
351
377
  }
352
378
  this.cleanup();
353
379
  };
354
380
  this.pipeEntry.end();
355
381
  if (this.formatTransform) {
356
- // 等 passThrough end 事件触发(整个 transform 链 flush 完成后),再关闭 reply
382
+ // 等 passThrough end 事件触发(整个 transform 链 flush 完成后),再关闭 reply
357
383
  this.passThrough.once("end", endReply);
358
- // 安全超时兜底,防止 end 事件未触发导致连接挂起
384
+ // 安全超时兜底,防止 end 事件未触发导致连接挂起
359
385
  setTimeout(endReply, END_REPLY_TIMEOUT_MS).unref();
360
386
  }
361
387
  else {
@@ -366,6 +392,10 @@ class StreamProxy {
366
392
  onUpstreamError(err) {
367
393
  if (this.resolved)
368
394
  return;
395
+ // 状态机一致:上游错误视为中止(resolved=true 不保证 state 已转终态)
396
+ if (this.state === "BUFFERING" || this.state === "STREAMING") {
397
+ this.transition("ABORTED");
398
+ }
369
399
  this.resolved = true;
370
400
  this.cleanup();
371
401
  const result = { kind: "throw", error: err, headersSent: this.headersSent };
@@ -378,7 +408,7 @@ class StreamProxy {
378
408
  }
379
409
  }
380
410
  // ---------- callStream ----------
381
- export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutMs, upstreamPath, buildHeaders, metricsTransform, checkEarlyError, compatResolve, loopGuard, formatTransform, timeoutContext, onTimeoutAbort, agent) {
411
+ export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutMs, upstreamPath, buildHeaders, metricsTransform, checkEarlyError, compatResolve, loopGuard, formatTransform, timeoutContext, onTimeoutAbort, agent, opts) {
382
412
  return new Promise((resolve) => {
383
413
  const effectiveResolve = compatResolve ?? resolve;
384
414
  const url = new URL(buildUpstreamUrl(backend.base_url, upstreamPath));
@@ -386,13 +416,59 @@ export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutM
386
416
  const upstreamHeaders = buildHeaders(clientHeaders, apiKey, Buffer.byteLength(payload));
387
417
  const options = buildRequestOptions(url, upstreamHeaders);
388
418
  const upstreamReq = _transportInternals.createUpstreamRequest(url, options, agent);
419
+ // 响应头前无活动超时(拿到 upstreamRes 后改由 StreamProxy.idleTimer 接管)。
420
+ // destroy(error) 触发 'error' 事件 → effectiveResolve({kind:'throw'})。
421
+ const connectTimeoutMs = opts?.connectTimeoutMs;
422
+ const hasConnectTimeout = connectTimeoutMs !== undefined && Number.isFinite(connectTimeoutMs) && connectTimeoutMs > 0;
423
+ if (hasConnectTimeout) {
424
+ upstreamReq.setTimeout(connectTimeoutMs);
425
+ upstreamReq.on("timeout", () => upstreamReq.destroy(new Error("upstream inactivity timeout (pre-response)")));
426
+ }
427
+ // 客户端断连:abort 信号穿透到上游 socket,立即切断连接。
428
+ // resolveOnce 在 Promise settle 时移除 abort listener,避免重试累积残留
429
+ // (listener 受 ITERATION_CAP 上界约束,非进程级泄漏,此处显式清理保持干净)。
430
+ const clientSignal = opts?.signal;
431
+ const onClientAbort = clientSignal
432
+ ? () => upstreamReq.destroy(new Error("client aborted"))
433
+ : undefined;
434
+ const resolveOnce = (r) => {
435
+ if (onClientAbort && clientSignal && !clientSignal.aborted) {
436
+ clientSignal.removeEventListener("abort", onClientAbort);
437
+ }
438
+ effectiveResolve(r);
439
+ };
440
+ if (onClientAbort && clientSignal) {
441
+ if (clientSignal.aborted) {
442
+ onClientAbort();
443
+ }
444
+ else {
445
+ clientSignal.addEventListener("abort", onClientAbort, { once: true });
446
+ }
447
+ }
389
448
  upstreamReq.on("response", (upstreamRes) => {
449
+ // 响应头已到达:清除响应头前 socket 超时,避免与 StreamProxy.idleTimer 竞争
450
+ if (hasConnectTimeout)
451
+ upstreamReq.setTimeout(0);
390
452
  const statusCode = upstreamRes.statusCode || UPSTREAM_BAD_GATEWAY;
391
453
  if (statusCode !== UPSTREAM_SUCCESS) {
392
454
  const chunks = [];
455
+ // 非 2xx body 兜底超时:connect timeout 已在响应头到达时清零(L474),
456
+ // 若上游发半截 body 后静默挂住(不 FIN 不 RST,CDN/反代常见故障),
457
+ // end/error 均不触发 → resolveOnce 永不调用 → slot 泄漏。
458
+ // 复用 connectTimeoutMs 作为 body 读取上限,与 200 路径 idleTimer 同源。
459
+ const bodyGuard = hasConnectTimeout
460
+ ? setTimeout(() => upstreamRes.destroy(new Error("upstream error-body timeout")), connectTimeoutMs)
461
+ : null;
462
+ if (bodyGuard)
463
+ bodyGuard.unref();
464
+ const finishBody = (r) => {
465
+ if (bodyGuard)
466
+ clearTimeout(bodyGuard);
467
+ resolveOnce(r);
468
+ };
393
469
  upstreamRes.on("data", (chunk) => chunks.push(chunk));
394
470
  upstreamRes.on("end", () => {
395
- effectiveResolve({
471
+ finishBody({
396
472
  kind: "stream_error",
397
473
  statusCode,
398
474
  body: Buffer.concat(chunks).toString("utf-8"),
@@ -400,10 +476,13 @@ export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutM
400
476
  sentHeaders: upstreamHeaders,
401
477
  });
402
478
  });
479
+ // 非 2xx body 传输中连接中断:无 listener 会 uncaughtException + Promise 永挂(slot 泄漏)。
480
+ // 与 200 分支 proxy.onUpstreamError 对称,与 callNonStream(http.ts) 同路径一致。
481
+ upstreamRes.on("error", (err) => finishBody({ kind: "throw", error: err }));
403
482
  return;
404
483
  }
405
- const proxy = new StreamProxy(statusCode, upstreamRes.headers, upstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs, loopGuard, formatTransform, timeoutContext, onTimeoutAbort);
406
- proxy.bindResolve(effectiveResolve);
484
+ const proxy = new StreamProxy(statusCode, upstreamRes.headers, upstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs, loopGuard, formatTransform, timeoutContext, onTimeoutAbort, upstreamRes, upstreamReq);
485
+ proxy.bindResolve(resolveOnce);
407
486
  proxy.registerCloseHandler();
408
487
  // 无 early error checker 时直接开始流式传输
409
488
  if (!checkEarlyError)
@@ -413,7 +492,7 @@ export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutM
413
492
  upstreamRes.on("end", () => proxy.onEnd());
414
493
  upstreamRes.on("error", (err) => proxy.onUpstreamError(err));
415
494
  });
416
- upstreamReq.on("error", (error) => effectiveResolve({ kind: "throw", error }));
495
+ upstreamReq.on("error", (error) => resolveOnce({ kind: "throw", error }));
417
496
  upstreamReq.write(payload);
418
497
  upstreamReq.end();
419
498
  });
@@ -18,6 +18,7 @@ export interface TransportFnParams {
18
18
  logId: string;
19
19
  effectiveModel: string;
20
20
  streamTimeoutMs: number;
21
+ nonStreamTimeoutMs?: number;
21
22
  tracker?: RequestTracker;
22
23
  matcher?: RetryRuleMatcher;
23
24
  request: FastifyRequest;
@@ -32,4 +33,4 @@ export interface TransportFnParams {
32
33
  proxyAgentFactory?: ProxyAgentFactory;
33
34
  resolvedBaseUrl: string;
34
35
  }
35
- export declare function buildTransportFn(p: TransportFnParams): (target: Target) => Promise<TransportResult>;
36
+ export declare function buildTransportFn(p: TransportFnParams): (target: Target, signal?: AbortSignal) => Promise<TransportResult>;
@@ -45,7 +45,7 @@ export function buildTransportFn(p) {
45
45
  : undefined;
46
46
  // _target 未使用 — resilience 层始终传入当前 resolved target;
47
47
  // 跨 target failover 由外层 executeFailoverLoop 的 ProviderSwitchNeeded 处理
48
- return async (_target) => {
48
+ return async (_target, signal) => {
49
49
  if (p.isStream) {
50
50
  let streamLoopGuard;
51
51
  if (p.streamLoopEnabled) {
@@ -84,14 +84,14 @@ export function buildTransportFn(p) {
84
84
  catch { /* reply may be destroyed */ } // eslint-disable-line taste/no-silent-catch
85
85
  }
86
86
  : undefined;
87
- const streamResult = await callStream(effectiveBackend, p.apiKey, p.body, p.cliHdrs, p.reply, p.streamTimeoutMs, p.upstreamPath, buildHeaders, metricsTransform, checkEarlyError, undefined, streamLoopGuard, p.formatTransform, p.timeoutContext, onTimeoutAbort, agent);
87
+ const streamResult = await callStream(effectiveBackend, p.apiKey, p.body, p.cliHdrs, p.reply, p.streamTimeoutMs, p.upstreamPath, buildHeaders, metricsTransform, checkEarlyError, undefined, streamLoopGuard, p.formatTransform, p.timeoutContext, onTimeoutAbort, agent, { signal, connectTimeoutMs: p.streamTimeoutMs });
88
88
  const m = (streamResult.kind === "stream_success" || streamResult.kind === "stream_abort")
89
89
  ? streamResult.metrics : undefined;
90
90
  if (m)
91
91
  p.tracker?.update(p.logId, { streamMetrics: toStreamMetrics(m) });
92
92
  return streamResult;
93
93
  }
94
- let result = await callNonStream(effectiveBackend, p.apiKey, p.body, p.cliHdrs, p.upstreamPath, buildHeaders, agent);
94
+ let result = await callNonStream(effectiveBackend, p.apiKey, p.body, p.cliHdrs, p.upstreamPath, buildHeaders, agent, { signal, timeoutMs: p.nonStreamTimeoutMs });
95
95
  if (result.kind === "success") {
96
96
  const mr = MetricsExtractor.fromNonStreamResponse(p.apiType, result.body);
97
97
  if (mr)
@@ -0,0 +1 @@
1
+ import{$t as e,Jt as t,Mt as n,St as r,et as i,n as a,ot as o,rt as s,st as c,tt as l,yt as u}from"./button-CEjoKdKq.js";import{dt as d,ft as f,r as p}from"./index-DE_N-dT0.js";import{t as m}from"./Card-C_9nHSrd.js";import{t as h}from"./CardContent-i69eqvwn.js";var g={class:`min-h-screen flex items-center justify-center bg-background relative`},_={class:`text-center mb-6`},v={class:`text-sm text-muted-foreground mt-1`},y=c({__name:`AuthLayout`,props:{subtitle:{}},setup(c){let{isDark:y,toggleTheme:b}=p();return(p,x)=>(u(),s(`div`,g,[o(t(a),{variant:`ghost`,size:`icon`,class:`absolute top-4 right-4 text-muted-foreground hover:text-foreground`,onClick:t(b)},{default:n(()=>[t(y)?(u(),l(t(d),{key:1,class:`w-4 h-4`})):(u(),l(t(f),{key:0,class:`w-4 h-4`}))]),_:1},8,[`onClick`]),o(t(m),{class:`w-full max-w-sm shadow-lg`},{default:n(()=>[o(t(h),{class:`pt-6`},{default:n(()=>[i(`div`,_,[x[0]||=i(`div`,{class:`w-12 h-12 bg-primary rounded-lg mx-auto mb-3 flex items-center justify-center`},[i(`svg`,{class:`w-7 h-7 text-white`,fill:`none`,stroke:`currentColor`,viewBox:`0 0 24 24`},[i(`path`,{"stroke-linecap":`round`,"stroke-linejoin":`round`,"stroke-width":`2`,d:`M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z`})])],-1),x[1]||=i(`h1`,{class:`text-xl font-semibold text-foreground`},` LLM Simple Router `,-1),i(`p`,v,e(c.subtitle),1)]),r(p.$slots,`default`)]),_:3})]),_:3})]))}});export{y as t};
@@ -0,0 +1 @@
1
+ import{Jt as e,St as t,Xt as n,r,rt as i,st as a,yt as o}from"./button-CEjoKdKq.js";var s=[`data-size`,`data-flush`],c=a({__name:`Card`,props:{class:{type:[Boolean,null,String,Object,Array]},size:{default:`default`},flush:{type:Boolean,default:!1}},setup(a){let c=a;return(l,u)=>(o(),i(`div`,{"data-slot":`card`,"data-size":a.size,"data-flush":a.flush||void 0,class:n(e(r)(`border-border bg-card text-card-foreground rounded-lg border text-sm group/card flex flex-col`,a.flush?`overflow-hidden`:`overflow-hidden gap-4 py-4 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg`,`data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0`,c.class))},[t(l.$slots,`default`)],10,s))}});export{c as t};
@@ -0,0 +1 @@
1
+ import{Jt as e,St as t,Xt as n,r,rt as i,st as a,yt as o}from"./button-CEjoKdKq.js";var s=a({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(a){let s=a;return(a,c)=>(o(),i(`div`,{"data-slot":`card-content`,class:n(e(r)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[t(a.$slots,`default`)],2))}});export{s as t};
@@ -0,0 +1 @@
1
+ import{Jt as e,St as t,Xt as n,r,rt as i,st as a,yt as o}from"./button-CEjoKdKq.js";var s=a({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(a){let s=a;return(a,c)=>(o(),i(`div`,{"data-slot":`card-header`,class:n(e(r)(`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))},[t(a.$slots,`default`)],2))}}),c=a({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(a){let s=a;return(a,c)=>(o(),i(`div`,{"data-slot":`card-title`,class:n(e(r)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[t(a.$slots,`default`)],2))}});export{s as n,c as t};
@@ -0,0 +1 @@
1
+ import{$ as e,$t as t,Bt as n,Jt as r,Mt as i,Q as a,Qt as o,W as s,Xt as c,Z as l,et as u,ht as d,nt as f,ot as p,rt as m,st as h,tt as g,xt as _,yt as v}from"./button-CEjoKdKq.js";import{t as y}from"./chevron-right-Bvw4z2Yf.js";import{b,vt as x,x as S,xt as C,y as w}from"./index-DE_N-dT0.js";var T=[`onMouseenter`],E={class:`truncate max-w-40`},D={key:0,class:`ml-1 text-[10px] px-1 py-px rounded bg-emerald-500/15 text-emerald-400 shrink-0`},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=150,N=2,P=h({__name:`CascadingSelect`,props:{groups:{},modelValue:{},placeholder:{default:``},compact:{type:Boolean,default:!1},dashed:{type:Boolean,default:!1}},emits:[`update:modelValue`],setup(h,{emit:C}){let{t:P}=s(),F=h,I=e(()=>F.placeholder||P(`common.selectPlaceholder`)),L=C,R=n(!1),z=n(null),B=n(new Map),V=n({top:0,left:0}),H=null;function U(e,t){t?B.value.set(e,t):B.value.delete(e)}function W(e){H&&=(clearTimeout(H),null),z.value=e,d(()=>J(e))}function G(){H=setTimeout(()=>{z.value=null},M)}function K(){H&&=(clearTimeout(H),null)}function q(){z.value=null}function J(e){let t=B.value.get(e);if(!t)return;let n=t.getBoundingClientRect();V.value={top:n.top,left:n.right+N}}let Y=e(()=>{if(!F.modelValue)return``;let e=F.groups.find(e=>e.key===F.modelValue.groupKey);if(!e)return``;let t=e.options.find(e=>e.value===F.modelValue.value);return t?`${e.label} / ${t.label}`:``});function X(e,t){L(`update:modelValue`,{groupKey:e,value:t}),R.value=!1}function Z(e){R.value=e,e||(z.value=null)}return(e,n)=>(v(),g(r(S),{open:R.value,"onUpdate:open":Z},{default:i(()=>[p(r(w),{"as-child":``},{default:i(()=>[u(`div`,{class:c([`flex w-full items-center justify-between rounded-md bg-background ring-offset-background cursor-pointer hover:bg-accent hover:text-accent-foreground`,[h.compact?`h-7 text-xs px-2 py-1 rounded-md`:`h-10 text-sm px-3 py-2 border border-border rounded-md`,!h.compact&&h.dashed?`border-dashed border-primary/20`:``,{"ring-2 ring-ring ring-offset-2":R.value}]])},[u(`span`,{class:c([`truncate`,h.modelValue?`text-foreground`:`text-muted-foreground`])},t(Y.value||I.value),3),p(r(x),{class:`h-4 w-4 shrink-0 opacity-50`})],2)]),_:1}),p(r(b),{align:`start`,"side-offset":4,class:`z-[200] w-auto min-w-56 max-h-[80vh] overflow-y-auto p-1`},{default:i(()=>[(v(!0),m(l,null,_(h.groups,e=>(v(),m(`div`,{key:e.key,ref_for:!0,ref:t=>U(e.key,t),class:c([`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":z.value===e.key}]),onMouseenter:t=>W(e.key),onMouseleave:G},[u(`span`,E,t(e.label),1),e.badge?(v(),m(`span`,D,t(e.badge),1)):f(``,!0),p(r(y),{class:`ml-1 h-4 w-4 shrink-0 opacity-50`})],42,T))),128)),(v(),g(a,{to:`body`},[z.value&&h.groups.find(e=>e.key===z.value)?.options.length?(v(),m(`div`,{key:0,class:`fixed z-[201] min-w-48 max-h-[80vh] overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md`,style:o({top:`${V.value.top}px`,left:`${V.value.left}px`}),onMouseenter:K,onMouseleave:q},[(v(!0),m(l,null,_(h.groups.find(e=>e.key===z.value)?.options??[],e=>(v(),m(`div`,{key:e.value,class:c([`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":h.modelValue?.groupKey===z.value&&h.modelValue?.value===e.value}]),onClick:t=>X(z.value,e.value)},[u(`span`,k,t(e.label),1),e.tag?(v(),m(`span`,A,t(e.tag),1)):f(``,!0)],10,O))),128))],36)):f(``,!0)])),h.groups.length===0?(v(),m(`div`,j,t(r(P)(`common.noOptions`)),1)):f(``,!0)]),_:1})]),_:1},8,[`open`]))}}),F=h({__name:`CascadingModelSelect`,props:{providers:{},modelValue:{},placeholder:{default:``},compact:{type:Boolean,default:!1},dashed:{type:Boolean,default:!1}},emits:[`update:modelValue`],setup(t,{emit:n}){let{t:r}=s(),i=t,a=e(()=>i.placeholder||r(`mappings.selectProviderModel`)),o=n,c=e(()=>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:C(e.contextWindow)}))}))),l=e(()=>i.modelValue?{groupKey:i.modelValue.provider_id,value:i.modelValue.model}:void 0);function u(e){o(`update:modelValue`,{provider_id:e.groupKey,model:e.value})}return(e,n)=>(v(),g(P,{groups:c.value,"model-value":l.value,placeholder:a.value,compact:t.compact,dashed:t.dashed,"onUpdate:modelValue":u},null,8,[`groups`,`model-value`,`placeholder`,`compact`,`dashed`]))}});export{F as t};
@@ -0,0 +1 @@
1
+ import{$ as e,Jt as t,Mt as n,S as r,St as i,X as a,Y as o,Zt as s,h as c,i as l,lt as u,mt as d,nt as f,o as p,ot as m,r as h,st as g,tt as _,wt as v,yt as y}from"./button-CEjoKdKq.js";import{t as b}from"./VisuallyHiddenInput-CzjlCU82.js";import{t as x}from"./RovingFocusItem-8-Pjjqen.js";import{H as S,J as C,Q as w,W as T,X as E,yt as D,z as O}from"./index-DE_N-dT0.js";function k(e,t){return C(e)?!1:Array.isArray(e)?e.some(e=>w(e,t)):w(e,t)}var[A,j]=E(`CheckboxGroupRoot`);function M(e){return e===`indeterminate`}function N(e){return M(e)?`indeterminate`:e?`checked`:`unchecked`}var[P,F]=E(`CheckboxRoot`),I=g({inheritAttrs:!1,__name:`CheckboxRoot`,props:{defaultValue:{type:null,required:!1},modelValue:{type:null,required:!1,default:void 0},disabled:{type:Boolean,required:!1},value:{type:null,required:!1,default:`on`},id:{type:String,required:!1},trueValue:{type:null,required:!1,default:()=>!0},falseValue:{type:null,required:!1,default:()=>!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`},name:{type:String,required:!1},required:{type:Boolean,required:!1}},emits:[`update:modelValue`],setup(r,{emit:s}){let u=r,m=s,{forwardRef:h,currentElement:g}=p(),S=A(null),E=c(u,`modelValue`,m,{defaultValue:u.defaultValue??u.falseValue,passive:u.modelValue===void 0}),D=e(()=>S?.disabled.value||u.disabled),O=e(()=>w(E.value,u.trueValue)),j=e(()=>C(S?.modelValue.value)?E.value===`indeterminate`?`indeterminate`:O.value:k(S.modelValue.value,u.value));function P(){if(C(S?.modelValue.value))E.value===`indeterminate`?E.value=u.trueValue:E.value=O.value?u.falseValue:u.trueValue;else{let e=[...S.modelValue.value||[]];if(k(e,u.value)){let t=e.findIndex(e=>w(e,u.value));e.splice(t,1)}else e.push(u.value);S.modelValue.value=e}}let I=T(g),L=e(()=>u.id&&g.value?document.querySelector(`[for="${u.id}"]`)?.innerText:void 0);return F({disabled:D,state:j}),(e,r)=>(y(),_(v(t(S)?.rovingFocus.value?t(x):t(l)),d(e.$attrs,{id:e.id,ref:t(h),role:`checkbox`,"as-child":e.asChild,as:e.as,type:e.as===`button`?`button`:void 0,"aria-checked":t(M)(j.value)?`mixed`:j.value,"aria-required":e.required,"aria-label":e.$attrs[`aria-label`]||L.value,"data-state":t(N)(j.value),"data-disabled":D.value?``:void 0,disabled:D.value,focusable:t(S)?.rovingFocus.value?!D.value:void 0,onKeydown:o(a(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:n(()=>[i(e.$slots,`default`,{modelValue:t(E),state:j.value}),t(I)&&e.name&&!t(S)?(y(),_(t(b),{key:0,type:`checkbox`,checked:!!j.value,name:e.name,value:e.value,disabled:D.value,required:e.required},null,8,[`checked`,`name`,`value`,`disabled`,`required`])):f(`v-if`,!0)]),_:3},16,[`id`,`as-child`,`as`,`type`,`aria-checked`,`aria-required`,`aria-label`,`data-state`,`data-disabled`,`disabled`,`focusable`,`onKeydown`]))}}),L=g({__name:`CheckboxIndicator`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`span`}},setup(e){let{forwardRef:r}=p(),a=P();return(e,o)=>(y(),_(t(O),{present:e.forceMount||t(M)(t(a).state.value)||t(a).state.value===!0},{default:n(()=>[m(t(l),d({ref:t(r),"data-state":t(N)(t(a).state.value),"data-disabled":t(a).disabled.value?``:void 0,style:{pointerEvents:`none`},"as-child":e.asChild,as:e.as},e.$attrs),{default:n(()=>[i(e.$slots,`default`)]),_:3},16,[`data-state`,`data-disabled`,`as-child`,`as`])]),_:3},8,[`present`]))}}),R=g({__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:a}){let o=e,c=a,l=S(r(o,`class`),c);return(e,r)=>(y(),_(t(I),d({"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`,o.class)}),{default:n(r=>[m(t(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:n(()=>[i(e.$slots,`default`,s(u(r)),()=>[m(t(D))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};
@@ -0,0 +1 @@
1
+ import{$ as e,Bt as t,Gt as n,Jt as r,Mt as i,St as a,Zt as o,_t as s,f as c,h as l,ht as u,i as d,kt as f,lt as p,mt as m,nt as h,o as g,ot as _,st as v,tt as y,yt as b}from"./button-CEjoKdKq.js";import{H as x,V as S,X as C,z as w}from"./index-DE_N-dT0.js";var[T,E]=C(`CollapsibleRoot`),D=v({__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:o}){let s=e,c=l(s,`open`,o,{defaultValue:s.defaultOpen,passive:s.open===void 0}),{disabled:u,unmountOnHide:f}=n(s);return E({contentId:``,disabled:u,open:c,unmountOnHide:f,onOpenToggle:()=>{u.value||(c.value=!c.value)}}),t({open:c}),g(),(e,t)=>(b(),y(r(d),{as:e.as,"as-child":s.asChild,"data-state":r(c)?`open`:`closed`,"data-disabled":r(u)?``:void 0},{default:i(()=>[a(e.$slots,`default`,{open:r(c)})]),_:3},8,[`as`,`as-child`,`data-state`,`data-disabled`]))}}),O=v({inheritAttrs:!1,__name:`CollapsibleContent`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`contentFound`],setup(n,{emit:o}){let l=n,p=o,v=T();v.contentId||=S(void 0,`reka-collapsible-content`);let x=t(),{forwardRef:C,currentElement:E}=g(),D=t(0),O=t(0),k=e(()=>v.open.value),A=t(k.value),j=t();f(()=>[k.value,x.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=e(()=>A.value&&v.open.value);return s(()=>{requestAnimationFrame(()=>{A.value=!1})}),c(E,`beforematch`,e=>{requestAnimationFrame(()=>{v.onOpenToggle(),p(`contentFound`)})}),(e,t)=>(b(),y(r(w),{ref_key:`presentRef`,ref:x,present:e.forceMount||r(v).open.value,"force-mount":!0},{default:i(({present:t})=>[_(r(d),m(e.$attrs,{id:r(v).contentId,ref:r(C),"as-child":l.asChild,as:e.as,hidden:t?void 0:r(v).unmountOnHide.value?``:`until-found`,"data-state":M.value?void 0:r(v).open.value?`open`:`closed`,"data-disabled":r(v).disabled?.value?``:void 0,style:{"--reka-collapsible-content-height":`${O.value}px`,"--reka-collapsible-content-width":`${D.value}px`}}),{default:i(()=>[!r(v).unmountOnHide.value||t?a(e.$slots,`default`,{key:0}):h(`v-if`,!0)]),_:2},1040,[`id`,`as-child`,`as`,`hidden`,`data-state`,`data-disabled`,`style`])]),_:3},8,[`present`]))}}),k=v({__name:`Collapsible`,props:{defaultOpen:{type:Boolean},open:{type:Boolean},disabled:{type:Boolean},unmountOnHide:{type:Boolean},asChild:{type:Boolean},as:{}},emits:[`update:open`],setup(e,{emit:t}){let n=x(e,t);return(e,t)=>(b(),y(r(D),m({"data-slot":`collapsible`},r(n)),{default:i(t=>[a(e.$slots,`default`,o(p(t)))]),_:3},16))}}),A=v({__name:`CollapsibleContent`,props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{}},setup(e){let t=e;return(e,n)=>(b(),y(r(O),m({"data-slot":`collapsible-content`},t),{default:i(()=>[a(e.$slots,`default`)]),_:3},16))}});export{k as n,T as r,A as t};
@@ -0,0 +1 @@
1
+ import{Jt as e,Mt as t,St as n,i as r,mt as i,o as a,st as o,tt as s,yt as c}from"./button-CEjoKdKq.js";import{r as l}from"./CollapsibleContent-vZSsPw6t.js";var u=o({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`}},setup(i){let o=i;a();let u=l();return(i,a)=>(c(),s(e(r),{type:i.as===`button`?`button`:void 0,as:i.as,"as-child":o.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:t(()=>[n(i.$slots,`default`)]),_:3},8,[`type`,`as`,`as-child`,`aria-controls`,`aria-expanded`,`data-state`,`data-disabled`,`disabled`,`onClick`]))}}),d=o({__name:`CollapsibleTrigger`,props:{asChild:{type:Boolean},as:{}},setup(r){let a=r;return(r,o)=>(c(),s(e(u),i({"data-slot":`collapsible-trigger`},a),{default:t(()=>[n(r.$slots,`default`)]),_:3},16))}});export{d as t};
@@ -0,0 +1 @@
1
+ import{$ as e,$t as t,Bt as n,Jt as r,Mt as i,W as a,X as o,Xt as s,Y as c,Z as l,at as u,et as d,n as f,nt as p,ot as m,rt as h,st as g,tt as _,xt as v,yt as y,z as b}from"./button-CEjoKdKq.js";import{t as x}from"./plus-B9mBymA8.js";import{t as S}from"./trash-2-m7vH2ViZ.js";import{t as C}from"./Label-C7Hesw43.js";import{t as ee}from"./Switch-BVug67mH.js";import{_ as w,ct as te,g as T,h as E,m as D,ut as ne,v as O,vt as re}from"./index-DE_N-dT0.js";import{t as ie}from"./badge-CxTrJsDs.js";import{t as k}from"./Input-CCoAeW8B.js";import{t as A}from"./CascadingModelSelect-DSBog9EX.js";import{r as j}from"./transform-domain-C42QlTbk.js";var M=b(`arrow-right`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`m12 5 7 7-7 7`,key:`xquz4c`}]]),N=b(`grid-3x3`,[[`rect`,{width:`18`,height:`18`,x:`3`,y:`3`,rx:`2`,key:`afitv7`}],[`path`,{d:`M3 9h18`,key:`1pudct`}],[`path`,{d:`M3 15h18`,key:`5xshup`}],[`path`,{d:`M9 3v18`,key:`fh3hqa`}],[`path`,{d:`M15 3v18`,key:`14nvp0`}]]),ae=[`title`],oe={class:`flex items-center gap-1 min-w-0 flex-1 flex-wrap`},se={key:0,class:`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-mono bg-primary/10 border border-border text-primary whitespace-nowrap`},P={key:0,class:`text-[10px] opacity-60`},F={class:`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-mono bg-muted/30 border border-border text-muted-foreground whitespace-nowrap`},I={class:`text-[10px] font-semibold`},L={class:`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-mono border border-dashed border-border text-primary/60 whitespace-nowrap`},R={class:`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-mono border border-dashed border-border text-blue-500/60 whitespace-nowrap`},z={class:`text-[10px] text-muted-foreground/30 hidden sm:inline shrink-0 whitespace-nowrap`},B={key:1,class:`space-y-2`},V={class:`flex items-center gap-2`},H={class:`text-[10px] text-muted-foreground/60 shrink-0 w-10 uppercase tracking-wider font-medium`},U={key:1,class:`flex-1 flex items-center gap-1.5`},W={class:`pt-0.5`},G={class:`flex items-center gap-1 mb-1`},K={class:`text-[10px] font-medium text-muted-foreground/70 uppercase tracking-wider`},ce={class:`flex items-center gap-2`},le={class:`flex-1`},ue={key:0,class:`flex items-center gap-1 pl-7 py-0.5`},de={class:`text-[9px] text-orange-400/50`},fe={class:`pt-2 mt-1 border-t border-dashed border-primary/15`},pe={class:`flex items-center gap-1.5 mb-1`},me={class:`text-[10px] font-medium text-primary/60 uppercase tracking-wider`},he={class:`text-[9px] text-primary/40 hidden sm:inline`},ge={key:0,class:`flex items-center gap-2`},_e={class:`flex-1`},ve={class:`pt-2 mt-1 border-t border-dashed border-blue-500/15`},ye={class:`flex items-center gap-1.5 mb-1`},be={class:`text-[10px] font-medium text-blue-500/60 uppercase tracking-wider`},xe={key:0,class:`flex items-center gap-2`},Se={class:`text-xs shrink-0 w-5 text-center px-0.5 py-0.5 rounded bg-blue-500/6 text-blue-500/60`},Ce={class:`flex-1`},we={key:2,class:`mt-2 px-2.5 py-2 rounded border-[1px] border-orange-400/25 bg-orange-400/5 flex gap-2`},Te={class:`text-[10px] leading-relaxed space-y-0.5`},Ee={class:`text-orange-400 font-medium`},De={class:`text-muted-foreground/60`},Oe={class:`text-muted-foreground/40`},ke=6,q=g({__name:`MappingEntryEditor`,props:{entry:{},providerGroups:{},expanded:{type:Boolean},editableClientModel:{type:Boolean,default:!1}},emits:[`update:targets`,`update:clientModel`,`update:multimodalFallback`,`toggle-active`,`remove`,`expand`],setup(g,{emit:b}){let{t:C}=a(),w=g,T=b,E=n(!1),D=n(``);function O(){w.editableClientModel&&(D.value=w.entry.clientModel,E.value=!0)}function j(){let e=D.value.trim();e&&e!==w.entry.clientModel&&T(`update:clientModel`,e),E.value=!1}function q(){E.value=!1}function J(e){return e===`__new__`?null:w.providerGroups.find(t=>t.provider.id===e)?.provider.name??e.slice(0,ke)}function Y(){let e=w.providerGroups[0];T(`update:targets`,[...w.entry.targets,{backend_model:e?.models[0]?.name??``,provider_id:e?.provider.id??``}])}function Ae(e){w.entry.targets.length<=1||T(`update:targets`,w.entry.targets.filter((t,n)=>n!==e))}function je(e,t){let n=[...w.entry.targets];n[e]={...n[e],provider_id:t.provider_id,backend_model:t.model},T(`update:targets`,n)}function Me(){let e=w.providerGroups[0];T(`update:targets`,w.entry.targets.map((t,n)=>n===0?{...t,overflow_provider_id:e?.provider.id??``,overflow_model:e?.models[0]?.name??``}:t))}function X(e){T(`update:targets`,w.entry.targets.map((t,n)=>{if(n===0){if(e)return{...t,overflow_provider_id:e.provider_id,overflow_model:e.model};let{overflow_provider_id:n,overflow_model:r,...i}=t;return i}return t}))}function Z(){let e=w.providerGroups[0];T(`update:multimodalFallback`,{provider_id:e?.provider.id??``,backend_model:``})}function Ne(e){T(`update:multimodalFallback`,{provider_id:e.provider_id,backend_model:e.model})}function Pe(){T(`update:multimodalFallback`,void 0)}let Fe=e(()=>{switch(w.entry.tag){case`def`:return`bg-primary/15 text-primary`;case`auto`:return`bg-green-500/15 text-green-500`;case`cust`:return`bg-blue-500/15 text-blue-500`;case`existing`:return`bg-muted/40 text-muted-foreground`;default:return`bg-muted/40 text-muted-foreground`}}),Ie=e(()=>{switch(w.entry.tag){case`def`:return C(`providers.shared.tagDefault`);case`auto`:return C(`providers.shared.tagAuto`);case`cust`:return C(`providers.shared.tagCustom`);case`existing`:return C(`providers.shared.tagExisting`);default:return``}}),Q=e(()=>!!w.entry.targets[0]?.overflow_model),$=e(()=>!!w.entry.multimodalFallback?.backend_model);return(e,n)=>(y(),h(`div`,null,[g.expanded?(y(),h(`div`,B,[d(`div`,V,[d(`span`,H,t(r(C)(`mappings.editor.clientLabel`)),1),E.value?(y(),h(`div`,{key:0,class:`flex-1 flex items-center gap-1`,onClick:n[5]||=o(()=>{},[`stop`])},[m(r(k),{modelValue:D.value,"onUpdate:modelValue":n[4]||=e=>D.value=e,class:`h-7 flex-1 text-xs font-mono border-[1px] border-border`,placeholder:r(C)(`mappings.editor.clientInputPlaceholder`),onKeydown:[c(o(j,[`prevent`]),[`enter`]),c(o(q,[`prevent`]),[`escape`])],onBlur:j,autofocus:``},null,8,[`modelValue`,`placeholder`,`onKeydown`])])):(y(),h(`div`,U,[d(`span`,{class:s([`font-mono text-xs font-semibold text-foreground`,{"cursor-pointer hover:text-primary transition-colors":g.editableClientModel}]),onClick:O},t(g.entry.clientModel),3),g.editableClientModel?(y(),_(r(f),{key:0,variant:`ghost`,size:`xs`,class:`text-[10px] text-muted-foreground/40`,onClick:o(O,[`stop`])},{default:i(()=>[u(t(r(C)(`mappings.editor.edit`)),1)]),_:1})):p(``,!0)]))]),d(`div`,W,[d(`div`,G,[m(r(te),{class:`size-3 text-muted-foreground/50`}),d(`span`,K,t(r(C)(`mappings.editor.failoverChain`)),1)]),(y(!0),h(l,null,v(g.entry.targets,(e,a)=>(y(),h(`div`,{key:a,class:`space-y-0`},[d(`div`,ce,[d(`span`,{class:s([`text-xs font-medium shrink-0 w-5 text-center px-0.5 py-0.5 rounded`,a===0?`bg-primary/10 text-primary`:`bg-muted/40 text-muted-foreground`])},t(a===0?`①`:a===1?`②`:a===2?`③`:`${a+1}`),3),d(`div`,le,[m(A,{providers:g.providerGroups,"model-value":{provider_id:e.provider_id,model:e.backend_model},compact:``,placeholder:r(C)(`providers.shared.selectModel`),"onUpdate:modelValue":e=>je(a,e)},null,8,[`providers`,`model-value`,`placeholder`,`onUpdate:modelValue`])]),a>0&&g.entry.targets.length>1?(y(),_(r(f),{key:0,variant:`ghost`,size:`icon-xs`,class:`shrink-0 text-muted-foreground/30 hover:text-destructive`,onClick:e=>Ae(a)},{default:i(()=>[m(r(S),{class:`size-3`})]),_:1},8,[`onClick`])):p(``,!0)]),a<g.entry.targets.length-1?(y(),h(`div`,ue,[n[13]||=d(`div`,{class:`w-px h-1.5 bg-orange-400/30`},null,-1),d(`span`,de,t(r(C)(`mappings.editor.failoverOnError`)),1)])):p(``,!0)]))),128)),m(r(f),{variant:`ghost`,class:`flex items-center justify-center gap-1 w-full py-1 mt-0.5 text-xs text-muted-foreground/40 border-[1px] border-dashed border-border rounded hover:text-primary hover:border-primary/40`,onClick:Y},{default:i(()=>[m(r(x),{class:`size-3`}),u(` `+t(r(C)(`mappings.addBackup`)),1)]),_:1})]),d(`div`,fe,[d(`div`,pe,[m(r(re),{class:`size-3 text-primary/50`}),d(`span`,me,t(r(C)(`mappings.editor.contextOverflow`)),1),d(`span`,he,t(r(C)(`mappings.editor.contextOverflowHint`)),1)]),Q.value?(y(),h(`div`,ge,[n[14]||=d(`span`,{class:`text-xs shrink-0 w-5 text-center px-0.5 py-0.5 rounded bg-primary/6 text-primary/60`},`↓`,-1),d(`div`,_e,[m(A,{providers:g.providerGroups,"model-value":g.entry.targets[0]?.overflow_provider_id&&g.entry.targets[0]?.overflow_model?{provider_id:g.entry.targets[0].overflow_provider_id,model:g.entry.targets[0].overflow_model}:void 0,compact:``,dashed:``,placeholder:r(C)(`providers.shared.overflowPlaceholder`),"onUpdate:modelValue":n[6]||=e=>X(e)},null,8,[`providers`,`model-value`,`placeholder`])]),m(r(f),{variant:`ghost`,size:`icon-xs`,class:`shrink-0 text-muted-foreground/30 hover:text-destructive`,onClick:n[7]||=e=>X(void 0)},{default:i(()=>[m(r(S),{class:`size-3`})]),_:1})])):(y(),_(r(f),{key:1,variant:`ghost`,size:`xs`,class:`text-xs text-muted-foreground/50 hover:text-primary/60`,onClick:Me},{default:i(()=>[m(r(x),{class:`size-3`}),u(` `+t(r(C)(`providers.shared.addOverflow`)),1)]),_:1}))]),d(`div`,ve,[d(`div`,ye,[m(r(N),{class:`size-3 text-blue-500/50`}),d(`span`,be,t(r(C)(`mappings.editor.multimodalFallback`)),1),$.value?(y(),_(r(ie),{key:0,variant:`outline`,class:`text-[9px] px-1.5 py-0 leading-none text-primary/60 border-[1px] border-primary/20`},{default:i(()=>[u(t(r(C)(`mappings.multimodalFallback.configured`)),1)]),_:1})):p(``,!0)]),$.value?(y(),h(`div`,xe,[d(`span`,Se,[m(r(N),{class:`size-2.5`})]),d(`div`,Ce,[m(A,{providers:g.providerGroups,"model-value":{provider_id:g.entry.multimodalFallback.provider_id,model:g.entry.multimodalFallback.backend_model},compact:``,dashed:``,placeholder:r(C)(`mappings.multimodalFallback.selectProviderModel`),"onUpdate:modelValue":n[8]||=e=>Ne(e)},null,8,[`providers`,`model-value`,`placeholder`])]),m(r(f),{variant:`ghost`,size:`icon-xs`,class:`shrink-0 text-muted-foreground/30 hover:text-destructive`,onClick:Pe},{default:i(()=>[m(r(S),{class:`size-3`})]),_:1})])):(y(),_(r(f),{key:1,variant:`ghost`,size:`xs`,class:`text-xs text-muted-foreground/50 hover:text-primary/60`,onClick:Z},{default:i(()=>[m(r(x),{class:`size-3`}),u(` `+t(r(C)(`mappings.multimodalFallback.add`)),1)]),_:1})),$.value?(y(),h(`div`,we,[m(r(ne),{class:`size-3.5 text-orange-400 shrink-0 mt-0.5`}),d(`div`,Te,[d(`div`,Ee,t(r(C)(`mappings.multimodalFallback.sessionLockWarning`)),1),d(`div`,De,t(r(C)(`mappings.multimodalFallback.sessionLockReason`)),1),d(`div`,Oe,t(r(C)(`mappings.multimodalFallback.costSuggestion`)),1)])])):p(``,!0)])])):(y(),h(`div`,{key:0,class:`flex items-center gap-2 cursor-pointer select-none min-h-[28px]`,onClick:n[3]||=e=>T(`expand`)},[d(`span`,{class:`font-mono text-xs font-semibold text-foreground min-w-[90px] truncate shrink-0`,title:g.entry.clientModel},t(g.entry.clientModel),9,ae),m(r(M),{class:`size-3 text-muted-foreground/30 shrink-0`}),d(`div`,oe,[g.entry.targets.length>0?(y(),h(`span`,se,[n[9]||=d(`span`,{class:`text-[10px] font-semibold`},`①`,-1),u(` `+t(g.entry.targets[0].backend_model)+` `,1),J(g.entry.targets[0].provider_id)?(y(),h(`span`,P,t(J(g.entry.targets[0].provider_id)),1)):p(``,!0)])):p(``,!0),(y(!0),h(l,null,v(g.entry.targets.slice(1),(e,r)=>(y(),h(l,{key:`fo-`+r},[n[10]||=d(`span`,{class:`text-orange-400/40 text-xs shrink-0`},`|`,-1),d(`span`,F,[d(`span`,I,t(r===0?`②`:r===1?`③`:`${r+2}`),1),u(` `+t(e.backend_model),1)])],64))),128)),Q.value?(y(),h(l,{key:1},[n[11]||=d(`span`,{class:`text-muted-foreground/30 text-xs shrink-0`},`|`,-1),d(`span`,L,` ↓ `+t(g.entry.targets[0].overflow_model),1)],64)):p(``,!0),$.value?(y(),h(l,{key:2},[n[12]||=d(`span`,{class:`text-muted-foreground/30 text-xs shrink-0`},`|`,-1),d(`span`,R,[m(r(N),{class:`size-2.5`}),u(` `+t(g.entry.multimodalFallback.backend_model),1)])],64)):p(``,!0)]),d(`span`,{class:s([`text-[10px] px-1.5 py-px rounded font-medium leading-none shrink-0`,Fe.value])},t(Ie.value),3),d(`div`,{class:`shrink-0 flex items-center`,onClick:n[1]||=o(()=>{},[`stop`])},[m(r(ee),{"model-value":g.entry.active,"onUpdate:modelValue":n[0]||=e=>T(`toggle-active`),class:`scale-75`},null,8,[`model-value`])]),m(r(f),{variant:`ghost`,size:`icon-xs`,class:`shrink-0 text-muted-foreground/20 hover:text-destructive`,onClick:n[2]||=o(e=>T(`remove`),[`stop`])},{default:i(()=>[m(r(S),{class:`size-3`})]),_:1}),d(`span`,z,t(r(C)(`mappings.editor.clickToEdit`)),1)]))]))}}),J={key:1,class:`text-[10px] text-muted-foreground leading-snug`},Y=g({__name:`ConcurrencyControl`,props:{modelValue:{},compact:{type:Boolean,default:!1}},emits:[`update:modelValue`],setup(n,{emit:o}){let{t:c}=a(),f=n,g=o;function _(e){g(`update:modelValue`,{...f.modelValue,...e})}let v=e(()=>f.modelValue.mode??j.mode),b=e(()=>f.modelValue.max_concurrency??j.max_concurrency),x=e(()=>f.modelValue.queue_timeout_ms??j.queue_timeout_ms),S=e(()=>f.modelValue.max_queue_size??j.max_queue_size);return(e,a)=>(y(),h(`div`,{class:s(n.compact?`space-y-2`:`flex items-end gap-2 flex-wrap`)},[d(`div`,{class:s([n.compact?``:`w-36`,`space-y-1`])},[m(r(C),{class:`text-xs text-muted-foreground`},{default:i(()=>[u(t(r(c)(`providers.concurrency.mode`)),1)]),_:1}),m(r(O),{"model-value":v.value,"onUpdate:modelValue":a[0]||=e=>_({mode:e})},{default:i(()=>[m(r(E),{class:`w-full text-xs data-[size=default]:h-7`},{default:i(()=>[m(r(D))]),_:1}),m(r(w),null,{default:i(()=>[m(r(T),{value:`auto`},{default:i(()=>[u(t(r(c)(`providers.concurrency.autoAdaptive`)),1)]),_:1}),m(r(T),{value:`manual`},{default:i(()=>[u(t(r(c)(`providers.concurrency.manual`)),1)]),_:1}),m(r(T),{value:`none`},{default:i(()=>[u(t(r(c)(`providers.concurrency.none`)),1)]),_:1})]),_:1})]),_:1},8,[`model-value`])],2),v.value===`none`?p(``,!0):(y(),h(l,{key:0},[d(`div`,{class:s([n.compact?``:`w-24`,`space-y-1`])},[m(r(C),{class:`text-xs text-muted-foreground`},{default:i(()=>[u(t(r(c)(`providers.concurrency.maxConcurrency`)),1)]),_:1}),m(r(k),{"model-value":b.value,type:`number`,min:`1`,max:`100`,class:`text-xs md:text-xs md:text-xs h-7`,"onUpdate:modelValue":a[1]||=e=>_({max_concurrency:Number(e)})},null,8,[`model-value`])],2),d(`div`,{class:s([n.compact?``:`w-30`,`space-y-1`])},[m(r(C),{class:`text-xs text-muted-foreground`},{default:i(()=>[u(t(r(c)(`providers.concurrency.queueTimeout`)),1)]),_:1}),m(r(k),{"model-value":x.value,type:`number`,min:`0`,placeholder:r(c)(`providers.shared.queueTimeoutPlaceholder`),class:`font-mono text-xs md:text-xs md:text-xs h-7`,"onUpdate:modelValue":a[2]||=e=>_({queue_timeout_ms:Number(e)})},null,8,[`model-value`,`placeholder`])],2),d(`div`,{class:s([n.compact?``:`w-30`,`space-y-1`])},[m(r(C),{class:`text-xs text-muted-foreground`},{default:i(()=>[u(t(r(c)(`providers.concurrency.maxQueueSize`)),1)]),_:1}),m(r(k),{"model-value":S.value,type:`number`,min:`1`,max:`1000`,class:`text-xs md:text-xs md:text-xs h-7`,"onUpdate:modelValue":a[3]||=e=>_({max_queue_size:Number(e)})},null,8,[`model-value`])],2)],64)),v.value===`auto`&&!n.compact?(y(),h(`div`,J,t(r(c)(`providers.shared.autoHint`)),1)):p(``,!0)],2))}});export{M as i,q as n,N as r,Y as t};