llm-simple-router 0.11.25 → 0.11.27

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/core/monitor/stream-extractor.js +32 -14
  2. package/dist/proxy/format/adapters/anthropic.js +1 -0
  3. package/dist/proxy/format/adapters/shared-error-meta.js +1 -0
  4. package/dist/proxy/format/types.d.ts +1 -1
  5. package/dist/proxy/handler/create-proxy-handler.js +1 -0
  6. package/dist/proxy/handler/failover-loop.js +24 -31
  7. package/dist/proxy/handler/proxy-handler-utils.js +19 -2
  8. package/dist/proxy/proxy-core.d.ts +3 -1
  9. package/dist/proxy/proxy-core.js +4 -0
  10. package/dist/proxy/routing/modality-redirect.d.ts +6 -3
  11. package/dist/proxy/routing/modality-redirect.js +87 -64
  12. package/frontend-dist/assets/{CardContent-C3yh4PGf.js → CardContent-BLlnppsG.js} +1 -1
  13. package/frontend-dist/assets/{CardTitle-BjctPLMN.js → CardTitle-8HHCqwln.js} +1 -1
  14. package/frontend-dist/assets/{CascadingModelSelect-D9g0YGCD.js → CascadingModelSelect-CrtNasp8.js} +1 -1
  15. package/frontend-dist/assets/{Checkbox-C9AN3_0E.js → Checkbox-B8Vw60jj.js} +1 -1
  16. package/frontend-dist/assets/{CollapsibleContent-Dvc5Q0jk.js → CollapsibleContent-CvvkOB4a.js} +1 -1
  17. package/frontend-dist/assets/{CollapsibleTrigger-COO-0iMJ.js → CollapsibleTrigger-DeMItRLr.js} +1 -1
  18. package/frontend-dist/assets/{Dashboard-CvIsUmiT.js → Dashboard-9dJqc_2w.js} +1 -1
  19. package/frontend-dist/assets/{Input-HMh9MVqW.js → Input-vc5GUwkU.js} +1 -1
  20. package/frontend-dist/assets/{Label-WihZ5JyO.js → Label-FWcCWV4Q.js} +1 -1
  21. package/frontend-dist/assets/{Login-BLfBZ8QI.js → Login-9QkzUJm_.js} +1 -1
  22. package/frontend-dist/assets/{Logs-Cbdbrly6.js → Logs-B2Ywp4Aj.js} +1 -1
  23. package/frontend-dist/assets/{MappingEntryEditor-85XYz-6b.js → MappingEntryEditor-Cx85eUWy.js} +1 -1
  24. package/frontend-dist/assets/{ModelMappings-CL86Xmxc.js → ModelMappings-rUvMpbjc.js} +1 -1
  25. package/frontend-dist/assets/{Monitor-CdPLST7K.js → Monitor-DCo3DSbc.js} +1 -1
  26. package/frontend-dist/assets/{Providers-BS-GHbKg.js → Providers-CwvTWI9Z.js} +1 -1
  27. package/frontend-dist/assets/{ProxyEnhancement-BRRVvv2n.js → ProxyEnhancement-Cn1rEBFB.js} +1 -1
  28. package/frontend-dist/assets/{QuickSetup-BwjL5C3V.js → QuickSetup-CdpVo45V.js} +1 -1
  29. package/frontend-dist/assets/{RetryRules-Br_p5QFd.js → RetryRules-DDJMY1IF.js} +1 -1
  30. package/frontend-dist/assets/{RouterKeys-MVj7WKCD.js → RouterKeys-DMyGtSxS.js} +1 -1
  31. package/frontend-dist/assets/{RovingFocusItem-Cksm8vEW.js → RovingFocusItem-Bx5uXXN2.js} +1 -1
  32. package/frontend-dist/assets/{Schedules-CFRHIUaY.js → Schedules-CDDBsNpW.js} +1 -1
  33. package/frontend-dist/assets/{Settings-Ct2pXj7T.js → Settings-Cr9sYZKR.js} +1 -1
  34. package/frontend-dist/assets/{Setup-6xTd2xao.js → Setup-LSmgyhc3.js} +1 -1
  35. package/frontend-dist/assets/{Switch-B-sHFO9X.js → Switch-DyyEun5Q.js} +1 -1
  36. package/frontend-dist/assets/{TooltipTrigger-Dr21-MXr.js → TooltipTrigger-DCldZMnb.js} +1 -1
  37. package/frontend-dist/assets/{TransformRulesForm-CmFd3-6R.js → TransformRulesForm-Gfvlk0Xk.js} +1 -1
  38. package/frontend-dist/assets/UnifiedRequestDialog-BnrOpRXA.js +3 -0
  39. package/frontend-dist/assets/{VisuallyHiddenInput-ClOeZr8Z.js → VisuallyHiddenInput-Bkpr-VfM.js} +1 -1
  40. package/frontend-dist/assets/{button-D37rLhG3.js → button-CT60E_JD.js} +2 -2
  41. package/frontend-dist/assets/{copy-DbUbyYpk.js → copy-CVYlfrkY.js} +1 -1
  42. package/frontend-dist/assets/{dialog-dnuhN9AI.js → dialog-B4JCV9g7.js} +1 -1
  43. package/frontend-dist/assets/{index-n3QfsdYg.js → index-bGqHF9Js.js} +2 -2
  44. package/frontend-dist/assets/{model-patches-QbKfCo2A.js → model-patches-C-_TDTXT.js} +1 -1
  45. package/frontend-dist/assets/plus-C0-OMLZ3.js +1 -0
  46. package/frontend-dist/assets/{sparkles-CEvoWhC-.js → sparkles-CbfRkkI9.js} +1 -1
  47. package/frontend-dist/assets/{trash-2-NR9QxAhS.js → trash-2-wGNSUXAY.js} +1 -1
  48. package/frontend-dist/assets/{useClipboard-DaZSrO7r.js → useClipboard-Br_VVE9B.js} +1 -1
  49. package/frontend-dist/assets/{useLogRetention-BsuqKeeK.js → useLogRetention-DOuufHTD.js} +1 -1
  50. package/frontend-dist/index.html +2 -2
  51. package/package.json +2 -1
  52. package/frontend-dist/assets/UnifiedRequestDialog-BX3m_-da.js +0 -3
  53. package/frontend-dist/assets/plus-DMzNAXWz.js +0 -1
@@ -3,6 +3,18 @@ const SSE_DATA_PREFIX = "data: ";
3
3
  const OPENAI_BLOCK_REASONING = 0;
4
4
  const OPENAI_BLOCK_TEXT = 1;
5
5
  const OPENAI_BLOCK_TOOLS = 2;
6
+ // Responses SSE 事件类型 → block 类型映射
7
+ // 与 transform/types-responses.ts 的 RESPONSES_SSE_EVENTS 保持同步
8
+ const RESPONSES_DELTA_MAP = {
9
+ "response.output_text.delta": "text",
10
+ "response.function_call_arguments.delta": "tool_use",
11
+ "response.reasoning_summary_text.delta": "thinking",
12
+ "response.reasoning_text.delta": "thinking",
13
+ "response.refusal.delta": "text",
14
+ "response.code_interpreter_call_code.delta": "text",
15
+ };
16
+ // 多种 Provider 的思考内容字段名(按优先级排列)
17
+ const REASONING_FIELDS = ["reasoning_content", "reasoning", "reasoning_text"];
6
18
  export function extractStreamText(line, apiType) {
7
19
  const empty = { text: "", block: null };
8
20
  if (!line.startsWith(SSE_DATA_PREFIX))
@@ -21,7 +33,15 @@ export function extractStreamText(line, apiType) {
21
33
  const choices = obj.choices;
22
34
  const delta = choices?.[0]?.delta;
23
35
  const text = delta?.content ?? "";
24
- const reasoning = delta?.reasoning_content ?? "";
36
+ // 多种 Provider 的思考字段名:reasoning_content(标准)、reasoning、reasoning_text
37
+ let reasoning = "";
38
+ for (const field of REASONING_FIELDS) {
39
+ const val = delta?.[field];
40
+ if (typeof val === "string" && val) {
41
+ reasoning = val;
42
+ break;
43
+ }
44
+ }
25
45
  // OpenAI 不像 Anthropic 那样为不同 content type 分配独立 index。
26
46
  // 策略:reasoning → OPENAI_BLOCK_REASONING, text → OPENAI_BLOCK_TEXT,
27
47
  // tool_calls[N] → OPENAI_BLOCK_TOOLS + N。
@@ -32,6 +52,11 @@ export function extractStreamText(line, apiType) {
32
52
  if (text) {
33
53
  return { text, block: { index: OPENAI_BLOCK_TEXT, type: "text", content: text } };
34
54
  }
55
+ // refusal 降级为 text block(内容审核拒绝原因)
56
+ const refusal = delta?.refusal ?? "";
57
+ if (refusal) {
58
+ return { text: refusal, block: { index: OPENAI_BLOCK_TEXT, type: "text", content: refusal } };
59
+ }
35
60
  const toolCalls = delta?.tool_calls;
36
61
  if (toolCalls) {
37
62
  const tc = toolCalls[0];
@@ -51,20 +76,13 @@ export function extractStreamText(line, apiType) {
51
76
  // Responses SSE uses named events, but line format is "data: {json}" (same as Anthropic)
52
77
  // The event type is in the data JSON's "type" field
53
78
  const type = obj.type;
54
- if (type === "response.output_text.delta") {
55
- const text = obj.delta ?? "";
56
- const outputIndex = obj.output_index ?? 0;
57
- return { text, block: text ? { index: outputIndex, type: "text", content: text } : empty.block };
58
- }
59
- if (type === "response.function_call_arguments.delta") {
60
- const partialJson = obj.delta ?? "";
61
- const outputIndex = obj.output_index ?? 0;
62
- return { text: "", block: { index: outputIndex, type: "tool_use", content: partialJson } };
63
- }
64
- if (type === "response.reasoning_summary_text.delta") {
65
- const thinking = obj.delta ?? "";
79
+ const blockType = RESPONSES_DELTA_MAP[type];
80
+ if (blockType) {
81
+ const delta = obj.delta ?? "";
66
82
  const outputIndex = obj.output_index ?? 0;
67
- return { text: "", block: { index: outputIndex, type: "thinking", content: thinking } };
83
+ if (delta) {
84
+ return { text: blockType === "text" ? delta : "", block: { index: outputIndex, type: blockType, content: delta } };
85
+ }
68
86
  }
69
87
  return empty;
70
88
  }
@@ -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) {
@@ -112,8 +112,25 @@ export function serializeBlocksForStorage(blocks, apiType) {
112
112
  });
113
113
  return JSON.stringify({ content });
114
114
  }
115
- const text = blocks.filter(b => b.type === "text" || b.type === "thinking").map(b => b.content).join("");
116
- return JSON.stringify({ choices: [{ message: { content: text } }] });
115
+ // OpenAI / openai-responses:按类型保留结构,前端 parseOpenAIChoices 可完整解析
116
+ const message = {};
117
+ const thinkingParts = blocks.filter(b => b.type === "thinking");
118
+ const textParts = blocks.filter(b => b.type === "text");
119
+ const toolParts = blocks.filter(b => b.type === "tool_use");
120
+ if (thinkingParts.length > 0) {
121
+ message.reasoning_content = thinkingParts.map(b => b.content).join("");
122
+ }
123
+ if (textParts.length > 0) {
124
+ message.content = textParts.map(b => b.content).join("");
125
+ }
126
+ if (toolParts.length > 0) {
127
+ message.tool_calls = toolParts.map((b, i) => ({
128
+ id: `call_storage_${i}`,
129
+ type: "function",
130
+ function: { name: b.name ?? "", arguments: b.content },
131
+ }));
132
+ }
133
+ return JSON.stringify({ choices: [{ message }] });
117
134
  }
118
135
  /** 从请求体中提取最后一次工具调用记录 */
119
136
  export function extractLastToolUse(body) {
@@ -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-D37rLhG3.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-CT60E_JD.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-D37rLhG3.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-CT60E_JD.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-D37rLhG3.js";import{b as _,ft as v,ht as y,v as b,y as x}from"./index-n3QfsdYg.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-CT60E_JD.js";import{b as _,ft as v,ht as y,v as b,y as x}from"./index-bGqHF9Js.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-D37rLhG3.js";import{t as b}from"./VisuallyHiddenInput-ClOeZr8Z.js";import{t as x}from"./RovingFocusItem-Cksm8vEW.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-n3QfsdYg.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-CT60E_JD.js";import{t as b}from"./VisuallyHiddenInput-Bkpr-VfM.js";import{t as x}from"./RovingFocusItem-Bx5uXXN2.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-bGqHF9Js.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 +1 @@
1
- import{$ as e,Et as t,Gt as n,Ht as r,Jt as i,Lt as a,Q as o,X as s,d as c,dt as l,ft as u,gt as d,i as f,it as p,kt as m,m as h,mt as g,o as _,ot as v,rt as y,yt as b}from"./button-D37rLhG3.js";import{B as x,J as S,R as C,V as w}from"./index-n3QfsdYg.js";var[T,E]=S(`CollapsibleRoot`),D=p({__name:`CollapsibleRoot`,props:{defaultOpen:{type:Boolean,required:!1,default:!1},open:{type:Boolean,required:!1,default:void 0},disabled:{type:Boolean,required:!1},unmountOnHide:{type:Boolean,required:!1,default:!0},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`update:open`],setup(e,{expose:t,emit:i}){let a=e,s=h(a,`open`,i,{defaultValue:a.defaultOpen,passive:a.open===void 0}),{disabled:c,unmountOnHide:l}=r(a);return E({contentId:``,disabled:c,open:s,unmountOnHide:l,onOpenToggle:()=>{c.value||(s.value=!s.value)}}),t({open:s}),_(),(e,t)=>(d(),o(n(f),{as:e.as,"as-child":a.asChild,"data-state":n(s)?`open`:`closed`,"data-disabled":n(c)?``:void 0},{default:m(()=>[b(e.$slots,`default`,{open:n(s)})]),_:3},8,[`as`,`as-child`,`data-state`,`data-disabled`]))}}),O=p({inheritAttrs:!1,__name:`CollapsibleContent`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`contentFound`],setup(r,{emit:i}){let p=r,h=i,v=T();v.contentId||=x(void 0,`reka-collapsible-content`);let S=a(),{forwardRef:w,currentElement:E}=_(),D=a(0),O=a(0),k=s(()=>v.open.value),A=a(k.value),j=a();t(()=>[k.value,S.value?.present],async()=>{await u();let e=E.value;if(!e)return;j.value=j.value||{transitionDuration:e.style.transitionDuration,animationName:e.style.animationName},e.style.transitionDuration=`0s`,e.style.animationName=`none`;let t=e.getBoundingClientRect();O.value=t.height,D.value=t.width,A.value||(e.style.transitionDuration=j.value.transitionDuration,e.style.animationName=j.value.animationName)},{immediate:!0});let M=s(()=>A.value&&v.open.value);return g(()=>{requestAnimationFrame(()=>{A.value=!1})}),c(E,`beforematch`,e=>{requestAnimationFrame(()=>{v.onOpenToggle(),h(`contentFound`)})}),(t,r)=>(d(),o(n(C),{ref_key:`presentRef`,ref:S,present:t.forceMount||n(v).open.value,"force-mount":!0},{default:m(({present:r})=>[y(n(f),l(t.$attrs,{id:n(v).contentId,ref:n(w),"as-child":p.asChild,as:t.as,hidden:r?void 0:n(v).unmountOnHide.value?``:`until-found`,"data-state":M.value?void 0:n(v).open.value?`open`:`closed`,"data-disabled":n(v).disabled?.value?``:void 0,style:{"--reka-collapsible-content-height":`${O.value}px`,"--reka-collapsible-content-width":`${D.value}px`}}),{default:m(()=>[!n(v).unmountOnHide.value||r?b(t.$slots,`default`,{key:0}):e(`v-if`,!0)]),_:2},1040,[`id`,`as-child`,`as`,`hidden`,`data-state`,`data-disabled`,`style`])]),_:3},8,[`present`]))}}),k=p({__name:`Collapsible`,props:{defaultOpen:{type:Boolean},open:{type:Boolean},disabled:{type:Boolean},unmountOnHide:{type:Boolean},asChild:{type:Boolean},as:{}},emits:[`update:open`],setup(e,{emit:t}){let r=w(e,t);return(e,t)=>(d(),o(n(D),l({"data-slot":`collapsible`},n(r)),{default:m(t=>[b(e.$slots,`default`,i(v(t)))]),_:3},16))}}),A=p({__name:`CollapsibleContent`,props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{}},setup(e){let t=e;return(e,r)=>(d(),o(n(O),l({"data-slot":`collapsible-content`},t),{default:m(()=>[b(e.$slots,`default`)]),_:3},16))}});export{k as n,T as r,A as t};
1
+ import{$ as e,Et as t,Gt as n,Ht as r,Jt as i,Lt as a,Q as o,X as s,d as c,dt as l,ft as u,gt as d,i as f,it as p,kt as m,m as h,mt as g,o as _,ot as v,rt as y,yt as b}from"./button-CT60E_JD.js";import{B as x,J as S,R as C,V as w}from"./index-bGqHF9Js.js";var[T,E]=S(`CollapsibleRoot`),D=p({__name:`CollapsibleRoot`,props:{defaultOpen:{type:Boolean,required:!1,default:!1},open:{type:Boolean,required:!1,default:void 0},disabled:{type:Boolean,required:!1},unmountOnHide:{type:Boolean,required:!1,default:!0},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`update:open`],setup(e,{expose:t,emit:i}){let a=e,s=h(a,`open`,i,{defaultValue:a.defaultOpen,passive:a.open===void 0}),{disabled:c,unmountOnHide:l}=r(a);return E({contentId:``,disabled:c,open:s,unmountOnHide:l,onOpenToggle:()=>{c.value||(s.value=!s.value)}}),t({open:s}),_(),(e,t)=>(d(),o(n(f),{as:e.as,"as-child":a.asChild,"data-state":n(s)?`open`:`closed`,"data-disabled":n(c)?``:void 0},{default:m(()=>[b(e.$slots,`default`,{open:n(s)})]),_:3},8,[`as`,`as-child`,`data-state`,`data-disabled`]))}}),O=p({inheritAttrs:!1,__name:`CollapsibleContent`,props:{forceMount:{type:Boolean,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1}},emits:[`contentFound`],setup(r,{emit:i}){let p=r,h=i,v=T();v.contentId||=x(void 0,`reka-collapsible-content`);let S=a(),{forwardRef:w,currentElement:E}=_(),D=a(0),O=a(0),k=s(()=>v.open.value),A=a(k.value),j=a();t(()=>[k.value,S.value?.present],async()=>{await u();let e=E.value;if(!e)return;j.value=j.value||{transitionDuration:e.style.transitionDuration,animationName:e.style.animationName},e.style.transitionDuration=`0s`,e.style.animationName=`none`;let t=e.getBoundingClientRect();O.value=t.height,D.value=t.width,A.value||(e.style.transitionDuration=j.value.transitionDuration,e.style.animationName=j.value.animationName)},{immediate:!0});let M=s(()=>A.value&&v.open.value);return g(()=>{requestAnimationFrame(()=>{A.value=!1})}),c(E,`beforematch`,e=>{requestAnimationFrame(()=>{v.onOpenToggle(),h(`contentFound`)})}),(t,r)=>(d(),o(n(C),{ref_key:`presentRef`,ref:S,present:t.forceMount||n(v).open.value,"force-mount":!0},{default:m(({present:r})=>[y(n(f),l(t.$attrs,{id:n(v).contentId,ref:n(w),"as-child":p.asChild,as:t.as,hidden:r?void 0:n(v).unmountOnHide.value?``:`until-found`,"data-state":M.value?void 0:n(v).open.value?`open`:`closed`,"data-disabled":n(v).disabled?.value?``:void 0,style:{"--reka-collapsible-content-height":`${O.value}px`,"--reka-collapsible-content-width":`${D.value}px`}}),{default:m(()=>[!n(v).unmountOnHide.value||r?b(t.$slots,`default`,{key:0}):e(`v-if`,!0)]),_:2},1040,[`id`,`as-child`,`as`,`hidden`,`data-state`,`data-disabled`,`style`])]),_:3},8,[`present`]))}}),k=p({__name:`Collapsible`,props:{defaultOpen:{type:Boolean},open:{type:Boolean},disabled:{type:Boolean},unmountOnHide:{type:Boolean},asChild:{type:Boolean},as:{}},emits:[`update:open`],setup(e,{emit:t}){let r=w(e,t);return(e,t)=>(d(),o(n(D),l({"data-slot":`collapsible`},n(r)),{default:m(t=>[b(e.$slots,`default`,i(v(t)))]),_:3},16))}}),A=p({__name:`CollapsibleContent`,props:{forceMount:{type:Boolean},asChild:{type:Boolean},as:{}},setup(e){let t=e;return(e,r)=>(d(),o(n(O),l({"data-slot":`collapsible-content`},t),{default:m(()=>[b(e.$slots,`default`)]),_:3},16))}});export{k as n,T as r,A as t};