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.
Files changed (87) hide show
  1. package/config/recommended-retry-rules.json +1 -2
  2. package/dist/admin/proxy-enhancement.js +6 -0
  3. package/dist/config.d.ts +0 -2
  4. package/dist/config.js +0 -5
  5. package/dist/index.js +2 -9
  6. package/dist/metrics/sse-metrics-transform.d.ts +1 -1
  7. package/dist/metrics/sse-metrics-transform.js +1 -2
  8. package/dist/proxy/enhancement/enhancement-handler.d.ts +2 -1
  9. package/dist/proxy/enhancement/enhancement-handler.js +2 -2
  10. package/dist/proxy/enhancement-config.d.ts +2 -0
  11. package/dist/proxy/enhancement-config.js +4 -0
  12. package/dist/proxy/patch/deepseek/index.d.ts +1 -1
  13. package/dist/proxy/patch/deepseek/index.js +3 -3
  14. package/dist/proxy/patch/deepseek/patch-orphan-tool-results.d.ts +4 -3
  15. package/dist/proxy/patch/deepseek/patch-orphan-tool-results.js +30 -23
  16. package/dist/proxy/patch/deepseek/patch-thinking-blocks.d.ts +4 -8
  17. package/dist/proxy/patch/deepseek/patch-thinking-blocks.js +12 -57
  18. package/dist/proxy/patch/index.d.ts +3 -12
  19. package/dist/proxy/patch/index.js +4 -13
  20. package/dist/proxy/proxy-handler.js +10 -9
  21. package/dist/proxy/stream-proxy.js +0 -2
  22. package/dist/proxy/transport-fn.d.ts +1 -2
  23. package/dist/proxy/transport-fn.js +5 -11
  24. package/frontend-dist/assets/{CardContent-CpiBn1Oc.js → CardContent-DG1NiXMU.js} +1 -1
  25. package/frontend-dist/assets/{CardTitle-dwtgd_nl.js → CardTitle-6JvqGNd9.js} +1 -1
  26. package/frontend-dist/assets/{CascadingModelSelect-CxEXwaeM.js → CascadingModelSelect-BzDHmRLv.js} +1 -1
  27. package/frontend-dist/assets/{Checkbox-D2U4I-pO.js → Checkbox-D4TrUHCb.js} +1 -1
  28. package/frontend-dist/assets/{CollapsibleTrigger-B2AdbZBh.js → CollapsibleTrigger-ZJCGAkxi.js} +1 -1
  29. package/frontend-dist/assets/{Collection-BJZSFJsF.js → Collection-CRTZGViV.js} +1 -1
  30. package/frontend-dist/assets/{Dashboard-D3cDhJNh.js → Dashboard-C-B4p8HM.js} +1 -1
  31. package/frontend-dist/assets/{DialogTitle-BTuQdRm1.js → DialogTitle-CecSHUUq.js} +1 -1
  32. package/frontend-dist/assets/{Input-BYULYPCe.js → Input-DoCE9j9O.js} +1 -1
  33. package/frontend-dist/assets/{Label-sImW5XUw.js → Label-6sa_UFaw.js} +1 -1
  34. package/frontend-dist/assets/{Login-B0kGGZFi.js → Login-BkCJQFjz.js} +1 -1
  35. package/frontend-dist/assets/{Logs-BpAeeJRi.js → Logs-HG_BuJe5.js} +1 -1
  36. package/frontend-dist/assets/{ModelMappings-CsHLYqQB.js → ModelMappings-CjI27TDc.js} +1 -1
  37. package/frontend-dist/assets/{Monitor-BmMFWFJg.js → Monitor-CElP7aKi.js} +1 -1
  38. package/frontend-dist/assets/{PopoverTrigger-CIN3yOIw.js → PopoverTrigger-D0nT5UVQ.js} +1 -1
  39. package/frontend-dist/assets/{PopperContent-BoOYHCag.js → PopperContent-BbE-uPX0.js} +1 -1
  40. package/frontend-dist/assets/{Providers-DrepCc4A.js → Providers-DTqfl249.js} +1 -1
  41. package/frontend-dist/assets/ProxyEnhancement-DQixi_0_.js +5 -0
  42. package/frontend-dist/assets/{RetryRules-Ce5HfNcc.js → RetryRules-B5r18TFL.js} +1 -1
  43. package/frontend-dist/assets/{RouterKeys-CeSGvjll.js → RouterKeys-BwdoieS_.js} +1 -1
  44. package/frontend-dist/assets/{RovingFocusItem-gAYs0l8Z.js → RovingFocusItem-CpDEc1ox.js} +1 -1
  45. package/frontend-dist/assets/{Schedules-DphkPAWD.js → Schedules-C0q4rt97.js} +1 -1
  46. package/frontend-dist/assets/{SelectValue-Cbbd2Xbm.js → SelectValue-CKJWYmgi.js} +1 -1
  47. package/frontend-dist/assets/{Settings-DIP7VawX.js → Settings-DQN3_4Gx.js} +1 -1
  48. package/frontend-dist/assets/{Setup-_d_M-Qi6.js → Setup-CEk8SRJu.js} +1 -1
  49. package/frontend-dist/assets/{Switch-BmmYsqAx.js → Switch-ThwlPMEz.js} +1 -1
  50. package/frontend-dist/assets/{TableHeader-C1mpCsyo.js → TableHeader-UwRhaVOA.js} +1 -1
  51. package/frontend-dist/assets/{TabsTrigger-kN1usMvC.js → TabsTrigger-CNV5JhP6.js} +1 -1
  52. package/frontend-dist/assets/{Teleport-CRn-gy0B.js → Teleport-BHTgdtZR.js} +1 -1
  53. package/frontend-dist/assets/{TooltipTrigger-rlKo7E3A.js → TooltipTrigger-DKa4gPvX.js} +1 -1
  54. package/frontend-dist/assets/UnifiedRequestDialog-BjEigSaR.css +1 -0
  55. package/frontend-dist/assets/UnifiedRequestDialog-CBbZxE1N.js +3 -0
  56. package/frontend-dist/assets/{VisuallyHidden-CQvCw9gB.js → VisuallyHidden-r2E3heZY.js} +1 -1
  57. package/frontend-dist/assets/{VisuallyHiddenInput-B-DnnaWN.js → VisuallyHiddenInput-DSyTANlz.js} +1 -1
  58. package/frontend-dist/assets/{alert-dialog-M3PhwD75.js → alert-dialog-CNAhVHUE.js} +1 -1
  59. package/frontend-dist/assets/arrow-down-CFVTVH7t.js +1 -0
  60. package/frontend-dist/assets/{badge-7WIbGMsE.js → badge-RFq5LS37.js} +1 -1
  61. package/frontend-dist/assets/{button-q9xTxPJh.js → button-D4qBQ0nA.js} +2 -2
  62. package/frontend-dist/assets/check-CkLQpfOO.js +1 -0
  63. package/frontend-dist/assets/{copy-C1a8OrYP.js → copy-BxjNh73N.js} +1 -1
  64. package/frontend-dist/assets/{dialog-B6RnMgGx.js → dialog-CLepZTFY.js} +1 -1
  65. package/frontend-dist/assets/{file-text-CsuQUjXR.js → file-text-DqxNey63.js} +1 -1
  66. package/frontend-dist/assets/{index-Dh7qL0Qt.js → index-C4H_2b0G.js} +1 -1
  67. package/frontend-dist/assets/index-_Icfkt3I.css +1 -0
  68. package/frontend-dist/assets/{lib-D1G8Xa05.js → lib-SdzBxIwM.js} +1 -1
  69. package/frontend-dist/assets/loader-circle-Cffc9Uf0.js +1 -0
  70. package/frontend-dist/assets/{useClipboard-CBONMfzU.js → useClipboard-DBClUufY.js} +1 -1
  71. package/frontend-dist/assets/{useFocusGuards-DwFX8o1a.js → useFocusGuards-B99Wx8XA.js} +1 -1
  72. package/frontend-dist/assets/useFormControl-DuWttDd8.js +1 -0
  73. package/frontend-dist/assets/{useLogRetention-C2IbjXjr.js → useLogRetention-CQ7Q54Tt.js} +1 -1
  74. package/frontend-dist/assets/useNonce-D1Mva4rM.js +1 -0
  75. package/frontend-dist/assets/x-BJjJvWU8.js +1 -0
  76. package/frontend-dist/index.html +20 -20
  77. package/package.json +1 -1
  78. package/frontend-dist/assets/ProxyEnhancement-dK_mOQ3m.js +0 -5
  79. package/frontend-dist/assets/UnifiedRequestDialog-C-Ui-fav.css +0 -1
  80. package/frontend-dist/assets/UnifiedRequestDialog-C7pEDa9D.js +0 -4
  81. package/frontend-dist/assets/arrow-down-BHq-drH-.js +0 -1
  82. package/frontend-dist/assets/check-CccuM1Sj.js +0 -1
  83. package/frontend-dist/assets/index-Ce7hFHTt.css +0 -1
  84. package/frontend-dist/assets/loader-circle-CVhxR0Tt.js +0 -1
  85. package/frontend-dist/assets/useFormControl-DC32gW1A.js +0 -1
  86. package/frontend-dist/assets/useNonce-t7XaR4bX.js +0 -1
  87. 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
- const loopConfig = config.LOOP_PREVENTION ?? DEFAULT_LOOP_PREVENTION_CONFIG;
163
- const sessionTracker = new SessionTracker(loopConfig.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
- /** 提取 SSE 事件中的内容文本,触发 onContentDelta 回调 */
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
- /** 提取 SSE 事件中的内容文本,触发 onContentDelta 回调 */
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,6 +1,6 @@
1
1
  /**
2
2
  * 按序执行所有 DeepSeek 特定补丁。
3
- * DeepSeek tool 消息补丁先执行(转换 tool_use/tool_result),
3
+ * thinking 补丁先执行(影响 assistant 消息结构),
4
4
  * tool_result 配对修复后执行。
5
5
  */
6
6
  export declare function applyDeepSeekPatches(body: Record<string, unknown>): void;
@@ -1,11 +1,11 @@
1
- import { patchNonDeepSeekToolMessages } from "./patch-thinking-blocks.js";
1
+ import { patchMissingThinkingBlocks } from "./patch-thinking-blocks.js";
2
2
  import { patchOrphanToolResults } from "./patch-orphan-tool-results.js";
3
3
  /**
4
4
  * 按序执行所有 DeepSeek 特定补丁。
5
- * DeepSeek tool 消息补丁先执行(转换 tool_use/tool_result),
5
+ * thinking 补丁先执行(影响 assistant 消息结构),
6
6
  * tool_result 配对修复后执行。
7
7
  */
8
8
  export function applyDeepSeekPatches(body) {
9
- patchNonDeepSeekToolMessages(body);
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. tool_use_id 不在集合中的 tool_result 块转为 text(保留信息)
8
- * 3. 合并相邻的 user 消息(Anthropic API 不允许连续 user 消息)
9
- * 4. 合并相邻的 assistant 消息(同理)
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. tool_use_id 不在集合中的 tool_result 块转为 text(保留信息)
8
- * 3. 合并相邻的 user 消息(Anthropic API 不允许连续 user 消息)
9
- * 4. 合并相邻的 assistant 消息(同理)
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: 将孤儿 tool_result 块转为 text,而非丢弃
29
- let convertedAny = false;
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 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;
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
- else {
44
- newBlocks.push(block);
45
- }
46
- }
47
- if (changed) {
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 (!convertedAny)
47
+ if (!removedAny)
53
48
  return;
54
- // Step 3: 合并相邻的 user 消息(转换后可能产生连续 user 消息)
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 4: 合并相邻的 assistant 消息
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
- * 将非 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 规避格式校验,同时完整保留信息。
2
+ * DeepSeek thinking 协议实现不完整:开启 thinking 模式后部分轮次不返回 thinking block,
3
+ * 但后续请求要求历史 assistant 消息必须携带 thinking block。
4
+ * content 数组开头补一个空 thinking block 以绕过上游校验。
9
5
  */
10
- export declare function patchNonDeepSeekToolMessages(body: Record<string, unknown>): void;
6
+ export declare function patchMissingThinkingBlocks(body: Record<string, unknown>): void;
@@ -1,69 +1,24 @@
1
1
  /**
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 规避格式校验,同时完整保留信息。
2
+ * DeepSeek thinking 协议实现不完整:开启 thinking 模式后部分轮次不返回 thinking block,
3
+ * 但后续请求要求历史 assistant 消息必须携带 thinking block。
4
+ * content 数组开头补一个空 thinking block 以绕过上游校验。
9
5
  */
10
- export function patchNonDeepSeekToolMessages(body) {
6
+ export function patchMissingThinkingBlocks(body) {
11
7
  if (!body.messages)
12
8
  return;
13
9
  const messages = body.messages;
14
- if (!Array.isArray(messages) || messages.length === 0)
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 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
- }
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
- * 返回新的 body(必要时深拷贝),不修改原始 body。
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
- * 返回新的 body(必要时深拷贝),不修改原始 body。
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
- // 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: [] } };
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 { effectiveModel, originalModel, interceptResponse } = applyEnhancement(deps.db, request, clientModel, sessionId);
77
- // --- 工具调用循环检测 ---
78
- if (deps.sessionTracker && sessionId) {
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
- let body = request.body;
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
- const patchResult = applyProviderPatches(body, provider);
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 (loopPreventionConfig.enabled && loopPreventionConfig.stream.enabled) {
47
- streamLoopGuard = new StreamLoopGuard(loopPreventionConfig.stream, new NGramLoopDetector(loopPreventionConfig.stream.detectorConfig), (reason) => {
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-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
+ 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-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};
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};
@@ -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-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};
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};