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.
- package/dist/proxy/format/adapters/anthropic.js +1 -0
- package/dist/proxy/format/adapters/shared-error-meta.js +1 -0
- package/dist/proxy/format/types.d.ts +1 -1
- package/dist/proxy/handler/create-proxy-handler.js +1 -0
- package/dist/proxy/handler/failover-loop.js +24 -31
- package/dist/proxy/patch/deepseek/patch-orphan-tool-results.d.ts +1 -1
- package/dist/proxy/patch/deepseek/patch-orphan-tool-results.js +24 -37
- package/dist/proxy/patch/index.js +3 -1
- package/dist/proxy/proxy-core.d.ts +3 -1
- package/dist/proxy/proxy-core.js +4 -0
- package/dist/proxy/routing/modality-redirect.d.ts +6 -3
- package/dist/proxy/routing/modality-redirect.js +87 -64
- package/frontend-dist/assets/{CardContent-DWjbiHIJ.js → CardContent-BbiKtIeE.js} +1 -1
- package/frontend-dist/assets/{CardTitle-DbC3CVDt.js → CardTitle-BKzqpj_c.js} +1 -1
- package/frontend-dist/assets/{CascadingModelSelect-D2I-4tJ4.js → CascadingModelSelect-B7lZ1mP0.js} +1 -1
- package/frontend-dist/assets/{Checkbox-CVZOHLQe.js → Checkbox-D1Y3kGGA.js} +1 -1
- package/frontend-dist/assets/{CollapsibleContent-CEHClfca.js → CollapsibleContent-CoARuSxM.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-CIj4JoFP.js → CollapsibleTrigger-D2zhWvtd.js} +1 -1
- package/frontend-dist/assets/{Dashboard-BPVp9LB7.js → Dashboard-CU8be5NR.js} +1 -1
- package/frontend-dist/assets/{Input-Dp2t6aNM.js → Input-BOlb6Kuw.js} +1 -1
- package/frontend-dist/assets/{Label-e5JBm-GF.js → Label-CCZ5BOi9.js} +1 -1
- package/frontend-dist/assets/{Login-Dj_Q5fQN.js → Login-Dux0E-6K.js} +1 -1
- package/frontend-dist/assets/{Logs-QZe87kGr.js → Logs-BDoOY0hr.js} +1 -1
- package/frontend-dist/assets/{MappingEntryEditor-BSiHd-eN.js → MappingEntryEditor-BPqzjEiL.js} +1 -1
- package/frontend-dist/assets/{ModelMappings-DUkirJmq.js → ModelMappings-BT_Cpt-a.js} +1 -1
- package/frontend-dist/assets/{Monitor-CQCms2Mk.js → Monitor-tOgUttQg.js} +1 -1
- package/frontend-dist/assets/{Providers-DMT1vBsA.js → Providers-Dl1g9D6s.js} +1 -1
- package/frontend-dist/assets/{ProxyEnhancement-CajLY_d0.js → ProxyEnhancement-BbBy7nrm.js} +1 -1
- package/frontend-dist/assets/{QuickSetup-DRIWC8Pf.js → QuickSetup-rGmuHGJq.js} +1 -1
- package/frontend-dist/assets/{RetryRules-Br00WS8z.js → RetryRules-DfEaQ1YN.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-e1lyXvx2.js → RouterKeys--rpRgKSn.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-tdat5oCl.js → RovingFocusItem-CvIxVLbB.js} +1 -1
- package/frontend-dist/assets/{Schedules-B8hAohK5.js → Schedules-B7EFQyXL.js} +1 -1
- package/frontend-dist/assets/{Settings-BeHJBE0b.js → Settings-BvKMJ0Qa.js} +1 -1
- package/frontend-dist/assets/{Setup-C6N1kNf0.js → Setup-4vmVajOU.js} +1 -1
- package/frontend-dist/assets/{Switch-BeJMgWUe.js → Switch-D9H1FPsB.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-CDSPqhiB.js → TooltipTrigger-Btj1Vsxm.js} +1 -1
- package/frontend-dist/assets/{TransformRulesForm-VKVF9_SM.js → TransformRulesForm-BJ4caGDj.js} +1 -1
- package/frontend-dist/assets/{UnifiedRequestDialog-BcrdA1If.js → UnifiedRequestDialog-BbQBSoU7.js} +1 -1
- package/frontend-dist/assets/{VisuallyHiddenInput-bxPkTBV5.js → VisuallyHiddenInput-BoPTforN.js} +1 -1
- package/frontend-dist/assets/{button-mPTFckrZ.js → button-wn0cCDld.js} +2 -2
- package/frontend-dist/assets/{copy-CwwALOfh.js → copy-DBpQ3kDg.js} +1 -1
- package/frontend-dist/assets/{dialog-BDefuMDw.js → dialog-DCaFnzS6.js} +1 -1
- package/frontend-dist/assets/{index-CrOh8o49.js → index-Bp-3UWSJ.js} +2 -2
- package/frontend-dist/assets/{model-patches-B4ilmdWE.js → model-patches-BmLrUDyn.js} +1 -1
- package/frontend-dist/assets/plus-BcoVh92Q.js +1 -0
- package/frontend-dist/assets/{sparkles-edZyAh6d.js → sparkles-b5zac545.js} +1 -1
- package/frontend-dist/assets/{trash-2-DIXrJYzm.js → trash-2-BgW_R7hP.js} +1 -1
- package/frontend-dist/assets/{useClipboard-DiphW3z1.js → useClipboard-DWvZAkuV.js} +1 -1
- package/frontend-dist/assets/{useLogRetention-BPMQUvws.js → useLogRetention-DwWdAQWh.js} +1 -1
- package/frontend-dist/index.html +2 -2
- package/package.json +2 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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 消息 →
|
|
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 消息 →
|
|
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
|
-
// ----
|
|
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
|
-
|
|
168
|
-
for (let 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 ===
|
|
174
|
-
|
|
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
|
|
179
|
-
const
|
|
175
|
+
const orphans = [];
|
|
176
|
+
for (const tc of toolCalls) {
|
|
180
177
|
const id = tc.id;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
|
20
|
+
export type { ErrorKind } from "./format/types.js";
|
|
19
21
|
/**
|
|
20
22
|
* 工厂函数,消除 openai/anthropic 错误格式化的重复代码。
|
|
21
23
|
* statusCode 和 message 两个 provider 完全一致,仅 body 格式不同,
|
package/dist/proxy/proxy-core.js
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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
|
|
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
|
-
//
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
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 ===
|
|
110
|
-
const
|
|
111
|
-
const allSupported = [...modalities].every(m => supportsModality(
|
|
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
|
-
|
|
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
|
-
//
|
|
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:
|
|
166
|
+
original_model: firstOriginalModel,
|
|
131
167
|
redirect_to: "",
|
|
132
168
|
redirect_provider: "",
|
|
133
169
|
reason: "no-mapping-group",
|
|
134
170
|
});
|
|
135
|
-
return
|
|
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:
|
|
182
|
+
original_model: firstOriginalModel,
|
|
146
183
|
redirect_to: "",
|
|
147
184
|
redirect_provider: "",
|
|
148
185
|
reason: "rule-parse-error",
|
|
149
186
|
});
|
|
150
|
-
return
|
|
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:
|
|
195
|
+
original_model: firstOriginalModel,
|
|
158
196
|
redirect_to: "",
|
|
159
197
|
redirect_provider: "",
|
|
160
|
-
reason: "no-
|
|
198
|
+
reason: "no-eligible-targets",
|
|
161
199
|
});
|
|
162
|
-
return
|
|
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:
|
|
209
|
+
original_model: firstOriginalModel,
|
|
172
210
|
redirect_to: "",
|
|
173
211
|
redirect_provider: "",
|
|
174
|
-
reason: "
|
|
212
|
+
reason: "no-eligible-targets",
|
|
175
213
|
});
|
|
176
|
-
return
|
|
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:
|
|
222
|
+
original_model: firstOriginalModel,
|
|
185
223
|
redirect_to: fbBackendModel,
|
|
186
224
|
redirect_provider: fbProviderId,
|
|
187
|
-
reason: "
|
|
225
|
+
reason: "no-eligible-targets",
|
|
188
226
|
});
|
|
189
|
-
return
|
|
227
|
+
return [];
|
|
190
228
|
}
|
|
191
|
-
//
|
|
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 =
|
|
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:
|
|
237
|
+
original_model: firstOriginalModel,
|
|
204
238
|
redirect_to: fbBackendModel,
|
|
205
239
|
redirect_provider: fbProviderId,
|
|
206
|
-
reason: "
|
|
240
|
+
reason: "no-eligible-targets",
|
|
207
241
|
detected_modalities: [...modalities],
|
|
208
242
|
});
|
|
209
|
-
return
|
|
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:
|
|
253
|
+
original_model: firstOriginalModel,
|
|
232
254
|
redirect_to: fbBackendModel,
|
|
233
255
|
redirect_provider: fbProviderId,
|
|
234
|
-
reason: "
|
|
256
|
+
reason: "replaced-with-fallback",
|
|
235
257
|
detected_modalities: [...modalities],
|
|
236
258
|
});
|
|
237
|
-
return [fbTarget
|
|
259
|
+
return [fbTarget];
|
|
238
260
|
}
|
|
239
261
|
catch (err) {
|
|
240
|
-
//
|
|
241
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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};
|
package/frontend-dist/assets/{CascadingModelSelect-D2I-4tJ4.js → CascadingModelSelect-B7lZ1mP0.js}
RENAMED
|
@@ -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-
|
|
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-
|
|
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};
|