llm-simple-router 0.6.3 → 0.6.4
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/config.d.ts +2 -0
- package/dist/config.js +5 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/index.js +14 -0
- package/dist/metrics/sse-metrics-transform.d.ts +11 -0
- package/dist/metrics/sse-metrics-transform.js +47 -1
- package/dist/proxy/anthropic.d.ts +1 -0
- package/dist/proxy/anthropic.js +2 -2
- package/dist/proxy/loop-prevention/detectors/detector.d.ts +10 -0
- package/dist/proxy/loop-prevention/detectors/detector.js +1 -0
- package/dist/proxy/loop-prevention/detectors/ngram-detector.d.ts +15 -0
- package/dist/proxy/loop-prevention/detectors/ngram-detector.js +65 -0
- package/dist/proxy/loop-prevention/session-tracker.d.ts +14 -0
- package/dist/proxy/loop-prevention/session-tracker.js +67 -0
- package/dist/proxy/loop-prevention/stream-loop-guard.d.ts +12 -0
- package/dist/proxy/loop-prevention/stream-loop-guard.js +28 -0
- package/dist/proxy/loop-prevention/tool-loop-guard.d.ts +13 -0
- package/dist/proxy/loop-prevention/tool-loop-guard.js +63 -0
- package/dist/proxy/loop-prevention/types.d.ts +38 -0
- package/dist/proxy/loop-prevention/types.js +18 -0
- package/dist/proxy/openai.d.ts +1 -0
- package/dist/proxy/openai.js +2 -2
- package/dist/proxy/patch/deepseek/index.d.ts +1 -1
- package/dist/proxy/patch/deepseek/index.js +3 -3
- package/dist/proxy/patch/deepseek/patch-orphan-tool-results.d.ts +3 -4
- package/dist/proxy/patch/deepseek/patch-orphan-tool-results.js +23 -30
- package/dist/proxy/patch/deepseek/patch-thinking-blocks.d.ts +8 -4
- package/dist/proxy/patch/deepseek/patch-thinking-blocks.js +57 -12
- package/dist/proxy/patch/index.d.ts +12 -3
- package/dist/proxy/patch/index.js +13 -4
- package/dist/proxy/patch/router-cleanup.d.ts +13 -0
- package/dist/proxy/patch/router-cleanup.js +87 -0
- package/dist/proxy/proxy-handler.d.ts +1 -0
- package/dist/proxy/proxy-handler.js +88 -2
- package/dist/proxy/stream-proxy.d.ts +2 -1
- package/dist/proxy/stream-proxy.js +11 -3
- package/dist/proxy/transport-fn.d.ts +2 -0
- package/dist/proxy/transport-fn.js +19 -1
- package/frontend-dist/assets/{CardContent-BtAcFNMy.js → CardContent-CpiBn1Oc.js} +1 -1
- package/frontend-dist/assets/{CardTitle-Bmwf1S5Y.js → CardTitle-dwtgd_nl.js} +1 -1
- package/frontend-dist/assets/{CascadingModelSelect-CicfrqcY.js → CascadingModelSelect-CxEXwaeM.js} +1 -1
- package/frontend-dist/assets/{Checkbox-B1o39YuC.js → Checkbox-D2U4I-pO.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-2jySTCeh.js → CollapsibleTrigger-B2AdbZBh.js} +1 -1
- package/frontend-dist/assets/{Collection-ChUVejsh.js → Collection-BJZSFJsF.js} +1 -1
- package/frontend-dist/assets/{Dashboard-DkJauxYu.js → Dashboard-D3cDhJNh.js} +1 -1
- package/frontend-dist/assets/{DialogTitle-D0erB-Fr.js → DialogTitle-BTuQdRm1.js} +1 -1
- package/frontend-dist/assets/{Input-BDbKynVD.js → Input-BYULYPCe.js} +1 -1
- package/frontend-dist/assets/{Label-CrHq5hrg.js → Label-sImW5XUw.js} +1 -1
- package/frontend-dist/assets/{Login-D2YdqYnu.js → Login-B0kGGZFi.js} +1 -1
- package/frontend-dist/assets/{Logs-DgeOPIkd.js → Logs-BpAeeJRi.js} +1 -1
- package/frontend-dist/assets/{ModelMappings-De_UjiND.js → ModelMappings-CsHLYqQB.js} +1 -1
- package/frontend-dist/assets/{Monitor-BgRMReMF.js → Monitor-BmMFWFJg.js} +1 -1
- package/frontend-dist/assets/{PopoverTrigger-BVsxIE2L.js → PopoverTrigger-CIN3yOIw.js} +1 -1
- package/frontend-dist/assets/{PopperContent-B23SzU9H.js → PopperContent-BoOYHCag.js} +1 -1
- package/frontend-dist/assets/{Providers-DQypvsEg.js → Providers-DrepCc4A.js} +1 -1
- package/frontend-dist/assets/{ProxyEnhancement-Cijb2FID.js → ProxyEnhancement-dK_mOQ3m.js} +1 -1
- package/frontend-dist/assets/{RetryRules-CSseSPoO.js → RetryRules-Ce5HfNcc.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-ccwqoMCX.js → RouterKeys-CeSGvjll.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-rwA4uA9N.js → RovingFocusItem-gAYs0l8Z.js} +1 -1
- package/frontend-dist/assets/{Schedules-8YYNjLNo.js → Schedules-DphkPAWD.js} +1 -1
- package/frontend-dist/assets/{SelectValue-TvIOOalu.js → SelectValue-Cbbd2Xbm.js} +1 -1
- package/frontend-dist/assets/{Settings-D1WDm5lQ.js → Settings-DIP7VawX.js} +1 -1
- package/frontend-dist/assets/{Setup-Bw-RIF9G.js → Setup-_d_M-Qi6.js} +1 -1
- package/frontend-dist/assets/{Switch-D9wFEsMF.js → Switch-BmmYsqAx.js} +1 -1
- package/frontend-dist/assets/{TableHeader-HOR173Xk.js → TableHeader-C1mpCsyo.js} +1 -1
- package/frontend-dist/assets/{TabsTrigger-BOsmgFYE.js → TabsTrigger-kN1usMvC.js} +1 -1
- package/frontend-dist/assets/{Teleport-BGbwtNTD.js → Teleport-CRn-gy0B.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-DPzNY2Sp.js → TooltipTrigger-rlKo7E3A.js} +1 -1
- package/frontend-dist/assets/UnifiedRequestDialog-C-Ui-fav.css +1 -0
- package/frontend-dist/assets/UnifiedRequestDialog-C7pEDa9D.js +4 -0
- package/frontend-dist/assets/{VisuallyHidden-ChauvWtH.js → VisuallyHidden-CQvCw9gB.js} +1 -1
- package/frontend-dist/assets/{VisuallyHiddenInput-BcDuL0V8.js → VisuallyHiddenInput-B-DnnaWN.js} +1 -1
- package/frontend-dist/assets/{alert-dialog-DgtxmV7t.js → alert-dialog-M3PhwD75.js} +1 -1
- package/frontend-dist/assets/arrow-down-BHq-drH-.js +1 -0
- package/frontend-dist/assets/{badge-NUBqZBxu.js → badge-7WIbGMsE.js} +1 -1
- package/frontend-dist/assets/{button-BLX8zWc1.js → button-q9xTxPJh.js} +2 -2
- package/frontend-dist/assets/check-CccuM1Sj.js +1 -0
- package/frontend-dist/assets/{copy-BMWzukd1.js → copy-C1a8OrYP.js} +1 -1
- package/frontend-dist/assets/{dialog-Dsvgfiw-.js → dialog-B6RnMgGx.js} +1 -1
- package/frontend-dist/assets/{file-text-CqJ33eWr.js → file-text-CsuQUjXR.js} +1 -1
- package/frontend-dist/assets/index-Ce7hFHTt.css +1 -0
- package/frontend-dist/assets/{index-C7LWG0FU.js → index-Dh7qL0Qt.js} +1 -1
- package/frontend-dist/assets/{lib-DQotd1d8.js → lib-D1G8Xa05.js} +1 -1
- package/frontend-dist/assets/loader-circle-CVhxR0Tt.js +1 -0
- package/frontend-dist/assets/{useClipboard-BDAhyrgL.js → useClipboard-CBONMfzU.js} +1 -1
- package/frontend-dist/assets/{useFocusGuards-CHAbXhQp.js → useFocusGuards-DwFX8o1a.js} +1 -1
- package/frontend-dist/assets/useFormControl-DC32gW1A.js +1 -0
- package/frontend-dist/assets/{useLogRetention-X-CkHhJ7.js → useLogRetention-C2IbjXjr.js} +1 -1
- package/frontend-dist/assets/useNonce-t7XaR4bX.js +1 -0
- package/frontend-dist/assets/x-Bq5KcbWI.js +1 -0
- package/frontend-dist/index.html +20 -20
- package/package.json +1 -1
- package/frontend-dist/assets/UnifiedRequestDialog-BjEigSaR.css +0 -1
- package/frontend-dist/assets/UnifiedRequestDialog-CevmD2P2.js +0 -3
- package/frontend-dist/assets/arrow-down-BuK6B6yc.js +0 -1
- package/frontend-dist/assets/check-CsZv9cnK.js +0 -1
- package/frontend-dist/assets/index-_Icfkt3I.css +0 -1
- package/frontend-dist/assets/loader-circle-CnEL8ILi.js +0 -1
- package/frontend-dist/assets/useFormControl-CCVkIi3o.js +0 -1
- package/frontend-dist/assets/useNonce-DyF1ycZV.js +0 -1
- package/frontend-dist/assets/x-DMAovOe-.js +0 -1
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* 算法:
|
|
6
6
|
* 1. 收集所有 assistant 消息中的 tool_use ID
|
|
7
|
-
* 2.
|
|
8
|
-
* 3.
|
|
9
|
-
* 4. 合并相邻的
|
|
10
|
-
* 5. 合并相邻的 assistant 消息(同理)
|
|
7
|
+
* 2. 将 tool_use_id 不在集合中的 tool_result 块转为 text(保留信息)
|
|
8
|
+
* 3. 合并相邻的 user 消息(Anthropic API 不允许连续 user 消息)
|
|
9
|
+
* 4. 合并相邻的 assistant 消息(同理)
|
|
11
10
|
*/
|
|
12
11
|
export declare function patchOrphanToolResults(body: Record<string, unknown>): void;
|
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* 算法:
|
|
6
6
|
* 1. 收集所有 assistant 消息中的 tool_use ID
|
|
7
|
-
* 2.
|
|
8
|
-
* 3.
|
|
9
|
-
* 4. 合并相邻的
|
|
10
|
-
* 5. 合并相邻的 assistant 消息(同理)
|
|
7
|
+
* 2. 将 tool_use_id 不在集合中的 tool_result 块转为 text(保留信息)
|
|
8
|
+
* 3. 合并相邻的 user 消息(Anthropic API 不允许连续 user 消息)
|
|
9
|
+
* 4. 合并相邻的 assistant 消息(同理)
|
|
11
10
|
*/
|
|
12
11
|
export function patchOrphanToolResults(body) {
|
|
13
12
|
if (!body.messages)
|
|
@@ -26,41 +25,35 @@ export function patchOrphanToolResults(body) {
|
|
|
26
25
|
}
|
|
27
26
|
}
|
|
28
27
|
}
|
|
29
|
-
// Step 2:
|
|
30
|
-
let
|
|
28
|
+
// Step 2: 将孤儿 tool_result 块转为 text,而非丢弃
|
|
29
|
+
let convertedAny = false;
|
|
31
30
|
for (const msg of messages) {
|
|
32
31
|
if (msg.role !== "user" || !Array.isArray(msg.content))
|
|
33
32
|
continue;
|
|
34
33
|
const blocks = msg.content;
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
const newBlocks = [];
|
|
35
|
+
let changed = false;
|
|
36
|
+
for (const block of blocks) {
|
|
37
|
+
if (block?.type === "tool_result" &&
|
|
38
|
+
typeof block.tool_use_id === "string" &&
|
|
39
|
+
!knownToolUseIds.has(block.tool_use_id)) {
|
|
40
|
+
newBlocks.push({ type: "text", text: JSON.stringify(block) });
|
|
41
|
+
changed = true;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
newBlocks.push(block);
|
|
39
45
|
}
|
|
40
|
-
return true;
|
|
41
|
-
});
|
|
42
|
-
if (filtered.length < before) {
|
|
43
|
-
msg.content = filtered;
|
|
44
|
-
removedAny = true;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
if (!removedAny)
|
|
48
|
-
return;
|
|
49
|
-
// Step 3: 移除清空后的空 user 消息(向后遍历避免索引错乱)
|
|
50
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
51
|
-
const msg = messages[i];
|
|
52
|
-
if (msg.role !== "user")
|
|
53
|
-
continue;
|
|
54
|
-
if (Array.isArray(msg.content) && msg.content.length === 0) {
|
|
55
|
-
messages.splice(i, 1);
|
|
56
46
|
}
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
if (changed) {
|
|
48
|
+
msg.content = newBlocks;
|
|
49
|
+
convertedAny = true;
|
|
59
50
|
}
|
|
60
51
|
}
|
|
61
|
-
|
|
52
|
+
if (!convertedAny)
|
|
53
|
+
return;
|
|
54
|
+
// Step 3: 合并相邻的 user 消息(转换后可能产生连续 user 消息)
|
|
62
55
|
mergeConsecutive(messages, "user");
|
|
63
|
-
// Step
|
|
56
|
+
// Step 4: 合并相邻的 assistant 消息
|
|
64
57
|
mergeConsecutive(messages, "assistant");
|
|
65
58
|
}
|
|
66
59
|
function mergeConsecutive(messages, role) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DeepSeek
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* 将非 DeepSeek 生成的消息中的 tool_use/tool_result 转为 text 块。
|
|
3
|
+
*
|
|
4
|
+
* 判断标准:assistant 消息含 tool_use 但(无 thinking 块或 thinking.signature 为空/缺失)。
|
|
5
|
+
*
|
|
6
|
+
* 背景:DeepSeek Anthropic API 开启 thinking 后要求含 tool_use 的 assistant 消息
|
|
7
|
+
* 必须携带 thinking 块。跨模型切换(GLM → DeepSeek)时历史消息缺 thinking 会导致 400。
|
|
8
|
+
* 转化为 text 规避格式校验,同时完整保留信息。
|
|
5
9
|
*/
|
|
6
|
-
export declare function
|
|
10
|
+
export declare function patchNonDeepSeekToolMessages(body: Record<string, unknown>): void;
|
|
@@ -1,24 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DeepSeek
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* 将非 DeepSeek 生成的消息中的 tool_use/tool_result 转为 text 块。
|
|
3
|
+
*
|
|
4
|
+
* 判断标准:assistant 消息含 tool_use 但(无 thinking 块或 thinking.signature 为空/缺失)。
|
|
5
|
+
*
|
|
6
|
+
* 背景:DeepSeek Anthropic API 开启 thinking 后要求含 tool_use 的 assistant 消息
|
|
7
|
+
* 必须携带 thinking 块。跨模型切换(GLM → DeepSeek)时历史消息缺 thinking 会导致 400。
|
|
8
|
+
* 转化为 text 规避格式校验,同时完整保留信息。
|
|
5
9
|
*/
|
|
6
|
-
export function
|
|
10
|
+
export function patchNonDeepSeekToolMessages(body) {
|
|
7
11
|
if (!body.messages)
|
|
8
12
|
return;
|
|
9
13
|
const messages = body.messages;
|
|
10
|
-
|
|
11
|
-
// 所以只要历史中存在任何 thinking block,就视为 thinking 模式激活。
|
|
12
|
-
const thinkingActive = !!body.thinking || messages.some((msg) => msg.role === "assistant" && Array.isArray(msg.content)
|
|
13
|
-
&& msg.content.some((b) => b && typeof b === "object" && b.type === "thinking"));
|
|
14
|
-
if (!thinkingActive)
|
|
14
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
15
15
|
return;
|
|
16
|
+
// Step 1: 识别非 DeepSeek 的 assistant 消息,转换 tool_use → text
|
|
17
|
+
const convertedIds = new Set();
|
|
16
18
|
for (const msg of messages) {
|
|
17
19
|
if (msg.role !== "assistant" || !Array.isArray(msg.content))
|
|
18
20
|
continue;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
const blocks = msg.content;
|
|
22
|
+
const hasToolUse = blocks.some((b) => b && typeof b === "object" && b.type === "tool_use");
|
|
23
|
+
if (!hasToolUse)
|
|
24
|
+
continue;
|
|
25
|
+
const thinkingBlock = blocks.find((b) => b && typeof b === "object" && b.type === "thinking");
|
|
26
|
+
const hasValidSignature = thinkingBlock &&
|
|
27
|
+
typeof thinkingBlock.signature === "string" &&
|
|
28
|
+
thinkingBlock.signature !== "";
|
|
29
|
+
// 有合法 signature → DeepSeek 原生,不动
|
|
30
|
+
if (hasValidSignature)
|
|
31
|
+
continue;
|
|
32
|
+
// 非 DeepSeek:将 tool_use 块替换为 text
|
|
33
|
+
const newBlocks = [];
|
|
34
|
+
for (const block of blocks) {
|
|
35
|
+
if (block && typeof block === "object" && block.type === "tool_use") {
|
|
36
|
+
newBlocks.push({ type: "text", text: JSON.stringify(block) });
|
|
37
|
+
if (typeof block.id === "string") {
|
|
38
|
+
convertedIds.add(block.id);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
newBlocks.push(block);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
msg.content = newBlocks;
|
|
46
|
+
}
|
|
47
|
+
if (convertedIds.size === 0)
|
|
48
|
+
return;
|
|
49
|
+
// Step 2: 转换对应 user 消息中的 tool_result → text
|
|
50
|
+
for (const msg of messages) {
|
|
51
|
+
if (msg.role !== "user" || !Array.isArray(msg.content))
|
|
52
|
+
continue;
|
|
53
|
+
const blocks = msg.content;
|
|
54
|
+
const newBlocks = [];
|
|
55
|
+
for (const block of blocks) {
|
|
56
|
+
if (block &&
|
|
57
|
+
typeof block === "object" &&
|
|
58
|
+
block.type === "tool_result" &&
|
|
59
|
+
typeof block.tool_use_id === "string" &&
|
|
60
|
+
convertedIds.has(block.tool_use_id)) {
|
|
61
|
+
newBlocks.push({ type: "text", text: JSON.stringify(block) });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
newBlocks.push(block);
|
|
65
|
+
}
|
|
22
66
|
}
|
|
67
|
+
msg.content = newBlocks;
|
|
23
68
|
}
|
|
24
69
|
}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
interface ProviderInfo {
|
|
2
2
|
base_url: string;
|
|
3
3
|
}
|
|
4
|
+
export interface ProviderPatchMeta {
|
|
5
|
+
types: string[];
|
|
6
|
+
}
|
|
4
7
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
8
|
+
* 通用消息补丁入口。
|
|
9
|
+
* 返回新的 body(必要时深拷贝),不修改原始 body。
|
|
10
|
+
* 执行顺序:
|
|
11
|
+
* 1. 清理 router 合成的 tool_use/tool_result(通用,所有 provider)
|
|
12
|
+
* 2. Provider-specific patches(如 DeepSeek thinking 校验)
|
|
7
13
|
*/
|
|
8
|
-
export declare function applyProviderPatches(body: Record<string, unknown>, provider: ProviderInfo):
|
|
14
|
+
export declare function applyProviderPatches(body: Record<string, unknown>, provider: ProviderInfo): {
|
|
15
|
+
body: Record<string, unknown>;
|
|
16
|
+
meta: ProviderPatchMeta;
|
|
17
|
+
};
|
|
9
18
|
export {};
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { applyDeepSeekPatches } from "./deepseek/index.js";
|
|
2
|
+
import { patchRouterSyntheticToolCalls } from "./router-cleanup.js";
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
4
|
+
* 通用消息补丁入口。
|
|
5
|
+
* 返回新的 body(必要时深拷贝),不修改原始 body。
|
|
6
|
+
* 执行顺序:
|
|
7
|
+
* 1. 清理 router 合成的 tool_use/tool_result(通用,所有 provider)
|
|
8
|
+
* 2. Provider-specific patches(如 DeepSeek thinking 校验)
|
|
5
9
|
*/
|
|
6
10
|
export function applyProviderPatches(body, provider) {
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
// router cleanup 始终执行,先克隆以避免修改原始 body
|
|
12
|
+
const cloned = JSON.parse(JSON.stringify(body));
|
|
13
|
+
patchRouterSyntheticToolCalls(cloned);
|
|
14
|
+
if (!needsDeepSeekPatch(body, provider)) {
|
|
15
|
+
return { body: cloned, meta: { types: [] } };
|
|
9
16
|
}
|
|
17
|
+
applyDeepSeekPatches(cloned);
|
|
18
|
+
return { body: cloned, meta: { types: ["deepseek_tool_use_to_text"] } };
|
|
10
19
|
}
|
|
11
20
|
/** DeepSeek patch 触发条件:直连 DeepSeek,或经代理转发且模型名含 deepseek */
|
|
12
21
|
function needsDeepSeekPatch(body, provider) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从消息中移除 router 注入的合成 tool_use/tool_result。
|
|
3
|
+
*
|
|
4
|
+
* Router 通过 AskUserQuestion 注入的 tool_use(ID 以 toolu_router_ 开头)是给客户端
|
|
5
|
+
* UI 用的交互机制,不是任何 LLM 生成的内容,不应出现在发送给上游 provider 的上下文中。
|
|
6
|
+
*
|
|
7
|
+
* 处理步骤:
|
|
8
|
+
* 1. 移除 assistant 消息中的 router 合成 tool_use 块
|
|
9
|
+
* 2. 移除 user 消息中对应的 tool_result 块
|
|
10
|
+
* 3. 移除内容为空的消息
|
|
11
|
+
* 4. 合并连续的同角色消息
|
|
12
|
+
*/
|
|
13
|
+
export declare function patchRouterSyntheticToolCalls(body: Record<string, unknown>): void;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { TOOL_USE_ID_PREFIX } from "../enhancement/directive-parser.js";
|
|
2
|
+
/**
|
|
3
|
+
* 从消息中移除 router 注入的合成 tool_use/tool_result。
|
|
4
|
+
*
|
|
5
|
+
* Router 通过 AskUserQuestion 注入的 tool_use(ID 以 toolu_router_ 开头)是给客户端
|
|
6
|
+
* UI 用的交互机制,不是任何 LLM 生成的内容,不应出现在发送给上游 provider 的上下文中。
|
|
7
|
+
*
|
|
8
|
+
* 处理步骤:
|
|
9
|
+
* 1. 移除 assistant 消息中的 router 合成 tool_use 块
|
|
10
|
+
* 2. 移除 user 消息中对应的 tool_result 块
|
|
11
|
+
* 3. 移除内容为空的消息
|
|
12
|
+
* 4. 合并连续的同角色消息
|
|
13
|
+
*/
|
|
14
|
+
export function patchRouterSyntheticToolCalls(body) {
|
|
15
|
+
if (!body.messages)
|
|
16
|
+
return;
|
|
17
|
+
const messages = body.messages;
|
|
18
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
19
|
+
return;
|
|
20
|
+
// Step 1: 收集 router 合成的 tool_use ID,移除这些块
|
|
21
|
+
const removedIds = new Set();
|
|
22
|
+
for (const msg of messages) {
|
|
23
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.content))
|
|
24
|
+
continue;
|
|
25
|
+
const blocks = msg.content;
|
|
26
|
+
const filtered = blocks.filter((block) => {
|
|
27
|
+
if (block?.type === "tool_use" &&
|
|
28
|
+
typeof block.id === "string" &&
|
|
29
|
+
block.id.startsWith(TOOL_USE_ID_PREFIX)) {
|
|
30
|
+
removedIds.add(block.id);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
});
|
|
35
|
+
if (filtered.length !== blocks.length)
|
|
36
|
+
msg.content = filtered;
|
|
37
|
+
}
|
|
38
|
+
if (removedIds.size === 0)
|
|
39
|
+
return;
|
|
40
|
+
// Step 2: 移除对应的 tool_result
|
|
41
|
+
for (const msg of messages) {
|
|
42
|
+
if (msg.role !== "user" || !Array.isArray(msg.content))
|
|
43
|
+
continue;
|
|
44
|
+
const blocks = msg.content;
|
|
45
|
+
const filtered = blocks.filter((block) => !(block?.type === "tool_result" &&
|
|
46
|
+
typeof block.tool_use_id === "string" &&
|
|
47
|
+
removedIds.has(block.tool_use_id)));
|
|
48
|
+
if (filtered.length !== blocks.length)
|
|
49
|
+
msg.content = filtered;
|
|
50
|
+
}
|
|
51
|
+
// Step 3: 移除内容为空的消息
|
|
52
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
53
|
+
const content = messages[i].content;
|
|
54
|
+
const empty = content == null ||
|
|
55
|
+
content === "" ||
|
|
56
|
+
(Array.isArray(content) && content.length === 0);
|
|
57
|
+
if (empty)
|
|
58
|
+
messages.splice(i, 1);
|
|
59
|
+
}
|
|
60
|
+
// Step 4: 合并连续的同角色消息
|
|
61
|
+
mergeConsecutive(messages, "user");
|
|
62
|
+
mergeConsecutive(messages, "assistant");
|
|
63
|
+
}
|
|
64
|
+
function mergeConsecutive(messages, role) {
|
|
65
|
+
let i = 1;
|
|
66
|
+
while (i < messages.length) {
|
|
67
|
+
if (messages[i].role === role && messages[i - 1].role === role) {
|
|
68
|
+
const prev = messages[i - 1];
|
|
69
|
+
const curr = messages[i];
|
|
70
|
+
prev.content = [
|
|
71
|
+
...normalizeToArray(prev.content),
|
|
72
|
+
...normalizeToArray(curr.content),
|
|
73
|
+
];
|
|
74
|
+
messages.splice(i, 1);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function normalizeToArray(content) {
|
|
82
|
+
if (Array.isArray(content))
|
|
83
|
+
return content;
|
|
84
|
+
if (typeof content === "string")
|
|
85
|
+
return [{ type: "text", text: content }];
|
|
86
|
+
return [{ type: "text", text: String(content ?? "") }];
|
|
87
|
+
}
|
|
@@ -12,6 +12,7 @@ export interface RouteHandlerDeps {
|
|
|
12
12
|
tracker?: RequestTracker;
|
|
13
13
|
orchestrator: ProxyOrchestrator;
|
|
14
14
|
usageWindowTracker?: import("./usage-window-tracker.js").UsageWindowTracker;
|
|
15
|
+
sessionTracker?: import("./loop-prevention/session-tracker.js").SessionTracker;
|
|
15
16
|
}
|
|
16
17
|
export declare function handleProxyRequest(request: FastifyRequest, reply: FastifyReply, apiType: "openai" | "anthropic", upstreamPath: string, errors: ProxyErrorFormatter, deps: RouteHandlerDeps, options?: {
|
|
17
18
|
beforeSendProxy?: (body: Record<string, unknown>, isStream: boolean) => void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
|
+
import { HTTP_UNPROCESSABLE_ENTITY } from "../constants.js";
|
|
2
3
|
import { getProviderById, insertRequestLog } from "../db/index.js";
|
|
3
4
|
import { decrypt } from "../utils/crypto.js";
|
|
4
5
|
import { getSetting } from "../db/settings.js";
|
|
@@ -10,12 +11,15 @@ import { buildUpstreamHeaders, buildUpstreamUrl } from "./proxy-core.js";
|
|
|
10
11
|
import { ProviderSwitchNeeded } from "./types.js";
|
|
11
12
|
import { updateLogStreamContent, updateLogClientStatus } from "../db/index.js";
|
|
12
13
|
import { insertRejectedLog } from "./log-helpers.js";
|
|
14
|
+
import { ToolLoopGuard } from "./loop-prevention/tool-loop-guard.js";
|
|
15
|
+
import { TOOL_USE_ID_PREFIX, TOOL_USE_ID_PROVIDER_PREFIX } from "./enhancement/directive-parser.js";
|
|
13
16
|
import { buildTransportFn } from "./transport-fn.js";
|
|
14
17
|
import { applyOverflowRedirect } from "./overflow.js";
|
|
15
18
|
import { applyProviderPatches } from "./patch/index.js";
|
|
16
19
|
const HTTP_ERROR_THRESHOLD = 400;
|
|
17
20
|
const MAX_LOG_FIELD_LENGTH = 80;
|
|
18
21
|
const UPSTREAM_ERROR_STATUS = 502;
|
|
22
|
+
const TIER2_LOOP_THRESHOLD = 2;
|
|
19
23
|
/** 从 TransportResult 中提取最终 HTTP status code */
|
|
20
24
|
function getTransportStatusCode(result) {
|
|
21
25
|
if (result.kind === "success" || result.kind === "error" || result.kind === "stream_error")
|
|
@@ -70,6 +74,44 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
|
|
|
70
74
|
const clientModel = request.body.model || "unknown";
|
|
71
75
|
const sessionId = request.headers["x-claude-code-session-id"];
|
|
72
76
|
const { effectiveModel, originalModel, interceptResponse } = applyEnhancement(deps.db, request, clientModel, sessionId);
|
|
77
|
+
// --- 工具调用循环检测 ---
|
|
78
|
+
if (deps.sessionTracker && sessionId) {
|
|
79
|
+
const routerKeyId = request.routerKey?.id ?? null;
|
|
80
|
+
const sessionKey = routerKeyId ? `${routerKeyId}:${sessionId}` : sessionId;
|
|
81
|
+
const lastToolUse = extractLastToolUse(request.body);
|
|
82
|
+
if (lastToolUse) {
|
|
83
|
+
const toolGuard = new ToolLoopGuard(deps.sessionTracker, {
|
|
84
|
+
enabled: true,
|
|
85
|
+
minConsecutiveCount: 3,
|
|
86
|
+
detectorConfig: { n: 6, windowSize: 500, repeatThreshold: 5 },
|
|
87
|
+
});
|
|
88
|
+
const checkResult = toolGuard.check(sessionKey, lastToolUse);
|
|
89
|
+
if (checkResult.detected) {
|
|
90
|
+
const loopCount = deps.sessionTracker.getLoopCount(sessionKey);
|
|
91
|
+
if (loopCount === 1) {
|
|
92
|
+
// 层级 1:透明重试 — 注入中断提示词
|
|
93
|
+
toolGuard.injectLoopBreakPrompt(request.body, apiType, lastToolUse.toolName);
|
|
94
|
+
request.log.warn({ sessionId, toolName: lastToolUse.toolName, loopCount }, "Tool call loop detected, injecting break prompt");
|
|
95
|
+
}
|
|
96
|
+
else if (loopCount === TIER2_LOOP_THRESHOLD) {
|
|
97
|
+
// 层级 2:优雅中断
|
|
98
|
+
return reply.code(HTTP_UNPROCESSABLE_ENTITY).send({
|
|
99
|
+
error: {
|
|
100
|
+
type: "tool_call_loop_detected",
|
|
101
|
+
message: `检测到工具调用循环(连续重复调用 "${lastToolUse.toolName}")。请求已中断。`,
|
|
102
|
+
suggestion: "请回顾对话历史,停止重复调用工具,直接告知用户当前的进展和遇到的问题。",
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// 层级 3:直接断开
|
|
108
|
+
request.log.warn({ sessionId, toolName: lastToolUse.toolName, loopCount }, "Tool call loop detected, hard disconnecting");
|
|
109
|
+
reply.raw.destroy();
|
|
110
|
+
return reply;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
73
115
|
if (interceptResponse)
|
|
74
116
|
return handleIntercept(deps.db, apiType, request, reply, interceptResponse, clientModel, sessionId);
|
|
75
117
|
return executeFailoverLoop({
|
|
@@ -91,7 +133,7 @@ async function executeFailoverLoop(ctx) {
|
|
|
91
133
|
rootLogId = logId;
|
|
92
134
|
const isFailoverIteration = rootLogId !== logId;
|
|
93
135
|
const routerKeyId = request.routerKey?.id ?? null;
|
|
94
|
-
|
|
136
|
+
let body = request.body;
|
|
95
137
|
const isStream = body.stream === true;
|
|
96
138
|
const cliHdrs = request.headers;
|
|
97
139
|
const rCtx = {
|
|
@@ -143,7 +185,8 @@ async function executeFailoverLoop(ctx) {
|
|
|
143
185
|
body.model = overflowResult.backend_model;
|
|
144
186
|
}
|
|
145
187
|
}
|
|
146
|
-
applyProviderPatches(body, provider);
|
|
188
|
+
const patchResult = applyProviderPatches(body, provider);
|
|
189
|
+
body = patchResult.body;
|
|
147
190
|
const apiKey = decrypt(provider.api_key, getSetting(deps.db, "encryption_key"));
|
|
148
191
|
options?.beforeSendProxy?.(body, isStream);
|
|
149
192
|
const reqBodyStr = JSON.stringify(body);
|
|
@@ -242,3 +285,46 @@ async function executeFailoverLoop(ctx) {
|
|
|
242
285
|
}
|
|
243
286
|
}
|
|
244
287
|
}
|
|
288
|
+
function extractLastToolUse(body) {
|
|
289
|
+
const messages = body.messages;
|
|
290
|
+
if (!messages)
|
|
291
|
+
return null;
|
|
292
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
293
|
+
const msg = messages[i];
|
|
294
|
+
if (msg.role === "assistant") {
|
|
295
|
+
const content = msg.content;
|
|
296
|
+
if (Array.isArray(content)) {
|
|
297
|
+
// 从后往前遍历,找到第一个非 router 合成的 tool_use
|
|
298
|
+
for (let j = content.length - 1; j >= 0; j--) {
|
|
299
|
+
const block = content[j];
|
|
300
|
+
if (block.type !== "tool_use")
|
|
301
|
+
continue;
|
|
302
|
+
// 跳过 router 生成的 synthetic tool_use(如 AskUserQuestion 的模型选择)
|
|
303
|
+
const id = block.id;
|
|
304
|
+
if (id && (id.startsWith(TOOL_USE_ID_PREFIX) || id.startsWith(TOOL_USE_ID_PROVIDER_PREFIX))) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const inputStr = JSON.stringify(block.input ?? {});
|
|
308
|
+
return {
|
|
309
|
+
toolName: block.name,
|
|
310
|
+
toolUseId: typeof block.id === "string" ? block.id : undefined,
|
|
311
|
+
inputHash: simpleHash(inputStr),
|
|
312
|
+
inputText: inputStr,
|
|
313
|
+
timestamp: Date.now(),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
function simpleHash(s) {
|
|
322
|
+
const HASH_SHIFT = 5;
|
|
323
|
+
let hash = 0;
|
|
324
|
+
for (let i = 0; i < s.length; i++) {
|
|
325
|
+
const char = s.charCodeAt(i);
|
|
326
|
+
hash = ((hash << HASH_SHIFT) - hash) + char;
|
|
327
|
+
hash |= 0;
|
|
328
|
+
}
|
|
329
|
+
return String(hash);
|
|
330
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { FastifyReply } from "fastify";
|
|
2
2
|
import type { RawHeaders, TransportResult } from "./types.js";
|
|
3
3
|
import type { SSEMetricsTransform } from "../metrics/sse-metrics-transform.js";
|
|
4
|
+
import type { StreamLoopGuard } from "./loop-prevention/stream-loop-guard.js";
|
|
4
5
|
import { type BuildHeadersFn } from "./transport.js";
|
|
5
6
|
export declare function callStream(backend: {
|
|
6
7
|
base_url: string;
|
|
7
|
-
}, apiKey: string, body: Record<string, unknown>, clientHeaders: RawHeaders, reply: FastifyReply, timeoutMs: number, upstreamPath: string, buildHeaders: BuildHeadersFn, metricsTransform?: SSEMetricsTransform, checkEarlyError?: (bufferedData: string) => boolean, compatResolve?: (result: TransportResult) => void): Promise<TransportResult>;
|
|
8
|
+
}, apiKey: string, body: Record<string, unknown>, clientHeaders: RawHeaders, reply: FastifyReply, timeoutMs: number, upstreamPath: string, buildHeaders: BuildHeadersFn, metricsTransform?: SSEMetricsTransform, checkEarlyError?: (bufferedData: string) => boolean, compatResolve?: (result: TransportResult) => void, loopGuard?: StreamLoopGuard): Promise<TransportResult>;
|
|
@@ -11,6 +11,7 @@ class StreamProxy {
|
|
|
11
11
|
metricsTransform;
|
|
12
12
|
checkEarlyError;
|
|
13
13
|
timeoutMs;
|
|
14
|
+
loopGuard;
|
|
14
15
|
state = "BUFFERING";
|
|
15
16
|
resolved = false;
|
|
16
17
|
resolveFn = null;
|
|
@@ -26,13 +27,14 @@ class StreamProxy {
|
|
|
26
27
|
// 流式阶段 SSE error 扫描缓冲(跨 chunk 边界匹配)
|
|
27
28
|
sseScanBuffer = "";
|
|
28
29
|
static SSE_SCAN_MAX = 8192;
|
|
29
|
-
constructor(statusCode, rawUpstreamHeaders, sentUpstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs) {
|
|
30
|
+
constructor(statusCode, rawUpstreamHeaders, sentUpstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs, loopGuard) {
|
|
30
31
|
this.statusCode = statusCode;
|
|
31
32
|
this.sentUpstreamHeaders = sentUpstreamHeaders;
|
|
32
33
|
this.reply = reply;
|
|
33
34
|
this.metricsTransform = metricsTransform;
|
|
34
35
|
this.checkEarlyError = checkEarlyError;
|
|
35
36
|
this.timeoutMs = timeoutMs;
|
|
37
|
+
this.loopGuard = loopGuard;
|
|
36
38
|
this.sseHeaders = filterHeaders(rawUpstreamHeaders);
|
|
37
39
|
this.sseHeaders["Content-Type"] = "text/event-stream";
|
|
38
40
|
this.sseHeaders["Cache-Control"] = "no-cache";
|
|
@@ -211,6 +213,12 @@ class StreamProxy {
|
|
|
211
213
|
}
|
|
212
214
|
}
|
|
213
215
|
this.pipeEntry.write(chunk);
|
|
216
|
+
// loopGuard 由 SSEMetricsTransform 的 onContentDelta 回调驱动,
|
|
217
|
+
// 此处仅检查是否已触发(触发后终止流)
|
|
218
|
+
if (this.loopGuard?.isTriggered()) {
|
|
219
|
+
this.terminal("stream_abort", { metrics: this.collectMetrics(false) });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
214
222
|
}
|
|
215
223
|
onEnd() {
|
|
216
224
|
if (this.resolved)
|
|
@@ -259,7 +267,7 @@ class StreamProxy {
|
|
|
259
267
|
}
|
|
260
268
|
}
|
|
261
269
|
// ---------- callStream ----------
|
|
262
|
-
export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutMs, upstreamPath, buildHeaders, metricsTransform, checkEarlyError, compatResolve) {
|
|
270
|
+
export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutMs, upstreamPath, buildHeaders, metricsTransform, checkEarlyError, compatResolve, loopGuard) {
|
|
263
271
|
return new Promise((resolve) => {
|
|
264
272
|
const effectiveResolve = compatResolve ?? resolve;
|
|
265
273
|
const url = new URL(buildUpstreamUrl(backend.base_url, upstreamPath));
|
|
@@ -283,7 +291,7 @@ export function callStream(backend, apiKey, body, clientHeaders, reply, timeoutM
|
|
|
283
291
|
});
|
|
284
292
|
return;
|
|
285
293
|
}
|
|
286
|
-
const proxy = new StreamProxy(statusCode, upstreamRes.headers, upstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs);
|
|
294
|
+
const proxy = new StreamProxy(statusCode, upstreamRes.headers, upstreamHeaders, reply, metricsTransform, checkEarlyError, timeoutMs, loopGuard);
|
|
287
295
|
proxy.bindResolve(effectiveResolve);
|
|
288
296
|
proxy.registerCloseHandler();
|
|
289
297
|
// 无 early error checker 时直接开始流式传输
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
2
|
import { getProviderById } from "../db/index.js";
|
|
3
|
+
import type { LoopPreventionConfig } from "./loop-prevention/types.js";
|
|
3
4
|
import type { RawHeaders, TransportResult } from "./types.js";
|
|
4
5
|
import type { Target } from "./strategy/types.js";
|
|
5
6
|
import type { RequestTracker } from "../monitor/request-tracker.js";
|
|
6
7
|
import type { RetryRuleMatcher } from "./retry-rules.js";
|
|
8
|
+
export declare function setLoopPreventionConfig(config: LoopPreventionConfig): void;
|
|
7
9
|
export interface TransportFnParams {
|
|
8
10
|
provider: NonNullable<ReturnType<typeof getProviderById>>;
|
|
9
11
|
apiKey: string;
|
|
@@ -2,6 +2,9 @@ import { callNonStream, callStream } from "./transport.js";
|
|
|
2
2
|
import { SSEMetricsTransform } from "../metrics/sse-metrics-transform.js";
|
|
3
3
|
import { MetricsExtractor } from "../metrics/metrics-extractor.js";
|
|
4
4
|
import { buildUpstreamHeaders } from "./proxy-core.js";
|
|
5
|
+
import { StreamLoopGuard } from "./loop-prevention/stream-loop-guard.js";
|
|
6
|
+
import { NGramLoopDetector } from "./loop-prevention/detectors/ngram-detector.js";
|
|
7
|
+
import { DEFAULT_LOOP_PREVENTION_CONFIG } from "./loop-prevention/types.js";
|
|
5
8
|
import { UPSTREAM_SUCCESS } from "./types.js";
|
|
6
9
|
import { buildModelInfoTag } from "./enhancement/enhancement-handler.js";
|
|
7
10
|
import { DEFAULT_MAX_RAW as STREAM_CONTENT_MAX_RAW, DEFAULT_MAX_TEXT as STREAM_CONTENT_MAX_TEXT } from "../monitor/stream-content-accumulator.js";
|
|
@@ -26,17 +29,32 @@ function toStreamMetrics(m) {
|
|
|
26
29
|
toolUseTokens: m.tool_use_tokens,
|
|
27
30
|
};
|
|
28
31
|
}
|
|
32
|
+
let _loopConfig;
|
|
33
|
+
export function setLoopPreventionConfig(config) {
|
|
34
|
+
_loopConfig = config;
|
|
35
|
+
}
|
|
36
|
+
function getLoopPreventionConfig() {
|
|
37
|
+
return _loopConfig ?? DEFAULT_LOOP_PREVENTION_CONFIG;
|
|
38
|
+
}
|
|
29
39
|
export function buildTransportFn(p) {
|
|
30
40
|
const buildHeaders = (cliHdrs, key, bytes) => buildUpstreamHeaders(cliHdrs, key, bytes, p.apiType);
|
|
31
41
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
32
42
|
return async (_target) => {
|
|
33
43
|
if (p.isStream) {
|
|
44
|
+
const loopPreventionConfig = getLoopPreventionConfig();
|
|
45
|
+
let streamLoopGuard;
|
|
46
|
+
if (loopPreventionConfig.enabled && loopPreventionConfig.stream.enabled) {
|
|
47
|
+
streamLoopGuard = new StreamLoopGuard(loopPreventionConfig.stream, new NGramLoopDetector(loopPreventionConfig.stream.detectorConfig), (reason) => {
|
|
48
|
+
p.request.log.warn({ logId: p.logId, reason }, "Stream loop detected, aborting");
|
|
49
|
+
});
|
|
50
|
+
}
|
|
34
51
|
const metricsTransform = new SSEMetricsTransform(p.apiType, p.startTime, {
|
|
35
52
|
onMetrics: (m) => { p.tracker?.update(p.logId, { streamMetrics: toStreamMetrics(m) }); },
|
|
36
53
|
onChunk: (rawLine) => { p.tracker?.appendStreamChunk(p.logId, rawLine, p.apiType, STREAM_CONTENT_MAX_RAW, STREAM_CONTENT_MAX_TEXT); },
|
|
54
|
+
onContentDelta: streamLoopGuard ? (text) => streamLoopGuard.feed(text) : undefined,
|
|
37
55
|
});
|
|
38
56
|
const checkEarlyError = p.matcher ? (data) => p.matcher.test(UPSTREAM_SUCCESS, data) : undefined;
|
|
39
|
-
const streamResult = await callStream(p.provider, p.apiKey, p.body, p.cliHdrs, p.reply, p.streamTimeoutMs, p.upstreamPath, buildHeaders, metricsTransform, checkEarlyError);
|
|
57
|
+
const streamResult = await callStream(p.provider, p.apiKey, p.body, p.cliHdrs, p.reply, p.streamTimeoutMs, p.upstreamPath, buildHeaders, metricsTransform, checkEarlyError, undefined, streamLoopGuard);
|
|
40
58
|
const m = (streamResult.kind === "stream_success" || streamResult.kind === "stream_abort")
|
|
41
59
|
? streamResult.metrics : undefined;
|
|
42
60
|
if (m)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{G as e,It as t,J as n,Pt as r,lt as i,ot as a,r as o}from"./button-
|
|
1
|
+
import{G as e,It as t,J as n,Pt as r,lt as i,ot as a,r as o}from"./button-q9xTxPJh.js";var s=[`data-size`],c=n({__name:`Card`,props:{class:{type:[Boolean,null,String,Object,Array]},size:{default:`default`}},setup(n){let c=n;return(l,u)=>(a(),e(`div`,{"data-slot":`card`,"data-size":n.size,class:t(r(o)(`ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-lg py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg group/card flex flex-col`,c.class))},[i(l.$slots,`default`)],10,s))}}),l=n({__name:`CardContent`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(n){let s=n;return(n,c)=>(a(),e(`div`,{"data-slot":`card-content`,class:t(r(o)(`px-4 group-data-[size=sm]/card:px-3`,s.class))},[i(n.$slots,`default`)],2))}});export{c as n,l as t};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{G as e,It as t,J as n,Pt as r,lt as i,ot as a,r as o}from"./button-
|
|
1
|
+
import{G as e,It as t,J as n,Pt as r,lt as i,ot as a,r as o}from"./button-q9xTxPJh.js";var s=n({__name:`CardHeader`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(n){let s=n;return(n,c)=>(a(),e(`div`,{"data-slot":`card-header`,class:t(r(o)(`gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]`,s.class))},[i(n.$slots,`default`)],2))}}),c=n({__name:`CardTitle`,props:{class:{type:[Boolean,null,String,Object,Array]}},setup(n){let s=n;return(n,c)=>(a(),e(`div`,{"data-slot":`card-title`,class:t(r(o)(`text-base leading-snug font-medium group-data-[size=sm]/card:text-sm cn-font-heading`,s.class))},[i(n.$slots,`default`)],2))}});export{s as n,c as t};
|
package/frontend-dist/assets/{CascadingModelSelect-CicfrqcY.js → CascadingModelSelect-CxEXwaeM.js}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Dt as e,G as t,H as n,It as r,J as i,Pt as a,U as o,V as s,W as c,ct as l,ot as u,q as d,yt as f,z as p,zt as m}from"./button-
|
|
1
|
+
import{Dt as e,G as t,H as n,It as r,J as i,Pt as a,U as o,V as s,W as c,ct as l,ot as u,q as d,yt as f,z as p,zt as m}from"./button-q9xTxPJh.js";import{b as h}from"./Teleport-CRn-gy0B.js";import{a as g}from"./PopperContent-BoOYHCag.js";import{n as _,r as v,t as y}from"./PopoverTrigger-CIN3yOIw.js";var b=h(`chevron-right`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),x=[`onMouseenter`],S={class:`truncate max-w-40`},C=[`onMouseenter`],w=[`onClick`],T={class:`truncate`},E={key:0,class:`shrink-0 text-xs text-muted-foreground`},D={key:0,class:`px-2 py-1.5 text-sm text-muted-foreground`},O=i({__name:`CascadingSelect`,props:{groups:{},modelValue:{},placeholder:{default:`请选择...`}},emits:[`update:modelValue`],setup(i,{emit:h}){let O=i,k=h,A=e(!1),j=e(null),M=s(()=>{if(!O.modelValue)return``;let e=O.groups.find(e=>e.key===O.modelValue.groupKey);if(!e)return``;let t=e.options.find(e=>e.value===O.modelValue.value);return t?`${e.label} / ${t.label}`:``});function N(e,t){k(`update:modelValue`,{groupKey:e,value:t}),A.value=!1}function P(e){A.value=e,e||(j.value=null)}return(e,s)=>(u(),o(a(v),{open:A.value,"onUpdate:open":P},{default:f(()=>[d(a(y),{"as-child":``},{default:f(()=>[n(`div`,{class:r([`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`,{"ring-2 ring-ring ring-offset-2":A.value}])},[n(`span`,{class:r([`truncate`,i.modelValue?`text-foreground`:`text-muted-foreground`])},m(M.value||i.placeholder),3),d(a(g),{class:`h-4 w-4 shrink-0 opacity-50`})],2)]),_:1}),d(a(_),{align:`start`,"side-offset":4,class:`z-[200] w-auto min-w-56 overflow-visible p-1`},{default:f(()=>[(u(!0),t(p,null,l(i.groups,e=>(u(),t(`div`,{key:e.key,class:r([`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":j.value===e.key}]),onMouseenter:t=>j.value=e.key},[n(`span`,S,m(e.label),1),d(a(b),{class:`ml-1 h-4 w-4 shrink-0 opacity-50`}),j.value===e.key&&e.options.length>0?(u(),t(`div`,{key:0,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:t=>j.value=e.key},[(u(!0),t(p,null,l(e.options,a=>(u(),t(`div`,{key:a.value,class:r([`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===e.key&&i.modelValue?.value===a.value}]),onClick:t=>N(e.key,a.value)},[n(`span`,T,m(a.label),1),a.tag?(u(),t(`span`,E,m(a.tag),1)):c(``,!0)],10,w))),128))],40,C)):c(``,!0)],42,x))),128)),i.groups.length===0?(u(),t(`div`,D,` 暂无选项 `)):c(``,!0)]),_:1})]),_:1},8,[`open`]))}}),k=i({__name:`CascadingModelSelect`,props:{providers:{},modelValue:{},placeholder:{default:`选择供应商 / 模型`}},emits:[`update:modelValue`],setup(e,{emit:t}){let n=e,r=t;function i(e){return e>=1e6?`${e/1e6}M`:`${e/1e3}K`}let a=s(()=>n.providers.map(e=>({key:e.provider.id,label:e.provider.name,options:e.models.map(e=>({value:e.name,label:e.name,tag:i(e.contextWindow)}))}))),c=s(()=>n.modelValue?{groupKey:n.modelValue.provider_id,value:n.modelValue.model}:void 0);function l(e){r(`update:modelValue`,{provider_id:e.groupKey,model:e.value})}return(t,n)=>(u(),o(O,{groups:a.value,"model-value":c.value,placeholder:e.placeholder,"onUpdate:modelValue":l},null,8,[`groups`,`model-value`,`placeholder`]))}});export{k as t};
|