llm-simple-router 0.6.4 → 0.6.5
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/config/recommended-retry-rules.json +1 -2
- package/dist/admin/proxy-enhancement.js +6 -0
- package/dist/config.d.ts +0 -2
- package/dist/config.js +0 -5
- package/dist/index.js +2 -9
- package/dist/metrics/sse-metrics-transform.d.ts +1 -1
- package/dist/metrics/sse-metrics-transform.js +1 -2
- package/dist/proxy/enhancement/enhancement-handler.d.ts +2 -1
- package/dist/proxy/enhancement/enhancement-handler.js +2 -2
- package/dist/proxy/enhancement-config.d.ts +2 -0
- package/dist/proxy/enhancement-config.js +4 -0
- 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 +4 -3
- package/dist/proxy/patch/deepseek/patch-orphan-tool-results.js +30 -23
- package/dist/proxy/patch/deepseek/patch-thinking-blocks.d.ts +4 -8
- package/dist/proxy/patch/deepseek/patch-thinking-blocks.js +12 -57
- package/dist/proxy/patch/index.d.ts +3 -12
- package/dist/proxy/patch/index.js +4 -13
- package/dist/proxy/proxy-handler.js +10 -9
- package/dist/proxy/stream-proxy.js +0 -2
- package/dist/proxy/transport-fn.d.ts +1 -2
- package/dist/proxy/transport-fn.js +5 -11
- package/frontend-dist/assets/{CardContent-CpiBn1Oc.js → CardContent-DG1NiXMU.js} +1 -1
- package/frontend-dist/assets/{CardTitle-dwtgd_nl.js → CardTitle-6JvqGNd9.js} +1 -1
- package/frontend-dist/assets/{CascadingModelSelect-CxEXwaeM.js → CascadingModelSelect-BzDHmRLv.js} +1 -1
- package/frontend-dist/assets/{Checkbox-D2U4I-pO.js → Checkbox-D4TrUHCb.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-B2AdbZBh.js → CollapsibleTrigger-ZJCGAkxi.js} +1 -1
- package/frontend-dist/assets/{Collection-BJZSFJsF.js → Collection-CRTZGViV.js} +1 -1
- package/frontend-dist/assets/{Dashboard-D3cDhJNh.js → Dashboard-C-B4p8HM.js} +1 -1
- package/frontend-dist/assets/{DialogTitle-BTuQdRm1.js → DialogTitle-CecSHUUq.js} +1 -1
- package/frontend-dist/assets/{Input-BYULYPCe.js → Input-DoCE9j9O.js} +1 -1
- package/frontend-dist/assets/{Label-sImW5XUw.js → Label-6sa_UFaw.js} +1 -1
- package/frontend-dist/assets/{Login-B0kGGZFi.js → Login-BkCJQFjz.js} +1 -1
- package/frontend-dist/assets/{Logs-BpAeeJRi.js → Logs-HG_BuJe5.js} +1 -1
- package/frontend-dist/assets/{ModelMappings-CsHLYqQB.js → ModelMappings-CjI27TDc.js} +1 -1
- package/frontend-dist/assets/{Monitor-BmMFWFJg.js → Monitor-CElP7aKi.js} +1 -1
- package/frontend-dist/assets/{PopoverTrigger-CIN3yOIw.js → PopoverTrigger-D0nT5UVQ.js} +1 -1
- package/frontend-dist/assets/{PopperContent-BoOYHCag.js → PopperContent-BbE-uPX0.js} +1 -1
- package/frontend-dist/assets/{Providers-DrepCc4A.js → Providers-DTqfl249.js} +1 -1
- package/frontend-dist/assets/ProxyEnhancement-DQixi_0_.js +5 -0
- package/frontend-dist/assets/{RetryRules-Ce5HfNcc.js → RetryRules-B5r18TFL.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-CeSGvjll.js → RouterKeys-BwdoieS_.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-gAYs0l8Z.js → RovingFocusItem-CpDEc1ox.js} +1 -1
- package/frontend-dist/assets/{Schedules-DphkPAWD.js → Schedules-C0q4rt97.js} +1 -1
- package/frontend-dist/assets/{SelectValue-Cbbd2Xbm.js → SelectValue-CKJWYmgi.js} +1 -1
- package/frontend-dist/assets/{Settings-DIP7VawX.js → Settings-DQN3_4Gx.js} +1 -1
- package/frontend-dist/assets/{Setup-_d_M-Qi6.js → Setup-CEk8SRJu.js} +1 -1
- package/frontend-dist/assets/{Switch-BmmYsqAx.js → Switch-ThwlPMEz.js} +1 -1
- package/frontend-dist/assets/{TableHeader-C1mpCsyo.js → TableHeader-UwRhaVOA.js} +1 -1
- package/frontend-dist/assets/{TabsTrigger-kN1usMvC.js → TabsTrigger-CNV5JhP6.js} +1 -1
- package/frontend-dist/assets/{Teleport-CRn-gy0B.js → Teleport-BHTgdtZR.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-rlKo7E3A.js → TooltipTrigger-DKa4gPvX.js} +1 -1
- package/frontend-dist/assets/UnifiedRequestDialog-BjEigSaR.css +1 -0
- package/frontend-dist/assets/UnifiedRequestDialog-CBbZxE1N.js +3 -0
- package/frontend-dist/assets/{VisuallyHidden-CQvCw9gB.js → VisuallyHidden-r2E3heZY.js} +1 -1
- package/frontend-dist/assets/{VisuallyHiddenInput-B-DnnaWN.js → VisuallyHiddenInput-DSyTANlz.js} +1 -1
- package/frontend-dist/assets/{alert-dialog-M3PhwD75.js → alert-dialog-CNAhVHUE.js} +1 -1
- package/frontend-dist/assets/arrow-down-CFVTVH7t.js +1 -0
- package/frontend-dist/assets/{badge-7WIbGMsE.js → badge-RFq5LS37.js} +1 -1
- package/frontend-dist/assets/{button-q9xTxPJh.js → button-D4qBQ0nA.js} +2 -2
- package/frontend-dist/assets/check-CkLQpfOO.js +1 -0
- package/frontend-dist/assets/{copy-C1a8OrYP.js → copy-BxjNh73N.js} +1 -1
- package/frontend-dist/assets/{dialog-B6RnMgGx.js → dialog-CLepZTFY.js} +1 -1
- package/frontend-dist/assets/{file-text-CsuQUjXR.js → file-text-DqxNey63.js} +1 -1
- package/frontend-dist/assets/{index-Dh7qL0Qt.js → index-C4H_2b0G.js} +1 -1
- package/frontend-dist/assets/index-_Icfkt3I.css +1 -0
- package/frontend-dist/assets/{lib-D1G8Xa05.js → lib-SdzBxIwM.js} +1 -1
- package/frontend-dist/assets/loader-circle-Cffc9Uf0.js +1 -0
- package/frontend-dist/assets/{useClipboard-CBONMfzU.js → useClipboard-DBClUufY.js} +1 -1
- package/frontend-dist/assets/{useFocusGuards-DwFX8o1a.js → useFocusGuards-B99Wx8XA.js} +1 -1
- package/frontend-dist/assets/useFormControl-DuWttDd8.js +1 -0
- package/frontend-dist/assets/{useLogRetention-C2IbjXjr.js → useLogRetention-CQ7Q54Tt.js} +1 -1
- package/frontend-dist/assets/useNonce-D1Mva4rM.js +1 -0
- package/frontend-dist/assets/x-BJjJvWU8.js +1 -0
- package/frontend-dist/index.html +20 -20
- package/package.json +1 -1
- package/frontend-dist/assets/ProxyEnhancement-dK_mOQ3m.js +0 -5
- package/frontend-dist/assets/UnifiedRequestDialog-C-Ui-fav.css +0 -1
- package/frontend-dist/assets/UnifiedRequestDialog-C7pEDa9D.js +0 -4
- package/frontend-dist/assets/arrow-down-BHq-drH-.js +0 -1
- package/frontend-dist/assets/check-CccuM1Sj.js +0 -1
- package/frontend-dist/assets/index-Ce7hFHTt.css +0 -1
- package/frontend-dist/assets/loader-circle-CVhxR0Tt.js +0 -1
- package/frontend-dist/assets/useFormControl-DC32gW1A.js +0 -1
- package/frontend-dist/assets/useNonce-t7XaR4bX.js +0 -1
- package/frontend-dist/assets/x-Bq5KcbWI.js +0 -1
|
@@ -6,6 +6,5 @@
|
|
|
6
6
|
{ "name": "ZAI 操作失败 (code 500)", "status_code": 400, "body_pattern": "\"type\"\\s*:\\s*\"error\".*\"code\"\\s*:\\s*\"500\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
|
|
7
7
|
{ "name": "ZAI 速率限制 (HTTP 200, code 1302)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"1302\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
|
|
8
8
|
{ "name": "ZAI SSE 错误 (HTTP 200, code 500)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"500\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 },
|
|
9
|
-
{ "name": "ZAI SSE 错误 (HTTP 200, code 1234)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"1234\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 }
|
|
10
|
-
{ "name": "ZAI 过载限流 (HTTP 200, code 1305)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"1305\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 }
|
|
9
|
+
{ "name": "ZAI SSE 错误 (HTTP 200, code 1234)", "status_code": 200, "body_pattern": "\"error\".*\"code\"\\s*:\\s*\"1234\"", "retry_strategy": "exponential", "retry_delay_ms": 5000, "max_retries": 10, "max_delay_ms": 60000 }
|
|
11
10
|
]
|
|
@@ -3,6 +3,8 @@ import { setSetting } from "../db/settings.js";
|
|
|
3
3
|
import { loadEnhancementConfig } from "../proxy/enhancement-config.js";
|
|
4
4
|
const UpdateProxyEnhancementSchema = Type.Object({
|
|
5
5
|
claude_code_enabled: Type.Boolean(),
|
|
6
|
+
tool_call_loop_enabled: Type.Boolean(),
|
|
7
|
+
stream_loop_enabled: Type.Boolean(),
|
|
6
8
|
});
|
|
7
9
|
const SessionParamsSchema = Type.Object({
|
|
8
10
|
keyId: Type.String(),
|
|
@@ -16,12 +18,16 @@ export const adminProxyEnhancementRoutes = (app, options, done) => {
|
|
|
16
18
|
const config = loadEnhancementConfig(db);
|
|
17
19
|
return reply.send({
|
|
18
20
|
claude_code_enabled: config.claude_code_enabled,
|
|
21
|
+
tool_call_loop_enabled: config.tool_call_loop_enabled,
|
|
22
|
+
stream_loop_enabled: config.stream_loop_enabled,
|
|
19
23
|
});
|
|
20
24
|
});
|
|
21
25
|
app.put("/admin/api/proxy-enhancement", { schema: { body: UpdateProxyEnhancementSchema } }, async (request, reply) => {
|
|
22
26
|
const body = request.body;
|
|
23
27
|
const config = {
|
|
24
28
|
claude_code_enabled: body.claude_code_enabled,
|
|
29
|
+
tool_call_loop_enabled: body.tool_call_loop_enabled,
|
|
30
|
+
stream_loop_enabled: body.stream_loop_enabled,
|
|
25
31
|
};
|
|
26
32
|
setSetting(db, "proxy_enhancement", JSON.stringify(config));
|
|
27
33
|
return reply.send({ success: true });
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { LoopPreventionConfig } from "./proxy/loop-prevention/types.js";
|
|
2
1
|
export interface Config {
|
|
3
2
|
PORT: number;
|
|
4
3
|
DB_PATH: string;
|
|
@@ -6,7 +5,6 @@ export interface Config {
|
|
|
6
5
|
TZ: string;
|
|
7
6
|
STREAM_TIMEOUT_MS: number;
|
|
8
7
|
RETRY_BASE_DELAY_MS: number;
|
|
9
|
-
LOOP_PREVENTION: LoopPreventionConfig;
|
|
10
8
|
}
|
|
11
9
|
export declare function resetConfig(): void;
|
|
12
10
|
export declare function getBaseConfig(): Config;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { DEFAULT_LOOP_PREVENTION_CONFIG } from "./proxy/loop-prevention/types.js";
|
|
4
3
|
let cachedConfig = null;
|
|
5
4
|
function getDefaultDbPath() {
|
|
6
5
|
if (process.env.DB_PATH)
|
|
@@ -20,10 +19,6 @@ export function getBaseConfig() {
|
|
|
20
19
|
TZ: process.env.TZ || "Asia/Shanghai",
|
|
21
20
|
STREAM_TIMEOUT_MS: parseInt(process.env.STREAM_TIMEOUT_MS || "3000000", 10),
|
|
22
21
|
RETRY_BASE_DELAY_MS: parseInt(process.env.RETRY_BASE_DELAY_MS || "1000", 10),
|
|
23
|
-
LOOP_PREVENTION: {
|
|
24
|
-
...DEFAULT_LOOP_PREVENTION_CONFIG,
|
|
25
|
-
...(process.env.LOOP_PREVENTION ? JSON.parse(process.env.LOOP_PREVENTION) : {}),
|
|
26
|
-
},
|
|
27
22
|
};
|
|
28
23
|
return cachedConfig;
|
|
29
24
|
}
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,6 @@ import { modelState } from "./proxy/model-state.js";
|
|
|
25
25
|
import { UsageWindowTracker } from "./proxy/usage-window-tracker.js";
|
|
26
26
|
import { SessionTracker } from "./proxy/loop-prevention/session-tracker.js";
|
|
27
27
|
import { DEFAULT_LOOP_PREVENTION_CONFIG } from "./proxy/loop-prevention/types.js";
|
|
28
|
-
import { setLoopPreventionConfig } from "./proxy/transport-fn.js";
|
|
29
28
|
import { scheduleLogCleanup } from "./db/log-cleaner.js";
|
|
30
29
|
import { scheduleDbSizeMonitor } from "./db/db-size-monitor.js";
|
|
31
30
|
import { startUpgradeChecker, stopUpgradeChecker } from "./admin/upgrade.js";
|
|
@@ -159,14 +158,8 @@ export async function buildApp(options) {
|
|
|
159
158
|
// 5h 用量窗口追踪器,启动时自动补齐缺失窗口
|
|
160
159
|
const usageWindowTracker = new UsageWindowTracker(db);
|
|
161
160
|
usageWindowTracker.reconcileOnStartup();
|
|
162
|
-
|
|
163
|
-
const sessionTracker = new SessionTracker(
|
|
164
|
-
// buildApp() 默认启用循环预防。
|
|
165
|
-
// 用户可通过环境变量 LOOP_PREVENTION='{"enabled":false}' 关闭。
|
|
166
|
-
// 直接注册插件的测试不使用 buildApp(),不受此影响。
|
|
167
|
-
setLoopPreventionConfig(process.env.LOOP_PREVENTION
|
|
168
|
-
? loopConfig
|
|
169
|
-
: { ...loopConfig, enabled: true });
|
|
161
|
+
// Session tracker(工具调用循环检测用),始终创建但检测受 proxy_enhancement 配置控制
|
|
162
|
+
const sessionTracker = new SessionTracker(DEFAULT_LOOP_PREVENTION_CONFIG.sessionTracker);
|
|
170
163
|
// 从 DB 读取已有 provider 的并发配置,初始化信号量管理器和 tracker
|
|
171
164
|
const allProviders = getAllProviders(db);
|
|
172
165
|
for (const p of allProviders) {
|
|
@@ -31,7 +31,7 @@ export declare class SSEMetricsTransform extends Transform {
|
|
|
31
31
|
_transform(chunk: Buffer, _encoding: BufferEncoding, callback: TransformCallback): void;
|
|
32
32
|
_flush(callback: TransformCallback): void;
|
|
33
33
|
getExtractor(): MetricsExtractor;
|
|
34
|
-
/**
|
|
34
|
+
/** 从 SSE 事件中提取内容文本,触发 onContentDelta 回调 */
|
|
35
35
|
private emitContentDelta;
|
|
36
36
|
/**
|
|
37
37
|
* 从 SSE data 字段中提取实际内容文本(thinking / text / tool JSON delta)。
|
|
@@ -47,7 +47,6 @@ export class SSEMetricsTransform extends Transform {
|
|
|
47
47
|
this.extractor.processEvent(event);
|
|
48
48
|
this.emitContentDelta(event);
|
|
49
49
|
}
|
|
50
|
-
// flush 无条件推送最终状态,确保消费者能拿到完整指标
|
|
51
50
|
if (this.onMetrics && !this.flushed) {
|
|
52
51
|
this.flushed = true;
|
|
53
52
|
this.lastCallbackTime = Date.now();
|
|
@@ -58,7 +57,7 @@ export class SSEMetricsTransform extends Transform {
|
|
|
58
57
|
getExtractor() {
|
|
59
58
|
return this.extractor;
|
|
60
59
|
}
|
|
61
|
-
/**
|
|
60
|
+
/** 从 SSE 事件中提取内容文本,触发 onContentDelta 回调 */
|
|
62
61
|
emitContentDelta(event) {
|
|
63
62
|
if (!this.onContentDelta || !event.data)
|
|
64
63
|
return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FastifyRequest } from "fastify";
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
|
+
import { type EnhancementConfig } from "../enhancement-config.js";
|
|
3
4
|
export interface InterceptResponse {
|
|
4
5
|
statusCode: number;
|
|
5
6
|
body: unknown;
|
|
@@ -18,6 +19,6 @@ export interface EnhancementResult {
|
|
|
18
19
|
* 在代理转发前应用代理增强逻辑(指令解析 + 会话记忆 + 模型替换 + 命令拦截)。
|
|
19
20
|
* 仅当 proxy_enhancement.claude_code_enabled 开启时生效。
|
|
20
21
|
*/
|
|
21
|
-
export declare function applyEnhancement(db: Database.Database, request: FastifyRequest, clientModel: string, sessionId?: string): EnhancementResult;
|
|
22
|
+
export declare function applyEnhancement(db: Database.Database, request: FastifyRequest, clientModel: string, sessionId?: string, enhancementConfig?: EnhancementConfig): EnhancementResult;
|
|
22
23
|
/** 生成注入到非流式响应中的模型信息标签 */
|
|
23
24
|
export declare function buildModelInfoTag(effectiveModel: string): string;
|
|
@@ -60,9 +60,9 @@ function buildDisplayModels(db, allowedModelsRaw) {
|
|
|
60
60
|
* 在代理转发前应用代理增强逻辑(指令解析 + 会话记忆 + 模型替换 + 命令拦截)。
|
|
61
61
|
* 仅当 proxy_enhancement.claude_code_enabled 开启时生效。
|
|
62
62
|
*/
|
|
63
|
-
export function applyEnhancement(db, request, clientModel, sessionId) {
|
|
63
|
+
export function applyEnhancement(db, request, clientModel, sessionId, enhancementConfig) {
|
|
64
64
|
const nullResult = { effectiveModel: clientModel, originalModel: null, interceptResponse: null };
|
|
65
|
-
const enhancement = loadEnhancementConfig(db);
|
|
65
|
+
const enhancement = enhancementConfig ?? loadEnhancementConfig(db);
|
|
66
66
|
if (!enhancement.claude_code_enabled) {
|
|
67
67
|
return nullResult;
|
|
68
68
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
2
|
export interface EnhancementConfig {
|
|
3
3
|
claude_code_enabled: boolean;
|
|
4
|
+
tool_call_loop_enabled: boolean;
|
|
5
|
+
stream_loop_enabled: boolean;
|
|
4
6
|
}
|
|
5
7
|
/** 集中加载 proxy_enhancement 配置,避免多处重复 getSetting + JSON.parse */
|
|
6
8
|
export declare function loadEnhancementConfig(db: Database.Database): EnhancementConfig;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getSetting } from "../db/settings.js";
|
|
2
2
|
const DEFAULT_CONFIG = {
|
|
3
3
|
claude_code_enabled: false,
|
|
4
|
+
tool_call_loop_enabled: false,
|
|
5
|
+
stream_loop_enabled: false,
|
|
4
6
|
};
|
|
5
7
|
/** 集中加载 proxy_enhancement 配置,避免多处重复 getSetting + JSON.parse */
|
|
6
8
|
export function loadEnhancementConfig(db) {
|
|
@@ -11,6 +13,8 @@ export function loadEnhancementConfig(db) {
|
|
|
11
13
|
const parsed = JSON.parse(raw);
|
|
12
14
|
return {
|
|
13
15
|
claude_code_enabled: parsed.claude_code_enabled ?? false,
|
|
16
|
+
tool_call_loop_enabled: parsed.tool_call_loop_enabled ?? false,
|
|
17
|
+
stream_loop_enabled: parsed.stream_loop_enabled ?? false,
|
|
14
18
|
};
|
|
15
19
|
}
|
|
16
20
|
catch {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { patchMissingThinkingBlocks } from "./patch-thinking-blocks.js";
|
|
2
2
|
import { patchOrphanToolResults } from "./patch-orphan-tool-results.js";
|
|
3
3
|
/**
|
|
4
4
|
* 按序执行所有 DeepSeek 特定补丁。
|
|
5
|
-
*
|
|
5
|
+
* thinking 补丁先执行(影响 assistant 消息结构),
|
|
6
6
|
* tool_result 配对修复后执行。
|
|
7
7
|
*/
|
|
8
8
|
export function applyDeepSeekPatches(body) {
|
|
9
|
-
|
|
9
|
+
patchMissingThinkingBlocks(body);
|
|
10
10
|
patchOrphanToolResults(body);
|
|
11
11
|
}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* 算法:
|
|
6
6
|
* 1. 收集所有 assistant 消息中的 tool_use ID
|
|
7
|
-
* 2.
|
|
8
|
-
* 3.
|
|
9
|
-
* 4. 合并相邻的
|
|
7
|
+
* 2. 移除 tool_use_id 不在集合中的 tool_result 块
|
|
8
|
+
* 3. 移除清空后的空 user 消息
|
|
9
|
+
* 4. 合并相邻的 user 消息(Anthropic API 不允许连续 user 消息)
|
|
10
|
+
* 5. 合并相邻的 assistant 消息(同理)
|
|
10
11
|
*/
|
|
11
12
|
export declare function patchOrphanToolResults(body: Record<string, unknown>): void;
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* 算法:
|
|
6
6
|
* 1. 收集所有 assistant 消息中的 tool_use ID
|
|
7
|
-
* 2.
|
|
8
|
-
* 3.
|
|
9
|
-
* 4. 合并相邻的
|
|
7
|
+
* 2. 移除 tool_use_id 不在集合中的 tool_result 块
|
|
8
|
+
* 3. 移除清空后的空 user 消息
|
|
9
|
+
* 4. 合并相邻的 user 消息(Anthropic API 不允许连续 user 消息)
|
|
10
|
+
* 5. 合并相邻的 assistant 消息(同理)
|
|
10
11
|
*/
|
|
11
12
|
export function patchOrphanToolResults(body) {
|
|
12
13
|
if (!body.messages)
|
|
@@ -25,35 +26,41 @@ export function patchOrphanToolResults(body) {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
|
-
// Step 2:
|
|
29
|
-
let
|
|
29
|
+
// Step 2: 移除孤儿 tool_result 块
|
|
30
|
+
let removedAny = false;
|
|
30
31
|
for (const msg of messages) {
|
|
31
32
|
if (msg.role !== "user" || !Array.isArray(msg.content))
|
|
32
33
|
continue;
|
|
33
34
|
const blocks = msg.content;
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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;
|
|
35
|
+
const before = blocks.length;
|
|
36
|
+
const filtered = blocks.filter(block => {
|
|
37
|
+
if (block?.type === "tool_result" && typeof block.tool_use_id === "string") {
|
|
38
|
+
return knownToolUseIds.has(block.tool_use_id);
|
|
42
39
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
msg.content = newBlocks;
|
|
49
|
-
convertedAny = true;
|
|
40
|
+
return true;
|
|
41
|
+
});
|
|
42
|
+
if (filtered.length < before) {
|
|
43
|
+
msg.content = filtered;
|
|
44
|
+
removedAny = true;
|
|
50
45
|
}
|
|
51
46
|
}
|
|
52
|
-
if (!
|
|
47
|
+
if (!removedAny)
|
|
53
48
|
return;
|
|
54
|
-
// Step 3:
|
|
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
|
+
}
|
|
57
|
+
else if (typeof msg.content === "string" && msg.content.trim() === "") {
|
|
58
|
+
messages.splice(i, 1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Step 4: 合并相邻的 user 消息
|
|
55
62
|
mergeConsecutive(messages, "user");
|
|
56
|
-
// Step
|
|
63
|
+
// Step 5: 合并相邻的 assistant 消息(删除空 user 消息后可能产生)
|
|
57
64
|
mergeConsecutive(messages, "assistant");
|
|
58
65
|
}
|
|
59
66
|
function mergeConsecutive(messages, role) {
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 背景:DeepSeek Anthropic API 开启 thinking 后要求含 tool_use 的 assistant 消息
|
|
7
|
-
* 必须携带 thinking 块。跨模型切换(GLM → DeepSeek)时历史消息缺 thinking 会导致 400。
|
|
8
|
-
* 转化为 text 规避格式校验,同时完整保留信息。
|
|
2
|
+
* DeepSeek thinking 协议实现不完整:开启 thinking 模式后部分轮次不返回 thinking block,
|
|
3
|
+
* 但后续请求要求历史 assistant 消息必须携带 thinking block。
|
|
4
|
+
* 在 content 数组开头补一个空 thinking block 以绕过上游校验。
|
|
9
5
|
*/
|
|
10
|
-
export declare function
|
|
6
|
+
export declare function patchMissingThinkingBlocks(body: Record<string, unknown>): void;
|
|
@@ -1,69 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 背景:DeepSeek Anthropic API 开启 thinking 后要求含 tool_use 的 assistant 消息
|
|
7
|
-
* 必须携带 thinking 块。跨模型切换(GLM → DeepSeek)时历史消息缺 thinking 会导致 400。
|
|
8
|
-
* 转化为 text 规避格式校验,同时完整保留信息。
|
|
2
|
+
* DeepSeek thinking 协议实现不完整:开启 thinking 模式后部分轮次不返回 thinking block,
|
|
3
|
+
* 但后续请求要求历史 assistant 消息必须携带 thinking block。
|
|
4
|
+
* 在 content 数组开头补一个空 thinking block 以绕过上游校验。
|
|
9
5
|
*/
|
|
10
|
-
export function
|
|
6
|
+
export function patchMissingThinkingBlocks(body) {
|
|
11
7
|
if (!body.messages)
|
|
12
8
|
return;
|
|
13
9
|
const messages = body.messages;
|
|
14
|
-
|
|
10
|
+
// DeepSeek 可能在不传 thinking 参数时也启用 thinking 模式(从历史推断),
|
|
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)
|
|
15
15
|
return;
|
|
16
|
-
// Step 1: 识别非 DeepSeek 的 assistant 消息,转换 tool_use → text
|
|
17
|
-
const convertedIds = new Set();
|
|
18
16
|
for (const msg of messages) {
|
|
19
17
|
if (msg.role !== "assistant" || !Array.isArray(msg.content))
|
|
20
18
|
continue;
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
}
|
|
19
|
+
const hasThinking = msg.content.some((b) => b && typeof b === "object" && b.type === "thinking");
|
|
20
|
+
if (!hasThinking) {
|
|
21
|
+
msg.content.unshift({ type: "thinking", thinking: "", signature: "" });
|
|
66
22
|
}
|
|
67
|
-
msg.content = newBlocks;
|
|
68
23
|
}
|
|
69
24
|
}
|
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
interface ProviderInfo {
|
|
2
2
|
base_url: string;
|
|
3
3
|
}
|
|
4
|
-
export interface ProviderPatchMeta {
|
|
5
|
-
types: string[];
|
|
6
|
-
}
|
|
7
4
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 执行顺序:
|
|
11
|
-
* 1. 清理 router 合成的 tool_use/tool_result(通用,所有 provider)
|
|
12
|
-
* 2. Provider-specific patches(如 DeepSeek thinking 校验)
|
|
5
|
+
* 根据 provider 信息分发到对应的补丁逻辑。
|
|
6
|
+
* 每个补丁直接修改 body,不返回新对象。
|
|
13
7
|
*/
|
|
14
|
-
export declare function applyProviderPatches(body: Record<string, unknown>, provider: ProviderInfo):
|
|
15
|
-
body: Record<string, unknown>;
|
|
16
|
-
meta: ProviderPatchMeta;
|
|
17
|
-
};
|
|
8
|
+
export declare function applyProviderPatches(body: Record<string, unknown>, provider: ProviderInfo): void;
|
|
18
9
|
export {};
|
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
import { applyDeepSeekPatches } from "./deepseek/index.js";
|
|
2
|
-
import { patchRouterSyntheticToolCalls } from "./router-cleanup.js";
|
|
3
2
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 执行顺序:
|
|
7
|
-
* 1. 清理 router 合成的 tool_use/tool_result(通用,所有 provider)
|
|
8
|
-
* 2. Provider-specific patches(如 DeepSeek thinking 校验)
|
|
3
|
+
* 根据 provider 信息分发到对应的补丁逻辑。
|
|
4
|
+
* 每个补丁直接修改 body,不返回新对象。
|
|
9
5
|
*/
|
|
10
6
|
export function applyProviderPatches(body, provider) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
patchRouterSyntheticToolCalls(cloned);
|
|
14
|
-
if (!needsDeepSeekPatch(body, provider)) {
|
|
15
|
-
return { body: cloned, meta: { types: [] } };
|
|
7
|
+
if (needsDeepSeekPatch(body, provider)) {
|
|
8
|
+
applyDeepSeekPatches(body);
|
|
16
9
|
}
|
|
17
|
-
applyDeepSeekPatches(cloned);
|
|
18
|
-
return { body: cloned, meta: { types: ["deepseek_tool_use_to_text"] } };
|
|
19
10
|
}
|
|
20
11
|
/** DeepSeek patch 触发条件:直连 DeepSeek,或经代理转发且模型名含 deepseek */
|
|
21
12
|
function needsDeepSeekPatch(body, provider) {
|
|
@@ -16,6 +16,7 @@ import { TOOL_USE_ID_PREFIX, TOOL_USE_ID_PROVIDER_PREFIX } from "./enhancement/d
|
|
|
16
16
|
import { buildTransportFn } from "./transport-fn.js";
|
|
17
17
|
import { applyOverflowRedirect } from "./overflow.js";
|
|
18
18
|
import { applyProviderPatches } from "./patch/index.js";
|
|
19
|
+
import { loadEnhancementConfig } from "./enhancement-config.js";
|
|
19
20
|
const HTTP_ERROR_THRESHOLD = 400;
|
|
20
21
|
const MAX_LOG_FIELD_LENGTH = 80;
|
|
21
22
|
const UPSTREAM_ERROR_STATUS = 502;
|
|
@@ -73,9 +74,10 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
|
|
|
73
74
|
});
|
|
74
75
|
const clientModel = request.body.model || "unknown";
|
|
75
76
|
const sessionId = request.headers["x-claude-code-session-id"];
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
const enhancementConfig = loadEnhancementConfig(deps.db);
|
|
78
|
+
const { effectiveModel, originalModel, interceptResponse } = applyEnhancement(deps.db, request, clientModel, sessionId, enhancementConfig);
|
|
79
|
+
// --- 工具调用循环检测(受 proxy_enhancement 配置控制) ---
|
|
80
|
+
if (enhancementConfig.tool_call_loop_enabled && deps.sessionTracker && sessionId) {
|
|
79
81
|
const routerKeyId = request.routerKey?.id ?? null;
|
|
80
82
|
const sessionKey = routerKeyId ? `${routerKeyId}:${sessionId}` : sessionId;
|
|
81
83
|
const lastToolUse = extractLastToolUse(request.body);
|
|
@@ -119,11 +121,12 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
|
|
|
119
121
|
effectiveModel, originalModel,
|
|
120
122
|
originalBody: JSON.parse(JSON.stringify(request.body)),
|
|
121
123
|
sessionId,
|
|
124
|
+
streamLoopEnabled: enhancementConfig.stream_loop_enabled,
|
|
122
125
|
});
|
|
123
126
|
}
|
|
124
127
|
// ---------- Failover loop ----------
|
|
125
128
|
async function executeFailoverLoop(ctx) {
|
|
126
|
-
const { request, reply, apiType, upstreamPath, errors, deps, options, effectiveModel, originalModel, originalBody, sessionId } = ctx;
|
|
129
|
+
const { request, reply, apiType, upstreamPath, errors, deps, options, effectiveModel, originalModel, originalBody, sessionId, streamLoopEnabled } = ctx;
|
|
127
130
|
const excludeTargets = [];
|
|
128
131
|
let rootLogId = null;
|
|
129
132
|
while (true) {
|
|
@@ -133,7 +136,7 @@ async function executeFailoverLoop(ctx) {
|
|
|
133
136
|
rootLogId = logId;
|
|
134
137
|
const isFailoverIteration = rootLogId !== logId;
|
|
135
138
|
const routerKeyId = request.routerKey?.id ?? null;
|
|
136
|
-
|
|
139
|
+
const body = request.body;
|
|
137
140
|
const isStream = body.stream === true;
|
|
138
141
|
const cliHdrs = request.headers;
|
|
139
142
|
const rCtx = {
|
|
@@ -185,8 +188,7 @@ async function executeFailoverLoop(ctx) {
|
|
|
185
188
|
body.model = overflowResult.backend_model;
|
|
186
189
|
}
|
|
187
190
|
}
|
|
188
|
-
|
|
189
|
-
body = patchResult.body;
|
|
191
|
+
applyProviderPatches(body, provider);
|
|
190
192
|
const apiKey = decrypt(provider.api_key, getSetting(deps.db, "encryption_key"));
|
|
191
193
|
options?.beforeSendProxy?.(body, isStream);
|
|
192
194
|
const reqBodyStr = JSON.stringify(body);
|
|
@@ -200,6 +202,7 @@ async function executeFailoverLoop(ctx) {
|
|
|
200
202
|
provider, apiKey, body, cliHdrs, reply, upstreamPath, apiType,
|
|
201
203
|
isStream, startTime, logId, effectiveModel, originalModel,
|
|
202
204
|
streamTimeoutMs: deps.streamTimeoutMs, tracker: deps.tracker, matcher: deps.matcher, request,
|
|
205
|
+
streamLoopEnabled,
|
|
203
206
|
});
|
|
204
207
|
try {
|
|
205
208
|
const resilienceResult = await deps.orchestrator.handle(request, reply, apiType, { resolved, provider, clientModel: effectiveModel, isStream, trackerId: logId, sessionId, clientRequest: clientReq, concurrencyOverride }, { retryBaseDelayMs: deps.retryBaseDelayMs, isFailover, ruleMatcher: deps.matcher, transportFn });
|
|
@@ -294,12 +297,10 @@ function extractLastToolUse(body) {
|
|
|
294
297
|
if (msg.role === "assistant") {
|
|
295
298
|
const content = msg.content;
|
|
296
299
|
if (Array.isArray(content)) {
|
|
297
|
-
// 从后往前遍历,找到第一个非 router 合成的 tool_use
|
|
298
300
|
for (let j = content.length - 1; j >= 0; j--) {
|
|
299
301
|
const block = content[j];
|
|
300
302
|
if (block.type !== "tool_use")
|
|
301
303
|
continue;
|
|
302
|
-
// 跳过 router 生成的 synthetic tool_use(如 AskUserQuestion 的模型选择)
|
|
303
304
|
const id = block.id;
|
|
304
305
|
if (id && (id.startsWith(TOOL_USE_ID_PREFIX) || id.startsWith(TOOL_USE_ID_PROVIDER_PREFIX))) {
|
|
305
306
|
continue;
|
|
@@ -213,8 +213,6 @@ class StreamProxy {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
this.pipeEntry.write(chunk);
|
|
216
|
-
// loopGuard 由 SSEMetricsTransform 的 onContentDelta 回调驱动,
|
|
217
|
-
// 此处仅检查是否已触发(触发后终止流)
|
|
218
216
|
if (this.loopGuard?.isTriggered()) {
|
|
219
217
|
this.terminal("stream_abort", { metrics: this.collectMetrics(false) });
|
|
220
218
|
return;
|
|
@@ -1,11 +1,9 @@
|
|
|
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";
|
|
4
3
|
import type { RawHeaders, TransportResult } from "./types.js";
|
|
5
4
|
import type { Target } from "./strategy/types.js";
|
|
6
5
|
import type { RequestTracker } from "../monitor/request-tracker.js";
|
|
7
6
|
import type { RetryRuleMatcher } from "./retry-rules.js";
|
|
8
|
-
export declare function setLoopPreventionConfig(config: LoopPreventionConfig): void;
|
|
9
7
|
export interface TransportFnParams {
|
|
10
8
|
provider: NonNullable<ReturnType<typeof getProviderById>>;
|
|
11
9
|
apiKey: string;
|
|
@@ -23,5 +21,6 @@ export interface TransportFnParams {
|
|
|
23
21
|
tracker?: RequestTracker;
|
|
24
22
|
matcher?: RetryRuleMatcher;
|
|
25
23
|
request: FastifyRequest;
|
|
24
|
+
streamLoopEnabled: boolean;
|
|
26
25
|
}
|
|
27
26
|
export declare function buildTransportFn(p: TransportFnParams): (target: Target) => Promise<TransportResult>;
|
|
@@ -4,10 +4,12 @@ import { MetricsExtractor } from "../metrics/metrics-extractor.js";
|
|
|
4
4
|
import { buildUpstreamHeaders } from "./proxy-core.js";
|
|
5
5
|
import { StreamLoopGuard } from "./loop-prevention/stream-loop-guard.js";
|
|
6
6
|
import { NGramLoopDetector } from "./loop-prevention/detectors/ngram-detector.js";
|
|
7
|
-
import { DEFAULT_LOOP_PREVENTION_CONFIG } from "./loop-prevention/types.js";
|
|
8
7
|
import { UPSTREAM_SUCCESS } from "./types.js";
|
|
9
8
|
import { buildModelInfoTag } from "./enhancement/enhancement-handler.js";
|
|
10
9
|
import { DEFAULT_MAX_RAW as STREAM_CONTENT_MAX_RAW, DEFAULT_MAX_TEXT as STREAM_CONTENT_MAX_TEXT } from "../monitor/stream-content-accumulator.js";
|
|
10
|
+
const LOOP_DETECTOR_N = 6;
|
|
11
|
+
const LOOP_DETECTOR_WINDOW_SIZE = 1000;
|
|
12
|
+
const LOOP_DETECTOR_REPEAT_THRESHOLD = 10;
|
|
11
13
|
function toStreamMetrics(m) {
|
|
12
14
|
return {
|
|
13
15
|
inputTokens: m.input_tokens,
|
|
@@ -29,22 +31,14 @@ function toStreamMetrics(m) {
|
|
|
29
31
|
toolUseTokens: m.tool_use_tokens,
|
|
30
32
|
};
|
|
31
33
|
}
|
|
32
|
-
let _loopConfig;
|
|
33
|
-
export function setLoopPreventionConfig(config) {
|
|
34
|
-
_loopConfig = config;
|
|
35
|
-
}
|
|
36
|
-
function getLoopPreventionConfig() {
|
|
37
|
-
return _loopConfig ?? DEFAULT_LOOP_PREVENTION_CONFIG;
|
|
38
|
-
}
|
|
39
34
|
export function buildTransportFn(p) {
|
|
40
35
|
const buildHeaders = (cliHdrs, key, bytes) => buildUpstreamHeaders(cliHdrs, key, bytes, p.apiType);
|
|
41
36
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
42
37
|
return async (_target) => {
|
|
43
38
|
if (p.isStream) {
|
|
44
|
-
const loopPreventionConfig = getLoopPreventionConfig();
|
|
45
39
|
let streamLoopGuard;
|
|
46
|
-
if (
|
|
47
|
-
streamLoopGuard = new StreamLoopGuard(
|
|
40
|
+
if (p.streamLoopEnabled) {
|
|
41
|
+
streamLoopGuard = new StreamLoopGuard({ enabled: true, detectorConfig: { n: LOOP_DETECTOR_N, windowSize: LOOP_DETECTOR_WINDOW_SIZE, repeatThreshold: LOOP_DETECTOR_REPEAT_THRESHOLD } }, new NGramLoopDetector({ n: LOOP_DETECTOR_N, windowSize: LOOP_DETECTOR_WINDOW_SIZE, repeatThreshold: LOOP_DETECTOR_REPEAT_THRESHOLD }), (reason) => {
|
|
48
42
|
p.request.log.warn({ logId: p.logId, reason }, "Stream loop detected, aborting");
|
|
49
43
|
});
|
|
50
44
|
}
|
|
@@ -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-D4qBQ0nA.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-D4qBQ0nA.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-CxEXwaeM.js → CascadingModelSelect-BzDHmRLv.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-D4qBQ0nA.js";import{b as h}from"./Teleport-BHTgdtZR.js";import{a as g}from"./PopperContent-BbE-uPX0.js";import{n as _,r as v,t as y}from"./PopoverTrigger-D0nT5UVQ.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};
|