llm-simple-router 0.11.26 → 0.11.28

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 (53) hide show
  1. package/dist/proxy/format/adapters/anthropic.js +1 -0
  2. package/dist/proxy/format/adapters/shared-error-meta.js +1 -0
  3. package/dist/proxy/format/types.d.ts +1 -1
  4. package/dist/proxy/handler/create-proxy-handler.js +1 -0
  5. package/dist/proxy/handler/failover-loop.js +24 -31
  6. package/dist/proxy/patch/deepseek/patch-orphan-tool-results.d.ts +1 -1
  7. package/dist/proxy/patch/deepseek/patch-orphan-tool-results.js +24 -37
  8. package/dist/proxy/patch/index.js +3 -1
  9. package/dist/proxy/proxy-core.d.ts +3 -1
  10. package/dist/proxy/proxy-core.js +4 -0
  11. package/dist/proxy/routing/modality-redirect.d.ts +6 -3
  12. package/dist/proxy/routing/modality-redirect.js +87 -64
  13. package/frontend-dist/assets/{CardContent-DWjbiHIJ.js → CardContent-BbiKtIeE.js} +1 -1
  14. package/frontend-dist/assets/{CardTitle-DbC3CVDt.js → CardTitle-BKzqpj_c.js} +1 -1
  15. package/frontend-dist/assets/{CascadingModelSelect-D2I-4tJ4.js → CascadingModelSelect-B7lZ1mP0.js} +1 -1
  16. package/frontend-dist/assets/{Checkbox-CVZOHLQe.js → Checkbox-D1Y3kGGA.js} +1 -1
  17. package/frontend-dist/assets/{CollapsibleContent-CEHClfca.js → CollapsibleContent-CoARuSxM.js} +1 -1
  18. package/frontend-dist/assets/{CollapsibleTrigger-CIj4JoFP.js → CollapsibleTrigger-D2zhWvtd.js} +1 -1
  19. package/frontend-dist/assets/{Dashboard-BPVp9LB7.js → Dashboard-CU8be5NR.js} +1 -1
  20. package/frontend-dist/assets/{Input-Dp2t6aNM.js → Input-BOlb6Kuw.js} +1 -1
  21. package/frontend-dist/assets/{Label-e5JBm-GF.js → Label-CCZ5BOi9.js} +1 -1
  22. package/frontend-dist/assets/{Login-Dj_Q5fQN.js → Login-Dux0E-6K.js} +1 -1
  23. package/frontend-dist/assets/{Logs-QZe87kGr.js → Logs-BDoOY0hr.js} +1 -1
  24. package/frontend-dist/assets/{MappingEntryEditor-BSiHd-eN.js → MappingEntryEditor-BPqzjEiL.js} +1 -1
  25. package/frontend-dist/assets/{ModelMappings-DUkirJmq.js → ModelMappings-BT_Cpt-a.js} +1 -1
  26. package/frontend-dist/assets/{Monitor-CQCms2Mk.js → Monitor-tOgUttQg.js} +1 -1
  27. package/frontend-dist/assets/{Providers-DMT1vBsA.js → Providers-Dl1g9D6s.js} +1 -1
  28. package/frontend-dist/assets/{ProxyEnhancement-CajLY_d0.js → ProxyEnhancement-BbBy7nrm.js} +1 -1
  29. package/frontend-dist/assets/{QuickSetup-DRIWC8Pf.js → QuickSetup-rGmuHGJq.js} +1 -1
  30. package/frontend-dist/assets/{RetryRules-Br00WS8z.js → RetryRules-DfEaQ1YN.js} +1 -1
  31. package/frontend-dist/assets/{RouterKeys-e1lyXvx2.js → RouterKeys--rpRgKSn.js} +1 -1
  32. package/frontend-dist/assets/{RovingFocusItem-tdat5oCl.js → RovingFocusItem-CvIxVLbB.js} +1 -1
  33. package/frontend-dist/assets/{Schedules-B8hAohK5.js → Schedules-B7EFQyXL.js} +1 -1
  34. package/frontend-dist/assets/{Settings-BeHJBE0b.js → Settings-BvKMJ0Qa.js} +1 -1
  35. package/frontend-dist/assets/{Setup-C6N1kNf0.js → Setup-4vmVajOU.js} +1 -1
  36. package/frontend-dist/assets/{Switch-BeJMgWUe.js → Switch-D9H1FPsB.js} +1 -1
  37. package/frontend-dist/assets/{TooltipTrigger-CDSPqhiB.js → TooltipTrigger-Btj1Vsxm.js} +1 -1
  38. package/frontend-dist/assets/{TransformRulesForm-VKVF9_SM.js → TransformRulesForm-BJ4caGDj.js} +1 -1
  39. package/frontend-dist/assets/{UnifiedRequestDialog-BcrdA1If.js → UnifiedRequestDialog-BbQBSoU7.js} +1 -1
  40. package/frontend-dist/assets/{VisuallyHiddenInput-bxPkTBV5.js → VisuallyHiddenInput-BoPTforN.js} +1 -1
  41. package/frontend-dist/assets/{button-mPTFckrZ.js → button-wn0cCDld.js} +2 -2
  42. package/frontend-dist/assets/{copy-CwwALOfh.js → copy-DBpQ3kDg.js} +1 -1
  43. package/frontend-dist/assets/{dialog-BDefuMDw.js → dialog-DCaFnzS6.js} +1 -1
  44. package/frontend-dist/assets/{index-CrOh8o49.js → index-Bp-3UWSJ.js} +2 -2
  45. package/frontend-dist/assets/{model-patches-B4ilmdWE.js → model-patches-BmLrUDyn.js} +1 -1
  46. package/frontend-dist/assets/plus-BcoVh92Q.js +1 -0
  47. package/frontend-dist/assets/{sparkles-edZyAh6d.js → sparkles-b5zac545.js} +1 -1
  48. package/frontend-dist/assets/{trash-2-DIXrJYzm.js → trash-2-BgW_R7hP.js} +1 -1
  49. package/frontend-dist/assets/{useClipboard-DiphW3z1.js → useClipboard-DWvZAkuV.js} +1 -1
  50. package/frontend-dist/assets/{useLogRetention-BPMQUvws.js → useLogRetention-DwWdAQWh.js} +1 -1
  51. package/frontend-dist/index.html +2 -2
  52. package/package.json +2 -1
  53. package/frontend-dist/assets/plus-BcuQs6XM.js +0 -1
@@ -7,6 +7,7 @@ const ANTHROPIC_ERROR_META = {
7
7
  concurrencyQueueFull: { type: "api_error", code: "concurrency_queue_full" },
8
8
  concurrencyTimeout: { type: "api_error", code: "concurrency_timeout" },
9
9
  promptTooLong: { type: "invalid_request_error", code: "context_window_exceeded" },
10
+ unsupportedModality: { type: "invalid_request_error", code: "unsupported_modality" },
10
11
  };
11
12
  export const anthropicAdapter = {
12
13
  apiType: "anthropic",
@@ -11,4 +11,5 @@ export const OPENAI_FAMILY_ERROR_META = {
11
11
  concurrencyQueueFull: { type: "server_error", code: "concurrency_queue_full" },
12
12
  concurrencyTimeout: { type: "server_error", code: "concurrency_timeout" },
13
13
  promptTooLong: { type: "invalid_request_error", code: "context_window_exceeded" },
14
+ unsupportedModality: { type: "invalid_request_error", code: "unsupported_modality" },
14
15
  };
@@ -1,5 +1,5 @@
1
1
  import type { Transform } from "stream";
2
- export type ErrorKind = "modelNotFound" | "modelNotAllowed" | "providerUnavailable" | "providerTypeMismatch" | "upstreamConnectionFailed" | "concurrencyQueueFull" | "concurrencyTimeout" | "promptTooLong";
2
+ export type ErrorKind = "modelNotFound" | "modelNotAllowed" | "providerUnavailable" | "providerTypeMismatch" | "upstreamConnectionFailed" | "concurrencyQueueFull" | "concurrencyTimeout" | "promptTooLong" | "unsupportedModality";
3
3
  export interface FormatAdapter {
4
4
  readonly apiType: string;
5
5
  readonly defaultPath: string;
@@ -112,6 +112,7 @@ export function createProxyHandler(config) {
112
112
  concurrencyQueueFull: { type: "server_error", code: "concurrency_queue_full" },
113
113
  concurrencyTimeout: { type: "server_error", code: "concurrency_timeout" },
114
114
  promptTooLong: { type: "invalid_request_error", code: "context_window_exceeded" },
115
+ unsupportedModality: { type: "invalid_request_error", code: "unsupported_modality" },
115
116
  };
116
117
  const apiTypeErrors = createErrorFormatter((kind, message) => ({ error: { message, ...errorMeta[kind] } }));
117
118
  // 默认 upstream path 从 adapter 获取
@@ -120,24 +120,31 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
120
120
  const resolveResult = resolveMapping(db, clientModel, { now: new Date() });
121
121
  // resolveMapping 返回 null 时,需要用占位 snapshot 做错误日志
122
122
  const rejectSnapshot = new PipelineSnapshot();
123
+ /** 构建 RejectParams,消除 4 处重复构造 */
124
+ const buildRejectCtx = (logId, snapshot, overrides) => ({
125
+ db, logId, apiType: ctx.apiType, model: clientModel,
126
+ startTime: Date.now(),
127
+ isStream: ctx.body.stream === true,
128
+ routerKeyId: request.routerKey?.id ?? null,
129
+ originalBody: rawBody, clientHeaders: cliHdrs,
130
+ isFailover: false, originalRequestId: null,
131
+ sessionId: ctx.metadata.get("session_id"),
132
+ pipelineSnapshot: snapshot.toJSON(),
133
+ matcher, logFileWriter,
134
+ ...overrides,
135
+ });
123
136
  if (!resolveResult) {
124
137
  const logId = randomUUID();
125
- const startTime = Date.now();
126
- const isStream = ctx.body.stream === true;
127
- const rCtx = {
128
- db, logId, apiType: ctx.apiType, model: clientModel,
129
- startTime, isStream, routerKeyId: request.routerKey?.id ?? null, originalBody: rawBody, clientHeaders: cliHdrs,
130
- isFailover: false, originalRequestId: null,
131
- sessionId: ctx.metadata.get("session_id"),
132
- pipelineSnapshot: rejectSnapshot.toJSON(),
133
- matcher, logFileWriter,
134
- };
135
- return rejectAndReply(reply, rCtx, errors.modelNotFound(clientModel), `No mapping found for model '${clientModel}'`);
138
+ return rejectAndReply(reply, buildRejectCtx(logId, rejectSnapshot), errors.modelNotFound(clientModel), `No mapping found for model '${clientModel}'`);
136
139
  }
137
140
  let allTargets = resolveResult.allTargets ?? [resolveResult.target];
138
141
  const concurrencyOverride = resolveResult.concurrency_override;
139
142
  // 2. modality-redirect 层:模态重定向 → 可能 prepend fallback target
140
143
  allTargets = computeModalityRedirectTargets(db, allTargets, clientModel, ctx.body, precomputeSnapshot);
144
+ // 2a. modality-redirect 层返回空列表 → 提前报错(无 target 支持请求模态)
145
+ if (allTargets.length === 0) {
146
+ return rejectAndReply(reply, buildRejectCtx(randomUUID(), precomputeSnapshot), errors.unsupportedModality(), `No eligible target: request modalities not supported by any available model`);
147
+ }
141
148
  // 3. OF 层:为每个 target 预计算 overflow
142
149
  const targetsBeforeOF = allTargets.length;
143
150
  const ofResult = expandOverflowTargets(allTargets, db, ctx.body);
@@ -160,18 +167,7 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
160
167
  allTargets = filtered;
161
168
  overflowIndices = newOverflowIndices;
162
169
  if (allTargets.length === 0) {
163
- const logId = randomUUID();
164
- const startTime = Date.now();
165
- const isStream = ctx.body.stream === true;
166
- const rCtx = {
167
- db, logId, apiType: ctx.apiType, model: clientModel,
168
- startTime, isStream, routerKeyId: request.routerKey?.id ?? null, originalBody: rawBody, clientHeaders: cliHdrs,
169
- isFailover: false, originalRequestId: null,
170
- sessionId: ctx.metadata.get("session_id"),
171
- pipelineSnapshot: precomputeSnapshot.toJSON(),
172
- matcher, logFileWriter,
173
- };
174
- return rejectAndReply(reply, rCtx, errors.modelNotAllowed(clientModel), `No allowed model available for '${clientModel}'`);
170
+ return rejectAndReply(reply, buildRejectCtx(randomUUID(), precomputeSnapshot), errors.modelNotAllowed(clientModel), `No allowed model available for '${clientModel}'`);
175
171
  }
176
172
  }
177
173
  // 预计算完成,缓存到循环外
@@ -206,14 +202,11 @@ export async function executeFailoverLoop(ctx, errors, deps, upstreamPath, adapt
206
202
  let currentBody = { ...ctx.body };
207
203
  const isStream = currentBody.stream === true;
208
204
  const iterationSnapshot = new PipelineSnapshot(precomputeSnapshot.getStages());
209
- const rCtx = {
210
- db, logId, apiType: ctx.apiType, model: clientModel,
211
- startTime, isStream, routerKeyId, originalBody: rawBody, clientHeaders: cliHdrs,
212
- isFailover: isFailoverIteration, originalRequestId: isFailoverIteration ? rootLogId : null,
213
- sessionId: ctx.metadata.get("session_id"),
214
- pipelineSnapshot: iterationSnapshot.toJSON(),
215
- matcher, logFileWriter,
216
- };
205
+ const rCtx = buildRejectCtx(logId, iterationSnapshot, {
206
+ startTime,
207
+ isFailover: isFailoverIteration,
208
+ originalRequestId: isFailoverIteration ? rootLogId : null,
209
+ });
217
210
  // --- 选第一个非 excluded target ---
218
211
  const filtered = filterExcluded(cachedTargets, excludeTargets);
219
212
  if (filtered.length === 0) {
@@ -19,7 +19,7 @@ export declare function patchOrphanToolResults(body: Record<string, unknown>): v
19
19
  *
20
20
  * 检测两种方向的不匹配:
21
21
  * - 正向:role:"tool" 消息的 tool_call_id 无对应 assistant tool_calls[].id → 移除孤儿 tool 消息
22
- * - 反向:非末尾 assistant 的 tool_calls[].id 无对应 tool 消息 → 移除该 tool_call 条目
22
+ * - 反向:非末尾 assistant 的 tool_calls[].id 无对应 tool 消息 → 补入合成 tool 消息
23
23
  *
24
24
  * 反向跳过最后一条 assistant:它可能是正常的工具调用中间状态(模型刚返回 tool_calls,
25
25
  * 客户端还没来得及执行并回传 tool 消息)。
@@ -116,7 +116,7 @@ export function patchOrphanToolResults(body) {
116
116
  *
117
117
  * 检测两种方向的不匹配:
118
118
  * - 正向:role:"tool" 消息的 tool_call_id 无对应 assistant tool_calls[].id → 移除孤儿 tool 消息
119
- * - 反向:非末尾 assistant 的 tool_calls[].id 无对应 tool 消息 → 移除该 tool_call 条目
119
+ * - 反向:非末尾 assistant 的 tool_calls[].id 无对应 tool 消息 → 补入合成 tool 消息
120
120
  *
121
121
  * 反向跳过最后一条 assistant:它可能是正常的工具调用中间状态(模型刚返回 tool_calls,
122
122
  * 客户端还没来得及执行并回传 tool 消息)。
@@ -129,7 +129,6 @@ export function patchOrphanToolResultsOA(body) {
129
129
  if (!messages || !Array.isArray(messages) || messages.length === 0)
130
130
  return;
131
131
  // ---- 正向:移除孤儿 tool 消息 ----
132
- // 收集所有 assistant tool_calls IDs
133
132
  const knownToolCallIds = new Set();
134
133
  for (const msg of messages) {
135
134
  if (msg.role !== "assistant")
@@ -142,7 +141,6 @@ export function patchOrphanToolResultsOA(body) {
142
141
  knownToolCallIds.add(tc.id);
143
142
  }
144
143
  }
145
- // 移除无主 tool 消息(逆序遍历避免索引偏移)
146
144
  let changed = false;
147
145
  for (let i = messages.length - 1; i >= 0; i--) {
148
146
  const msg = messages[i];
@@ -154,8 +152,7 @@ export function patchOrphanToolResultsOA(body) {
154
152
  changed = true;
155
153
  }
156
154
  }
157
- // ---- 反向:移除孤儿 tool_calls 条目(非末尾 assistant)----
158
- // 收集所有 tool 消息的 ID
155
+ // ---- 反向:为孤儿 tool_call 补入合成 tool 消息 ----
159
156
  const knownToolMsgIds = new Set();
160
157
  for (const msg of messages) {
161
158
  if (msg.role !== "tool")
@@ -164,46 +161,39 @@ export function patchOrphanToolResultsOA(body) {
164
161
  if (toolCallId)
165
162
  knownToolMsgIds.add(toolCallId);
166
163
  }
167
- const lastMsgIdx = messages.length - 1;
168
- for (let i = 0; i <= lastMsgIdx; i++) {
164
+ // 逆序遍历 assistant,这样 splice 插入不影响前面 assistant 的索引
165
+ for (let i = messages.length - 1; i >= 0; i--) {
169
166
  const msg = messages[i];
170
167
  if (msg.role !== "assistant")
171
168
  continue;
172
169
  // 跳过最后一条 assistant:它可能是正常的工具调用中间状态
173
- if (i === lastMsgIdx)
174
- break;
170
+ if (i === messages.length - 1)
171
+ continue;
175
172
  const toolCalls = msg.tool_calls;
176
173
  if (!toolCalls || toolCalls.length === 0)
177
174
  continue;
178
- const before = toolCalls.length;
179
- const filtered = toolCalls.filter(tc => {
175
+ const orphans = [];
176
+ for (const tc of toolCalls) {
180
177
  const id = tc.id;
181
- return !id || knownToolMsgIds.has(id);
182
- });
183
- if (filtered.length < before) {
184
- if (filtered.length === 0) {
185
- delete msg.tool_calls;
186
- }
187
- else {
188
- msg.tool_calls = filtered;
178
+ // id 忽略:不补不删
179
+ if (!id)
180
+ continue;
181
+ if (!knownToolMsgIds.has(id)) {
182
+ orphans.push(id);
189
183
  }
190
- changed = true;
191
184
  }
185
+ if (orphans.length === 0)
186
+ continue;
187
+ // 在该 assistant 后面插入合成 tool 消息
188
+ const syntheticMsgs = orphans.map(id => ({
189
+ role: "tool",
190
+ tool_call_id: id,
191
+ content: "[context truncated]",
192
+ }));
193
+ messages.splice(i + 1, 0, ...syntheticMsgs);
194
+ changed = true;
192
195
  }
193
196
  if (changed) {
194
- // 移除空壳 assistant(content 无实质内容且无 tool_calls)
195
- for (let i = messages.length - 1; i >= 0; i--) {
196
- const m = messages[i];
197
- if (m.role !== "assistant")
198
- continue;
199
- if (m.tool_calls)
200
- continue;
201
- const content = m.content;
202
- if (content === null || content === undefined || content === ""
203
- || (Array.isArray(content) && content.length === 0)) {
204
- messages.splice(i, 1);
205
- }
206
- }
207
197
  // 合并连续 user 消息
208
198
  for (let i = 1; i < messages.length;) {
209
199
  if (messages[i].role === "user" && messages[i - 1].role === "user") {
@@ -221,8 +211,6 @@ export function patchOrphanToolResultsOA(body) {
221
211
  }
222
212
  // Step 4: 修复 tool_calls 消息顺序——将插在 assistant(tool_calls) 与 tool 之间的
223
213
  // 非 tool 消息(如用户中断、系统提醒)挪到 tool 消息之后
224
- // scanLimit 上限:每个 tool_call 最多对应 1 个 tool 消息 + 1 个可能穿插的非 tool 消息,
225
- // 额外 +3 留出边界余量(额外的 user/system 消息)
226
214
  const SCAN_LIMIT_EXTRA = 3;
227
215
  for (let idx = 0; idx < messages.length; idx++) {
228
216
  const msg = messages[idx];
@@ -232,7 +220,7 @@ export function patchOrphanToolResultsOA(body) {
232
220
  const expectedIds = new Set(toolCalls.map(tc => tc.id));
233
221
  const intervening = [];
234
222
  const toolMsgs = [];
235
- const SCAN_SLOTS_PER_CALL = 2; // 每个 tool_call: 1 个 tool 消息 + 1 个可能穿插的消息
223
+ const SCAN_SLOTS_PER_CALL = 2;
236
224
  const scanLimit = idx + 1 + expectedIds.size * SCAN_SLOTS_PER_CALL + SCAN_LIMIT_EXTRA;
237
225
  let j = idx + 1;
238
226
  for (; j < messages.length && j <= scanLimit; j++) {
@@ -250,7 +238,6 @@ export function patchOrphanToolResultsOA(body) {
250
238
  if (intervening.length > 0 && toolMsgs.length > 0 && expectedIds.size === 0) {
251
239
  const count = intervening.length + toolMsgs.length;
252
240
  messages.splice(idx + 1, count, ...toolMsgs, ...intervening);
253
- // splice 后跳过已重排的区域(toolMsgs + intervening),避免重复处理
254
241
  idx += count;
255
242
  }
256
243
  }
@@ -89,7 +89,9 @@ function needsDeepSeekPatch(body, provider) {
89
89
  if (provider.base_url.includes("deepseek"))
90
90
  return true;
91
91
  const model = body.model ?? "";
92
- return model.includes("deepseek");
92
+ if (model.includes("deepseek"))
93
+ return true;
94
+ return false;
93
95
  }
94
96
  /**
95
97
  * 格式无关的 patch 匹配:将比较值与 modelPatches(已由 parseModels 归一化)
@@ -1,6 +1,7 @@
1
1
  import type { Provider } from "../db/index.js";
2
2
  import type { GetTransportResult } from "./transport/http.js";
3
3
  import type { RawHeaders } from "./types.js";
4
+ import type { ErrorKind } from "./format/types.js";
4
5
  export interface ProxyErrorResponse {
5
6
  statusCode: number;
6
7
  body: unknown;
@@ -14,8 +15,9 @@ export interface ProxyErrorFormatter {
14
15
  concurrencyQueueFull(providerId: string): ProxyErrorResponse;
15
16
  concurrencyTimeout(providerId: string, timeoutMs: number): ProxyErrorResponse;
16
17
  promptTooLong(): ProxyErrorResponse;
18
+ unsupportedModality(): ProxyErrorResponse;
17
19
  }
18
- export type ErrorKind = "modelNotFound" | "modelNotAllowed" | "providerUnavailable" | "providerTypeMismatch" | "upstreamConnectionFailed" | "concurrencyQueueFull" | "concurrencyTimeout" | "promptTooLong";
20
+ export type { ErrorKind } from "./format/types.js";
19
21
  /**
20
22
  * 工厂函数,消除 openai/anthropic 错误格式化的重复代码。
21
23
  * statusCode 和 message 两个 provider 完全一致,仅 body 格式不同,
@@ -38,6 +38,10 @@ export function createErrorFormatter(formatBody) {
38
38
  statusCode: 400,
39
39
  body: formatBody("promptTooLong", "Prompt is too long: the input tokens exceed the model context window limit."),
40
40
  }),
41
+ unsupportedModality: () => ({
42
+ statusCode: 400,
43
+ body: formatBody("unsupportedModality", "Request contains multimodal content but no available model supports the required modality."),
44
+ }),
41
45
  };
42
46
  }
43
47
  // ---------- URL utilities ----------
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * MRL(Modality Redirect)预计算层
3
3
  *
4
- * 纯函数:检测请求体是否包含多模态内容(图片、音频等),若首 target 不支持
5
- * 且配置了 multimodal_fallback,则将 fallback target prepend 到列表头部。
4
+ * 纯函数:检测请求体是否包含多模态内容(图片、音频等),过滤不支持对应模态的 targets,
5
+ * 必要时用 multimodal_fallback 替换全部被过滤的 targets。
6
6
  */
7
7
  import type Database from "better-sqlite3";
8
8
  import type { Target } from "../../core/types.js";
@@ -17,6 +17,9 @@ import { PipelineSnapshot } from "../pipeline-snapshot.js";
17
17
  */
18
18
  export declare function detectModalities(body: Record<string, unknown>): Set<string>;
19
19
  /**
20
- * MRL 层主函数。异常安全:任何内部错误均 catch 并返回原始 targets。
20
+ * MRL 层主函数。采用 filter+replace 策略:
21
+ * 1. 过滤不支持请求模态的 targets
22
+ * 2. 全部过滤完时尝试用 multimodal_fallback 替换
23
+ * 异常安全:任何内部错误均 catch 并返回原始 targets。
21
24
  */
22
25
  export declare function computeModalityRedirectTargets(db: Database.Database, targets: Target[], clientModel: string, body: Record<string, unknown>, snapshot: PipelineSnapshot): Target[];
@@ -80,16 +80,27 @@ function supportsModality(capabilities, modality) {
80
80
  return Array.isArray(capabilities) && capabilities.includes(modality);
81
81
  }
82
82
  /**
83
- * MRL 层主函数。异常安全:任何内部错误均 catch 并返回原始 targets。
83
+ * MRL 层主函数。采用 filter+replace 策略:
84
+ * 1. 过滤不支持请求模态的 targets
85
+ * 2. 全部过滤完时尝试用 multimodal_fallback 替换
86
+ * 异常安全:任何内部错误均 catch 并返回原始 targets。
84
87
  */
85
88
  export function computeModalityRedirectTargets(db, targets, clientModel, body, snapshot) {
86
89
  try {
87
- // 空列表直接返回
88
- if (targets.length === 0)
90
+ // 1. 空列表 → 记录诊断信息后返回(运维排查 400 时可看到 modality-redirect 阶段记录)
91
+ if (targets.length === 0) {
92
+ snapshot.add({
93
+ stage: "modality-redirect",
94
+ triggered: false,
95
+ original_model: "",
96
+ redirect_to: "",
97
+ redirect_provider: "",
98
+ reason: "empty-targets-input",
99
+ });
89
100
  return targets;
90
- // 检测多模态内容
101
+ }
102
+ // 2. 检测多模态内容
91
103
  const modalities = detectModalities(body);
92
- // 无多模态内容 → no-op
93
104
  if (modalities.size === 0) {
94
105
  snapshot.add({
95
106
  stage: "modality-redirect",
@@ -101,39 +112,65 @@ export function computeModalityRedirectTargets(db, targets, clientModel, body, s
101
112
  });
102
113
  return targets;
103
114
  }
104
- // 检查首 target 的 provider 是否已支持所有检测到的模态
105
- const firstTarget = targets[0];
106
- const provider = getProviderById(db, firstTarget.provider_id);
107
- if (provider) {
115
+ // 3. 过滤:遍历所有 targets,检查每个是否支持所有检测到的模态
116
+ const eligible = [];
117
+ for (const target of targets) {
118
+ const provider = getProviderById(db, target.provider_id);
119
+ if (!provider) {
120
+ // provider 不存在 → 保留(安全行为)
121
+ eligible.push(target);
122
+ continue;
123
+ }
108
124
  const entries = parseModels(provider.models);
109
- const entry = entries.find(e => e.name === firstTarget.backend_model);
110
- const firstTargetCapabilities = entry?.capabilities ?? [];
111
- const allSupported = [...modalities].every(m => supportsModality(firstTargetCapabilities, m));
125
+ const entry = entries.find(e => e.name === target.backend_model);
126
+ const capabilities = entry?.capabilities ?? [];
127
+ const allSupported = [...modalities].every(m => supportsModality(capabilities, m));
112
128
  if (allSupported) {
113
- snapshot.add({
114
- stage: "modality-redirect",
115
- triggered: false,
116
- original_model: firstTarget.backend_model,
117
- redirect_to: "",
118
- redirect_provider: "",
119
- reason: "first-target-supports-all-modalities",
120
- });
121
- return targets;
129
+ eligible.push(target);
122
130
  }
131
+ // 不支持 → 过滤掉
123
132
  }
124
- // 查找 multimodal_fallback 配置
133
+ // 4. 全部支持 → 不需要过滤
134
+ if (eligible.length === targets.length) {
135
+ snapshot.add({
136
+ stage: "modality-redirect",
137
+ triggered: false,
138
+ original_model: targets[0].backend_model,
139
+ redirect_to: "",
140
+ redirect_provider: "",
141
+ reason: "all-targets-support-modalities",
142
+ });
143
+ return targets;
144
+ }
145
+ // 5. 部分过滤 → 返回 eligible
146
+ if (eligible.length > 0) {
147
+ snapshot.add({
148
+ stage: "modality-redirect",
149
+ triggered: true,
150
+ original_model: targets[0].backend_model,
151
+ redirect_to: "",
152
+ redirect_provider: "",
153
+ reason: "filtered-ineligible-targets",
154
+ detected_modalities: [...modalities],
155
+ });
156
+ return eligible;
157
+ }
158
+ // 6. 全部过滤完 → 尝试 fallback
159
+ const firstOriginalModel = targets[0].backend_model;
160
+ // 6a. 查找映射组
125
161
  const group = getMappingGroup(db, clientModel);
126
162
  if (!group) {
127
163
  snapshot.add({
128
164
  stage: "modality-redirect",
129
165
  triggered: false,
130
- original_model: firstTarget.backend_model,
166
+ original_model: firstOriginalModel,
131
167
  redirect_to: "",
132
168
  redirect_provider: "",
133
169
  reason: "no-mapping-group",
134
170
  });
135
- return targets;
171
+ return [];
136
172
  }
173
+ // 6b. 解析 rule
137
174
  let rule;
138
175
  try {
139
176
  rule = JSON.parse(group.rule);
@@ -142,24 +179,25 @@ export function computeModalityRedirectTargets(db, targets, clientModel, body, s
142
179
  snapshot.add({
143
180
  stage: "modality-redirect",
144
181
  triggered: false,
145
- original_model: firstTarget.backend_model,
182
+ original_model: firstOriginalModel,
146
183
  redirect_to: "",
147
184
  redirect_provider: "",
148
185
  reason: "rule-parse-error",
149
186
  });
150
- return targets;
187
+ return [];
151
188
  }
189
+ // 6c. 检查 multimodal_fallback 配置
152
190
  const fallback = rule.multimodal_fallback;
153
191
  if (fallback == null || typeof fallback !== "object") {
154
192
  snapshot.add({
155
193
  stage: "modality-redirect",
156
194
  triggered: false,
157
- original_model: firstTarget.backend_model,
195
+ original_model: firstOriginalModel,
158
196
  redirect_to: "",
159
197
  redirect_provider: "",
160
- reason: "no-multimodal-fallback-configured",
198
+ reason: "no-eligible-targets",
161
199
  });
162
- return targets;
200
+ return [];
163
201
  }
164
202
  const fb = fallback;
165
203
  const fbProviderId = fb.provider_id;
@@ -168,59 +206,43 @@ export function computeModalityRedirectTargets(db, targets, clientModel, body, s
168
206
  snapshot.add({
169
207
  stage: "modality-redirect",
170
208
  triggered: false,
171
- original_model: firstTarget.backend_model,
209
+ original_model: firstOriginalModel,
172
210
  redirect_to: "",
173
211
  redirect_provider: "",
174
- reason: "invalid-fallback-config",
212
+ reason: "no-eligible-targets",
175
213
  });
176
- return targets;
214
+ return [];
177
215
  }
178
- // fallback provider 必须存在且 active
216
+ // 6d. fallback provider 必须存在且 active
179
217
  const fbProvider = getProviderById(db, fbProviderId);
180
218
  if (!fbProvider || fbProvider.is_active !== 1) {
181
219
  snapshot.add({
182
220
  stage: "modality-redirect",
183
221
  triggered: false,
184
- original_model: firstTarget.backend_model,
222
+ original_model: firstOriginalModel,
185
223
  redirect_to: fbBackendModel,
186
224
  redirect_provider: fbProviderId,
187
- reason: "fallback-provider-unavailable",
225
+ reason: "no-eligible-targets",
188
226
  });
189
- return targets;
227
+ return [];
190
228
  }
191
- // 检查 fallback model 是否覆盖所有首 target 缺失的模态
192
- const firstTargetCapabilities = provider
193
- ? parseModels(provider.models).find(e => e.name === firstTarget.backend_model)?.capabilities ?? []
194
- : [];
195
- const missingModalities = [...modalities].filter(m => !supportsModality(firstTargetCapabilities, m));
229
+ // 6e. fallback 必须覆盖所有检测到的模态
196
230
  const fbEntry = parseModels(fbProvider.models).find(e => e.name === fbBackendModel);
197
231
  const fbCapabilities = fbEntry?.capabilities ?? [];
198
- const fbMissing = missingModalities.filter(m => !supportsModality(fbCapabilities, m));
232
+ const fbMissing = [...modalities].filter(m => !supportsModality(fbCapabilities, m));
199
233
  if (fbMissing.length > 0) {
200
234
  snapshot.add({
201
235
  stage: "modality-redirect",
202
236
  triggered: false,
203
- original_model: firstTarget.backend_model,
237
+ original_model: firstOriginalModel,
204
238
  redirect_to: fbBackendModel,
205
239
  redirect_provider: fbProviderId,
206
- reason: "fallback-missing-modality",
240
+ reason: "no-eligible-targets",
207
241
  detected_modalities: [...modalities],
208
242
  });
209
- return targets;
210
- }
211
- // prepend fallback target(如果与首 target 相同则跳过,避免重复消耗 failover 迭代)
212
- if (fbProviderId === firstTarget.provider_id && fbBackendModel === firstTarget.backend_model) {
213
- snapshot.add({
214
- stage: "modality-redirect",
215
- triggered: false,
216
- original_model: firstTarget.backend_model,
217
- redirect_to: fbBackendModel,
218
- redirect_provider: fbProviderId,
219
- reason: "fallback-same-as-first-target",
220
- detected_modalities: [...modalities],
221
- });
222
- return targets;
243
+ return [];
223
244
  }
245
+ // 6f. fallback 覆盖所有模态 → 替换
224
246
  const fbTarget = {
225
247
  provider_id: fbProviderId,
226
248
  backend_model: fbBackendModel,
@@ -228,17 +250,18 @@ export function computeModalityRedirectTargets(db, targets, clientModel, body, s
228
250
  snapshot.add({
229
251
  stage: "modality-redirect",
230
252
  triggered: true,
231
- original_model: firstTarget.backend_model,
253
+ original_model: firstOriginalModel,
232
254
  redirect_to: fbBackendModel,
233
255
  redirect_provider: fbProviderId,
234
- reason: "first-target-lacks-modality",
256
+ reason: "replaced-with-fallback",
235
257
  detected_modalities: [...modalities],
236
258
  });
237
- return [fbTarget, ...targets];
259
+ return [fbTarget];
238
260
  }
239
261
  catch (err) {
240
- // 异常安全:返回原始 targets,但记录诊断信息
241
- console.error('computeModalityRedirectTargets: internal error, falling back to original targets', err);
262
+ // 异常安全:返回空数组,让 failover-loop 统一走 unsupportedModality 错误路径
263
+ // 避免将多模态请求发给不支持模态的 provider(比返回原始 targets 更安全)
264
+ console.error('computeModalityRedirectTargets: internal error, returning empty targets', err);
242
265
  snapshot.add({
243
266
  stage: "modality-redirect",
244
267
  triggered: false,
@@ -247,6 +270,6 @@ export function computeModalityRedirectTargets(db, targets, clientModel, body, s
247
270
  redirect_provider: "",
248
271
  reason: "internal-error",
249
272
  });
250
- return targets;
273
+ return [];
251
274
  }
252
275
  }
@@ -1 +1 @@
1
- import{Gt as e,et as t,gt as n,it as r,qt as i,r as a,yt as o}from"./button-mPTFckrZ.js";var s=[`data-size`],c=r({__name:`Card`,props:{class:{type:[Boolean,null,String,Object,Array]},size:{default:`default`}},setup(r){let c=r;return(l,u)=>(n(),t(`div`,{"data-slot":`card`,"data-size":r.size,class:i(e(a)(`ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-lg py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg group/card flex flex-col`,c.class))},[o(l.$slots,`default`)],10,s))}}),l=r({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(r){let s=r;return(r,c)=>(n(),t(`div`,{"data-slot":`card-content`,class:i(e(a)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[o(r.$slots,`default`)],2))}});export{c as n,l as t};
1
+ import{Gt as e,et as t,gt as n,it as r,qt as i,r as a,yt as o}from"./button-wn0cCDld.js";var s=[`data-size`],c=r({__name:`Card`,props:{class:{type:[Boolean,null,String,Object,Array]},size:{default:`default`}},setup(r){let c=r;return(l,u)=>(n(),t(`div`,{"data-slot":`card`,"data-size":r.size,class:i(e(a)(`ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-lg py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg group/card flex flex-col`,c.class))},[o(l.$slots,`default`)],10,s))}}),l=r({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(r){let s=r;return(r,c)=>(n(),t(`div`,{"data-slot":`card-content`,class:i(e(a)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[o(r.$slots,`default`)],2))}});export{c as n,l as t};
@@ -1 +1 @@
1
- import{Gt as e,et as t,gt as n,it as r,qt as i,r as a,yt as o}from"./button-mPTFckrZ.js";var s=r({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(r){let s=r;return(r,c)=>(n(),t(`div`,{"data-slot":`card-header`,class:i(e(a)(`gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]`,s.class))},[o(r.$slots,`default`)],2))}}),c=r({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(r){let s=r;return(r,c)=>(n(),t(`div`,{"data-slot":`card-title`,class:i(e(a)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[o(r.$slots,`default`)],2))}});export{s as n,c as t};
1
+ import{Gt as e,et as t,gt as n,it as r,qt as i,r as a,yt as o}from"./button-wn0cCDld.js";var s=r({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(r){let s=r;return(r,c)=>(n(),t(`div`,{"data-slot":`card-header`,class:i(e(a)(`gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]`,s.class))},[o(r.$slots,`default`)],2))}}),c=r({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(r){let s=r;return(r,c)=>(n(),t(`div`,{"data-slot":`card-title`,class:i(e(a)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[o(r.$slots,`default`)],2))}});export{s as n,c as t};
@@ -1 +1 @@
1
- import{$ as e,Gt as t,H as n,J as r,L as i,Lt as a,Q as o,X as s,Xt as c,Z as l,et as u,gt as d,it as f,kt as p,qt as m,rt as h,vt as g}from"./button-mPTFckrZ.js";import{b as _,ft as v,ht as y,v as b,y as x}from"./index-CrOh8o49.js";var S=i(`chevron-right`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),C=[`onMouseenter`],w={class:`truncate max-w-40`},T={key:0,class:`ml-1 text-[10px] px-1 py-px rounded bg-emerald-500/15 text-emerald-400 shrink-0`},E=[`onMouseenter`],D=[`onClick`],O={class:`truncate`},k={key:0,class:`shrink-0 text-xs text-muted-foreground`},A={key:0,class:`px-2 py-1.5 text-sm text-muted-foreground`},j=f({__name:`CascadingSelect`,props:{groups:{},modelValue:{},placeholder:{default:``},compact:{type:Boolean,default:!1}},emits:[`update:modelValue`],setup(i,{emit:f}){let{t:y}=n(),j=i,M=s(()=>j.placeholder||y(`common.selectPlaceholder`)),N=f,P=a(!1),F=a(null),I=s(()=>{if(!j.modelValue)return``;let e=j.groups.find(e=>e.key===j.modelValue.groupKey);if(!e)return``;let t=e.options.find(e=>e.value===j.modelValue.value);return t?`${e.label} / ${t.label}`:``});function L(e,t){N(`update:modelValue`,{groupKey:e,value:t}),P.value=!1}function R(e){P.value=e,e||(F.value=null)}return(n,a)=>(d(),o(t(_),{open:P.value,"onUpdate:open":R},{default:p(()=>[h(t(b),{"as-child":``},{default:p(()=>[l(`div`,{class:m([`flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer hover:bg-accent hover:text-accent-foreground`,[i.compact?`h-8 text-xs px-2.5 py-1`:`h-10 text-sm px-3 py-2`,{"ring-2 ring-ring ring-offset-2":P.value}]])},[l(`span`,{class:m([`truncate`,i.modelValue?`text-foreground`:`text-muted-foreground`])},c(I.value||M.value),3),h(t(v),{class:`h-4 w-4 shrink-0 opacity-50`})],2)]),_:1}),h(t(x),{align:`start`,"side-offset":4,class:`z-[200] w-auto min-w-56 overflow-visible p-1`},{default:p(()=>[(d(!0),u(r,null,g(i.groups,n=>(d(),u(`div`,{key:n.key,class:m([`relative flex cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground`,{"bg-accent text-accent-foreground z-10":F.value===n.key}]),onMouseenter:e=>F.value=n.key},[l(`span`,w,c(n.label),1),n.badge?(d(),u(`span`,T,c(n.badge),1)):e(``,!0),h(t(S),{class:`ml-1 h-4 w-4 shrink-0 opacity-50`}),F.value===n.key&&n.options.length>0?(d(),u(`div`,{key:1,class:`absolute left-full top-0 ml-0.5 min-w-48 rounded-md border bg-popover p-1 text-popover-foreground shadow-md`,onMouseenter:e=>F.value=n.key},[(d(!0),u(r,null,g(n.options,t=>(d(),u(`div`,{key:t.value,class:m([`flex cursor-pointer items-center justify-between gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground`,{"bg-accent text-accent-foreground":i.modelValue?.groupKey===n.key&&i.modelValue?.value===t.value}]),onClick:e=>L(n.key,t.value)},[l(`span`,O,c(t.label),1),t.tag?(d(),u(`span`,k,c(t.tag),1)):e(``,!0)],10,D))),128))],40,E)):e(``,!0)],42,C))),128)),i.groups.length===0?(d(),u(`div`,A,c(t(y)(`common.noOptions`)),1)):e(``,!0)]),_:1})]),_:1},8,[`open`]))}}),M=f({__name:`CascadingModelSelect`,props:{providers:{},modelValue:{},placeholder:{default:``},compact:{type:Boolean}},emits:[`update:modelValue`],setup(e,{emit:t}){let{t:r}=n(),i=e,a=s(()=>i.placeholder||r(`mappings.selectProviderModel`)),c=t,l=s(()=>i.providers.map(e=>({key:e.provider.id,label:e.provider.name,badge:e.isNew?r(`common.new`):void 0,options:e.models.map(e=>({value:e.name,label:e.name,tag:y(e.contextWindow)}))}))),u=s(()=>i.modelValue?{groupKey:i.modelValue.provider_id,value:i.modelValue.model}:void 0);function f(e){c(`update:modelValue`,{provider_id:e.groupKey,model:e.value})}return(t,n)=>(d(),o(j,{groups:l.value,"model-value":u.value,placeholder:a.value,compact:e.compact,"onUpdate:modelValue":f},null,8,[`groups`,`model-value`,`placeholder`,`compact`]))}});export{M as t};
1
+ import{$ as e,Gt as t,H as n,J as r,L as i,Lt as a,Q as o,X as s,Xt as c,Z as l,et as u,gt as d,it as f,kt as p,qt as m,rt as h,vt as g}from"./button-wn0cCDld.js";import{b as _,ft as v,ht as y,v as b,y as x}from"./index-Bp-3UWSJ.js";var S=i(`chevron-right`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),C=[`onMouseenter`],w={class:`truncate max-w-40`},T={key:0,class:`ml-1 text-[10px] px-1 py-px rounded bg-emerald-500/15 text-emerald-400 shrink-0`},E=[`onMouseenter`],D=[`onClick`],O={class:`truncate`},k={key:0,class:`shrink-0 text-xs text-muted-foreground`},A={key:0,class:`px-2 py-1.5 text-sm text-muted-foreground`},j=f({__name:`CascadingSelect`,props:{groups:{},modelValue:{},placeholder:{default:``},compact:{type:Boolean,default:!1}},emits:[`update:modelValue`],setup(i,{emit:f}){let{t:y}=n(),j=i,M=s(()=>j.placeholder||y(`common.selectPlaceholder`)),N=f,P=a(!1),F=a(null),I=s(()=>{if(!j.modelValue)return``;let e=j.groups.find(e=>e.key===j.modelValue.groupKey);if(!e)return``;let t=e.options.find(e=>e.value===j.modelValue.value);return t?`${e.label} / ${t.label}`:``});function L(e,t){N(`update:modelValue`,{groupKey:e,value:t}),P.value=!1}function R(e){P.value=e,e||(F.value=null)}return(n,a)=>(d(),o(t(_),{open:P.value,"onUpdate:open":R},{default:p(()=>[h(t(b),{"as-child":``},{default:p(()=>[l(`div`,{class:m([`flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer hover:bg-accent hover:text-accent-foreground`,[i.compact?`h-8 text-xs px-2.5 py-1`:`h-10 text-sm px-3 py-2`,{"ring-2 ring-ring ring-offset-2":P.value}]])},[l(`span`,{class:m([`truncate`,i.modelValue?`text-foreground`:`text-muted-foreground`])},c(I.value||M.value),3),h(t(v),{class:`h-4 w-4 shrink-0 opacity-50`})],2)]),_:1}),h(t(x),{align:`start`,"side-offset":4,class:`z-[200] w-auto min-w-56 overflow-visible p-1`},{default:p(()=>[(d(!0),u(r,null,g(i.groups,n=>(d(),u(`div`,{key:n.key,class:m([`relative flex cursor-pointer items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground`,{"bg-accent text-accent-foreground z-10":F.value===n.key}]),onMouseenter:e=>F.value=n.key},[l(`span`,w,c(n.label),1),n.badge?(d(),u(`span`,T,c(n.badge),1)):e(``,!0),h(t(S),{class:`ml-1 h-4 w-4 shrink-0 opacity-50`}),F.value===n.key&&n.options.length>0?(d(),u(`div`,{key:1,class:`absolute left-full top-0 ml-0.5 min-w-48 rounded-md border bg-popover p-1 text-popover-foreground shadow-md`,onMouseenter:e=>F.value=n.key},[(d(!0),u(r,null,g(n.options,t=>(d(),u(`div`,{key:t.value,class:m([`flex cursor-pointer items-center justify-between gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground`,{"bg-accent text-accent-foreground":i.modelValue?.groupKey===n.key&&i.modelValue?.value===t.value}]),onClick:e=>L(n.key,t.value)},[l(`span`,O,c(t.label),1),t.tag?(d(),u(`span`,k,c(t.tag),1)):e(``,!0)],10,D))),128))],40,E)):e(``,!0)],42,C))),128)),i.groups.length===0?(d(),u(`div`,A,c(t(y)(`common.noOptions`)),1)):e(``,!0)]),_:1})]),_:1},8,[`open`]))}}),M=f({__name:`CascadingModelSelect`,props:{providers:{},modelValue:{},placeholder:{default:``},compact:{type:Boolean}},emits:[`update:modelValue`],setup(e,{emit:t}){let{t:r}=n(),i=e,a=s(()=>i.placeholder||r(`mappings.selectProviderModel`)),c=t,l=s(()=>i.providers.map(e=>({key:e.provider.id,label:e.provider.name,badge:e.isNew?r(`common.new`):void 0,options:e.models.map(e=>({value:e.name,label:e.name,tag:y(e.contextWindow)}))}))),u=s(()=>i.modelValue?{groupKey:i.modelValue.provider_id,value:i.modelValue.model}:void 0);function f(e){c(`update:modelValue`,{provider_id:e.groupKey,model:e.value})}return(t,n)=>(d(),o(j,{groups:l.value,"model-value":u.value,placeholder:a.value,compact:e.compact,"onUpdate:modelValue":f},null,8,[`groups`,`model-value`,`placeholder`,`compact`]))}});export{M as t};
@@ -1 +1 @@
1
- import{$ as e,Gt as t,Jt as n,K as r,Q as i,X as a,dt as o,gt as s,i as c,it as l,kt as u,m as d,o as f,ot as p,q as m,r as h,rt as g,x as _,xt as v,yt as y}from"./button-mPTFckrZ.js";import{t as b}from"./VisuallyHiddenInput-bxPkTBV5.js";import{t as x}from"./RovingFocusItem-tdat5oCl.js";import{J as S,K as C,R as w,U as T,V as E,X as D,pt as O}from"./index-CrOh8o49.js";function k(e,t){return C(e)?!1:Array.isArray(e)?e.some(e=>D(e,t)):D(e,t)}var[A,j]=S(`CheckboxGroupRoot`);function M(e){return e===`indeterminate`}function N(e){return M(e)?`indeterminate`:e?`checked`:`unchecked`}var[P,F]=S(`CheckboxRoot`),I=l({inheritAttrs:!1,__name:`CheckboxRoot`,props:{defaultValue:{type:null,required:!1},modelValue:{type:null,required:!1,default:void 0},disabled:{type:Boolean,required:!1},value:{type:null,required:!1,default:`on`},id:{type:String,required:!1},trueValue:{type:null,required:!1,default:()=>!0},falseValue:{type:null,required:!1,default:()=>!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`},name:{type:String,required:!1},required:{type:Boolean,required:!1}},emits:[`update:modelValue`],setup(n,{emit:l}){let p=n,h=l,{forwardRef:g,currentElement:_}=f(),S=A(null),w=d(p,`modelValue`,h,{defaultValue:p.defaultValue??p.falseValue,passive:p.modelValue===void 0}),E=a(()=>S?.disabled.value||p.disabled),O=a(()=>D(w.value,p.trueValue)),j=a(()=>C(S?.modelValue.value)?w.value===`indeterminate`?`indeterminate`:O.value:k(S.modelValue.value,p.value));function P(){if(C(S?.modelValue.value))w.value===`indeterminate`?w.value=p.trueValue:w.value=O.value?p.falseValue:p.trueValue;else{let e=[...S.modelValue.value||[]];if(k(e,p.value)){let t=e.findIndex(e=>D(e,p.value));e.splice(t,1)}else e.push(p.value);S.modelValue.value=e}}let I=T(_),L=a(()=>p.id&&_.value?document.querySelector(`[for="${p.id}"]`)?.innerText:void 0);return F({disabled:E,state:j}),(n,a)=>(s(),i(v(t(S)?.rovingFocus.value?t(x):t(c)),o(n.$attrs,{id:n.id,ref:t(g),role:`checkbox`,"as-child":n.asChild,as:n.as,type:n.as===`button`?`button`:void 0,"aria-checked":t(M)(j.value)?`mixed`:j.value,"aria-required":n.required,"aria-label":n.$attrs[`aria-label`]||L.value,"data-state":t(N)(j.value),"data-disabled":E.value?``:void 0,disabled:E.value,focusable:t(S)?.rovingFocus.value?!E.value:void 0,onKeydown:r(m(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:u(()=>[y(n.$slots,`default`,{modelValue:t(w),state:j.value}),t(I)&&n.name&&!t(S)?(s(),i(t(b),{key:0,type:`checkbox`,checked:!!j.value,name:n.name,value:n.value,disabled:E.value,required:n.required},null,8,[`checked`,`name`,`value`,`disabled`,`required`])):e(`v-if`,!0)]),_:3},16,[`id`,`as-child`,`as`,`type`,`aria-checked`,`aria-required`,`aria-label`,`data-state`,`data-disabled`,`disabled`,`focusable`,`onKeydown`]))}}),L=l({__name:`CheckboxIndicator`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`span`}},setup(e){let{forwardRef:n}=f(),r=P();return(e,a)=>(s(),i(t(w),{present:e.forceMount||t(M)(t(r).state.value)||t(r).state.value===!0},{default:u(()=>[g(t(c),o({ref:t(n),"data-state":t(N)(t(r).state.value),"data-disabled":t(r).disabled.value?``:void 0,style:{pointerEvents:`none`},"as-child":e.asChild,as:e.as},e.$attrs),{default:u(()=>[y(e.$slots,`default`)]),_:3},16,[`data-state`,`data-disabled`,`as-child`,`as`])]),_:3},8,[`present`]))}}),R=l({__name:`Checkbox`,props:{defaultValue:{},modelValue:{},disabled:{type:Boolean},value:{},id:{},trueValue:{},falseValue:{},asChild:{type:Boolean},as:{},name:{},required:{type:Boolean},class:{type:[Boolean,null,String,Object,Array]}},emits:[`update:modelValue`],setup(e,{emit:r}){let a=e,c=r,l=E(_(a,`class`),c);return(e,r)=>(s(),i(t(I),o({"data-slot":`checkbox`},t(l),{class:t(h)(`border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-md border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50`,a.class)}),{default:u(r=>[g(t(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:u(()=>[y(e.$slots,`default`,n(p(r)),()=>[g(t(O))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};
1
+ import{$ as e,Gt as t,Jt as n,K as r,Q as i,X as a,dt as o,gt as s,i as c,it as l,kt as u,m as d,o as f,ot as p,q as m,r as h,rt as g,x as _,xt as v,yt as y}from"./button-wn0cCDld.js";import{t as b}from"./VisuallyHiddenInput-BoPTforN.js";import{t as x}from"./RovingFocusItem-CvIxVLbB.js";import{J as S,K as C,R as w,U as T,V as E,X as D,pt as O}from"./index-Bp-3UWSJ.js";function k(e,t){return C(e)?!1:Array.isArray(e)?e.some(e=>D(e,t)):D(e,t)}var[A,j]=S(`CheckboxGroupRoot`);function M(e){return e===`indeterminate`}function N(e){return M(e)?`indeterminate`:e?`checked`:`unchecked`}var[P,F]=S(`CheckboxRoot`),I=l({inheritAttrs:!1,__name:`CheckboxRoot`,props:{defaultValue:{type:null,required:!1},modelValue:{type:null,required:!1,default:void 0},disabled:{type:Boolean,required:!1},value:{type:null,required:!1,default:`on`},id:{type:String,required:!1},trueValue:{type:null,required:!1,default:()=>!0},falseValue:{type:null,required:!1,default:()=>!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`button`},name:{type:String,required:!1},required:{type:Boolean,required:!1}},emits:[`update:modelValue`],setup(n,{emit:l}){let p=n,h=l,{forwardRef:g,currentElement:_}=f(),S=A(null),w=d(p,`modelValue`,h,{defaultValue:p.defaultValue??p.falseValue,passive:p.modelValue===void 0}),E=a(()=>S?.disabled.value||p.disabled),O=a(()=>D(w.value,p.trueValue)),j=a(()=>C(S?.modelValue.value)?w.value===`indeterminate`?`indeterminate`:O.value:k(S.modelValue.value,p.value));function P(){if(C(S?.modelValue.value))w.value===`indeterminate`?w.value=p.trueValue:w.value=O.value?p.falseValue:p.trueValue;else{let e=[...S.modelValue.value||[]];if(k(e,p.value)){let t=e.findIndex(e=>D(e,p.value));e.splice(t,1)}else e.push(p.value);S.modelValue.value=e}}let I=T(_),L=a(()=>p.id&&_.value?document.querySelector(`[for="${p.id}"]`)?.innerText:void 0);return F({disabled:E,state:j}),(n,a)=>(s(),i(v(t(S)?.rovingFocus.value?t(x):t(c)),o(n.$attrs,{id:n.id,ref:t(g),role:`checkbox`,"as-child":n.asChild,as:n.as,type:n.as===`button`?`button`:void 0,"aria-checked":t(M)(j.value)?`mixed`:j.value,"aria-required":n.required,"aria-label":n.$attrs[`aria-label`]||L.value,"data-state":t(N)(j.value),"data-disabled":E.value?``:void 0,disabled:E.value,focusable:t(S)?.rovingFocus.value?!E.value:void 0,onKeydown:r(m(()=>{},[`prevent`]),[`enter`]),onClick:P}),{default:u(()=>[y(n.$slots,`default`,{modelValue:t(w),state:j.value}),t(I)&&n.name&&!t(S)?(s(),i(t(b),{key:0,type:`checkbox`,checked:!!j.value,name:n.name,value:n.value,disabled:E.value,required:n.required},null,8,[`checked`,`name`,`value`,`disabled`,`required`])):e(`v-if`,!0)]),_:3},16,[`id`,`as-child`,`as`,`type`,`aria-checked`,`aria-required`,`aria-label`,`data-state`,`data-disabled`,`disabled`,`focusable`,`onKeydown`]))}}),L=l({__name:`CheckboxIndicator`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:`span`}},setup(e){let{forwardRef:n}=f(),r=P();return(e,a)=>(s(),i(t(w),{present:e.forceMount||t(M)(t(r).state.value)||t(r).state.value===!0},{default:u(()=>[g(t(c),o({ref:t(n),"data-state":t(N)(t(r).state.value),"data-disabled":t(r).disabled.value?``:void 0,style:{pointerEvents:`none`},"as-child":e.asChild,as:e.as},e.$attrs),{default:u(()=>[y(e.$slots,`default`)]),_:3},16,[`data-state`,`data-disabled`,`as-child`,`as`])]),_:3},8,[`present`]))}}),R=l({__name:`Checkbox`,props:{defaultValue:{},modelValue:{},disabled:{type:Boolean},value:{},id:{},trueValue:{},falseValue:{},asChild:{type:Boolean},as:{},name:{},required:{type:Boolean},class:{type:[Boolean,null,String,Object,Array]}},emits:[`update:modelValue`],setup(e,{emit:r}){let a=e,c=r,l=E(_(a,`class`),c);return(e,r)=>(s(),i(t(I),o({"data-slot":`checkbox`},t(l),{class:t(h)(`border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-md border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50`,a.class)}),{default:u(r=>[g(t(L),{"data-slot":`checkbox-indicator`,class:`[&>svg]:size-3.5 grid place-content-center text-current transition-none`},{default:u(()=>[y(e.$slots,`default`,n(p(r)),()=>[g(t(O))])]),_:2},1024)]),_:3},16,[`class`]))}});export{R as t};