llm-simple-router 0.5.0 → 0.5.2

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 (179) hide show
  1. package/config/recommended-providers.json +76 -0
  2. package/config/recommended-retry-rules.json +10 -0
  3. package/dist/admin/api-response.d.ts +27 -0
  4. package/dist/admin/api-response.js +40 -0
  5. package/dist/admin/constants.d.ts +0 -2
  6. package/dist/admin/constants.js +0 -3
  7. package/dist/admin/groups.js +9 -5
  8. package/dist/admin/logs.js +3 -2
  9. package/dist/admin/mappings.js +7 -6
  10. package/dist/admin/metrics.js +23 -5
  11. package/dist/admin/monitor.js +2 -1
  12. package/dist/admin/providers.js +13 -4
  13. package/dist/admin/proxy-enhancement.js +11 -6
  14. package/dist/admin/recommended.js +1 -9
  15. package/dist/admin/retry-rules.js +8 -4
  16. package/dist/admin/router-keys.js +5 -1
  17. package/dist/admin/routes.js +2 -0
  18. package/dist/admin/settings-import-export.js +3 -2
  19. package/dist/admin/settings.js +7 -5
  20. package/dist/admin/setup.js +3 -2
  21. package/dist/admin/stats.js +20 -3
  22. package/dist/admin/upgrade.d.ts +13 -0
  23. package/dist/admin/upgrade.js +114 -0
  24. package/dist/admin/usage.js +12 -24
  25. package/dist/config.d.ts +1 -1
  26. package/dist/config.js +1 -1
  27. package/dist/constants.d.ts +3 -0
  28. package/dist/constants.js +11 -0
  29. package/dist/db/index.d.ts +3 -3
  30. package/dist/db/index.js +2 -2
  31. package/dist/db/mappings.js +5 -8
  32. package/dist/db/metrics.js +3 -4
  33. package/dist/db/providers.d.ts +8 -0
  34. package/dist/db/providers.js +6 -0
  35. package/dist/db/retry-rules.d.ts +1 -0
  36. package/dist/db/retry-rules.js +3 -0
  37. package/dist/db/settings.d.ts +2 -0
  38. package/dist/db/settings.js +7 -0
  39. package/dist/db/stats.d.ts +1 -2
  40. package/dist/db/stats.js +7 -11
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +55 -34
  43. package/dist/metrics/metrics-extractor.js +1 -1
  44. package/dist/metrics/sse-parser.js +2 -0
  45. package/dist/middleware/admin-auth.js +6 -5
  46. package/dist/middleware/auth.js +1 -10
  47. package/dist/monitor/request-tracker.d.ts +1 -0
  48. package/dist/monitor/request-tracker.js +9 -45
  49. package/dist/monitor/runtime-collector.js +1 -1
  50. package/dist/monitor/stream-content-accumulator.d.ts +14 -0
  51. package/dist/monitor/stream-content-accumulator.js +58 -0
  52. package/dist/proxy/anthropic.d.ts +2 -1
  53. package/dist/proxy/anthropic.js +3 -3
  54. package/dist/proxy/enhancement/directive-parser.d.ts +18 -0
  55. package/dist/proxy/{directive-parser.js → enhancement/directive-parser.js} +44 -0
  56. package/dist/proxy/{enhancement-handler.js → enhancement/enhancement-handler.js} +152 -32
  57. package/dist/proxy/enhancement/index.d.ts +3 -0
  58. package/dist/proxy/enhancement/index.js +3 -0
  59. package/dist/proxy/{response-cleaner.js → enhancement/response-cleaner.js} +14 -0
  60. package/dist/proxy/log-helpers.d.ts +1 -1
  61. package/dist/proxy/mapping-resolver.js +4 -4
  62. package/dist/proxy/openai.d.ts +2 -1
  63. package/dist/proxy/openai.js +4 -4
  64. package/dist/proxy/orchestrator.d.ts +0 -1
  65. package/dist/proxy/orchestrator.js +1 -3
  66. package/dist/proxy/proxy-core.d.ts +0 -4
  67. package/dist/proxy/proxy-core.js +0 -2
  68. package/dist/proxy/proxy-handler.d.ts +1 -1
  69. package/dist/proxy/proxy-handler.js +52 -132
  70. package/dist/proxy/proxy-logging.d.ts +0 -2
  71. package/dist/proxy/proxy-logging.js +1 -3
  72. package/dist/proxy/resilience.d.ts +5 -2
  73. package/dist/proxy/resilience.js +16 -7
  74. package/dist/proxy/strategy/failover.js +2 -7
  75. package/dist/proxy/strategy/random.js +2 -2
  76. package/dist/proxy/strategy/round-robin.js +2 -2
  77. package/dist/proxy/strategy/scheduled.js +1 -8
  78. package/dist/proxy/strategy/targets-rule.d.ts +1 -0
  79. package/dist/proxy/strategy/targets-rule.js +5 -0
  80. package/dist/proxy/transport-fn.d.ts +25 -0
  81. package/dist/proxy/transport-fn.js +55 -0
  82. package/dist/proxy/transport.d.ts +0 -25
  83. package/dist/proxy/transport.js +0 -38
  84. package/dist/upgrade/checker.d.ts +25 -0
  85. package/dist/upgrade/checker.js +120 -0
  86. package/dist/upgrade/deployment.d.ts +2 -0
  87. package/dist/upgrade/deployment.js +20 -0
  88. package/dist/upgrade/version.d.ts +1 -0
  89. package/dist/upgrade/version.js +13 -0
  90. package/dist/utils/password.js +4 -2
  91. package/dist/utils/time-range.d.ts +9 -0
  92. package/dist/utils/time-range.js +40 -0
  93. package/frontend-dist/assets/CardContent-WrBnGhTg.js +1 -0
  94. package/frontend-dist/assets/CardTitle-BcDYk7cq.js +1 -0
  95. package/frontend-dist/assets/Checkbox-MZf0YsDG.js +1 -0
  96. package/frontend-dist/assets/CollapsibleTrigger-CrOH9HlW.js +1 -0
  97. package/frontend-dist/assets/Collection-DcTx_Y54.js +1 -0
  98. package/frontend-dist/assets/Dashboard-D0oDrSLr.js +3 -0
  99. package/frontend-dist/assets/DialogTitle-Cl5Cd7QH.js +1 -0
  100. package/frontend-dist/assets/{Input-l5ZurXX5.js → Input-O0ebU-Va.js} +1 -1
  101. package/frontend-dist/assets/Label-C_S0y7Um.js +1 -0
  102. package/frontend-dist/assets/Login-DGY7uF8P.js +1 -0
  103. package/frontend-dist/assets/Logs-ls8pv89b.js +1 -0
  104. package/frontend-dist/assets/ModelMappings-DGlf0S4s.js +1 -0
  105. package/frontend-dist/assets/Monitor-BSI87grz.js +1 -0
  106. package/frontend-dist/assets/PopperContent-C6Q7hDmf.js +1 -0
  107. package/frontend-dist/assets/Providers-ZkRpj8_m.js +1 -0
  108. package/frontend-dist/assets/ProxyEnhancement-DFPI1W6Z.js +5 -0
  109. package/frontend-dist/assets/RetryRules-DtM31qsl.js +1 -0
  110. package/frontend-dist/assets/RouterKeys-D63tRFKm.js +1 -0
  111. package/frontend-dist/assets/RovingFocusItem-BJoylAKU.js +1 -0
  112. package/frontend-dist/assets/SelectValue-CLp5z6_I.js +1 -0
  113. package/frontend-dist/assets/Settings-DSgRKbTQ.js +6 -0
  114. package/frontend-dist/assets/Setup-BDmj6CRk.js +1 -0
  115. package/frontend-dist/assets/Switch-Wz-t_zkv.js +1 -0
  116. package/frontend-dist/assets/TableHeader-DGtcqGkw.js +1 -0
  117. package/frontend-dist/assets/TabsTrigger-CPCi2HIa.js +1 -0
  118. package/frontend-dist/assets/Teleport-DdjYHlNK.js +3 -0
  119. package/frontend-dist/assets/TooltipTrigger-H_QoPY1n.js +1 -0
  120. package/frontend-dist/assets/UnifiedRequestDialog-BAAfMJJl.js +3 -0
  121. package/frontend-dist/assets/{VisuallyHidden-BwwTtzb9.js → VisuallyHidden-Cyk-jWwh.js} +1 -1
  122. package/frontend-dist/assets/VisuallyHiddenInput-CYjNe_H8.js +1 -0
  123. package/frontend-dist/assets/alert-dialog-Bi3dliLl.js +1 -0
  124. package/frontend-dist/assets/badge-Kkta3e9W.js +1 -0
  125. package/frontend-dist/assets/button-BQ3s7yNh.js +12 -0
  126. package/frontend-dist/assets/{createLucideIcon-Biq59l_W.js → createLucideIcon-D1tkPDOQ.js} +1 -1
  127. package/frontend-dist/assets/dialog-DoIATUYw.js +1 -0
  128. package/frontend-dist/assets/{file-text-DoRW0hQW.js → file-text-Dt6QP1bZ.js} +1 -1
  129. package/frontend-dist/assets/index-BY0E7CHR.js +1 -0
  130. package/frontend-dist/assets/index-Bnrh1mFY.css +1 -0
  131. package/frontend-dist/assets/lib-CxwxnlwW.js +1 -0
  132. package/frontend-dist/assets/{ohash.D__AXeF1-BGxYMs6k.js → ohash.D__AXeF1-b0PiKZB_.js} +1 -1
  133. package/frontend-dist/assets/{useClipboard-vaHkvJHw.js → useClipboard-Cnnz6AAN.js} +1 -1
  134. package/frontend-dist/assets/useLogRetention-DYP5LOAc.js +1 -0
  135. package/frontend-dist/assets/useNonce-DKbOCfgM.js +1 -0
  136. package/frontend-dist/assets/x-CAoitXRt.js +1 -0
  137. package/frontend-dist/index.html +18 -9
  138. package/package.json +2 -1
  139. package/dist/proxy/directive-parser.d.ts +0 -7
  140. package/frontend-dist/assets/CardContent-CIO85eT6.js +0 -1
  141. package/frontend-dist/assets/CardTitle-DiqIReMT.js +0 -1
  142. package/frontend-dist/assets/Checkbox-C2u5pIp4.js +0 -1
  143. package/frontend-dist/assets/CollapsibleTrigger-RKFL41om.js +0 -1
  144. package/frontend-dist/assets/Collection-iiNnuTQj.js +0 -1
  145. package/frontend-dist/assets/Dashboard-DOEqP6gF.js +0 -3
  146. package/frontend-dist/assets/DialogTitle-CEqndrf6.js +0 -1
  147. package/frontend-dist/assets/Label-PgGtS8v2.js +0 -1
  148. package/frontend-dist/assets/Login-DaN6ZcCx.js +0 -1
  149. package/frontend-dist/assets/Logs-CleRQ7Xk.js +0 -1
  150. package/frontend-dist/assets/ModelMappings-CacA_ua_.js +0 -1
  151. package/frontend-dist/assets/Monitor-LSMFOBN2.js +0 -1
  152. package/frontend-dist/assets/PopperContent-zLFHqQP0.js +0 -1
  153. package/frontend-dist/assets/Providers-NT5MUDU0.js +0 -1
  154. package/frontend-dist/assets/ProxyEnhancement-DhOy8nNy.js +0 -5
  155. package/frontend-dist/assets/RetryRules-7arWa3jB.js +0 -1
  156. package/frontend-dist/assets/RouterKeys-CdaZunRg.js +0 -1
  157. package/frontend-dist/assets/SelectValue-CSg-MKW_.js +0 -1
  158. package/frontend-dist/assets/Settings-1ntV9XE3.js +0 -6
  159. package/frontend-dist/assets/Setup-CXLTDhYJ.js +0 -1
  160. package/frontend-dist/assets/Switch-DivrIFE3.js +0 -1
  161. package/frontend-dist/assets/TableHeader-Bn0bodWx.js +0 -1
  162. package/frontend-dist/assets/TabsContent-MWvOH_LJ.js +0 -1
  163. package/frontend-dist/assets/TabsTrigger-WKkUfO2M.js +0 -1
  164. package/frontend-dist/assets/Teleport-B0PNXZbP.js +0 -3
  165. package/frontend-dist/assets/UnifiedRequestDialog-Ba2e7YuJ.js +0 -3
  166. package/frontend-dist/assets/VisuallyHiddenInput-EGZSP7s8.js +0 -1
  167. package/frontend-dist/assets/alert-dialog-CS1yFhdV.js +0 -1
  168. package/frontend-dist/assets/badge-C-QcC5n2.js +0 -1
  169. package/frontend-dist/assets/button-Dbz2Be22.js +0 -12
  170. package/frontend-dist/assets/dialog-Cr0YQlLW.js +0 -1
  171. package/frontend-dist/assets/index-0H2uCGbx.js +0 -1
  172. package/frontend-dist/assets/index-D-cdVNCb.css +0 -1
  173. package/frontend-dist/assets/lib-B0lieqgg.js +0 -1
  174. package/frontend-dist/assets/useForwardExpose-C2_ks3sW.js +0 -1
  175. package/frontend-dist/assets/useLogRetention-Cs_fiKql.js +0 -1
  176. package/frontend-dist/assets/useNonce-C9do0jOI.js +0 -1
  177. package/frontend-dist/assets/x-BlTnH_0_.js +0 -1
  178. /package/dist/proxy/{enhancement-handler.d.ts → enhancement/enhancement-handler.d.ts} +0 -0
  179. /package/dist/proxy/{response-cleaner.d.ts → enhancement/response-cleaner.d.ts} +0 -0
@@ -1,9 +1,9 @@
1
1
  import { randomUUID } from "crypto";
2
- import { getSetting } from "../db/settings.js";
3
- import { getActiveProviderModels, resolveByProviderModel } from "../db/index.js";
4
- import { resolveMapping } from "./mapping-resolver.js";
5
- import { parseDirective } from "./directive-parser.js";
6
- import { modelState } from "./model-state.js";
2
+ import { getSetting } from "../../db/settings.js";
3
+ import { getActiveProviderModels, resolveByProviderModel } from "../../db/index.js";
4
+ import { resolveMapping } from "../mapping-resolver.js";
5
+ import { parseDirective, parseToolResult, TOOL_USE_ID_PREFIX } from "./directive-parser.js";
6
+ import { modelState } from "../model-state.js";
7
7
  import { cleanRouterResponses } from "./response-cleaner.js";
8
8
  const MODEL_INFO_TAG_TYPE = "model-info";
9
9
  /**
@@ -17,6 +17,42 @@ function resolveProviderModel(db, providerSlashModel) {
17
17
  const resolved = resolveByProviderModel(db, match[1], match[2]);
18
18
  return resolved?.client_model ?? null;
19
19
  }
20
+ /** 检查请求中是否包含 AskUserQuestion 工具(判断客户端是否为 Claude Code) */
21
+ function hasAskUserQuestion(body) {
22
+ const tools = body.tools;
23
+ if (!tools)
24
+ return false;
25
+ return tools.some(t => t.name === "AskUserQuestion");
26
+ }
27
+ /**
28
+ * 获取去重后的 provider/backend_model 显示列表,按 allowed_models 过滤。
29
+ * 供 buildSelectModelResponse 和 AskUserQuestion 路径复用。
30
+ */
31
+ function buildDisplayModels(db, allowedModelsRaw) {
32
+ const providerModels = getActiveProviderModels(db);
33
+ let allowedSet = null;
34
+ if (allowedModelsRaw) {
35
+ try {
36
+ const parsed = JSON.parse(allowedModelsRaw).filter((s) => s.trim() !== "");
37
+ if (parsed.length > 0)
38
+ allowedSet = new Set(parsed);
39
+ }
40
+ catch { /* 解析失败时不做过滤 */ }
41
+ }
42
+ const filtered = allowedSet
43
+ ? providerModels.filter(m => allowedSet.has(m.backend_model))
44
+ : providerModels;
45
+ const seen = new Set();
46
+ const displayModels = [];
47
+ for (const m of filtered) {
48
+ const key = `${m.provider_name}/${m.backend_model}`;
49
+ if (!seen.has(key)) {
50
+ seen.add(key);
51
+ displayModels.push(key);
52
+ }
53
+ }
54
+ return displayModels;
55
+ }
20
56
  /**
21
57
  * 在代理转发前应用代理增强逻辑(指令解析 + 会话记忆 + 模型替换 + 命令拦截)。
22
58
  * 仅当 proxy_enhancement.claude_code_enabled 开启时生效。
@@ -34,6 +70,35 @@ export function applyEnhancement(db, request, clientModel, sessionId) {
34
70
  if (enhancement?.claude_code_enabled !== true) {
35
71
  return nullResult;
36
72
  }
73
+ // 检测 AskUserQuestion 的 tool_result 回调(用户在 UI 上选择了模型)
74
+ const toolResult = parseToolResult(request.body);
75
+ if (toolResult.isRouterToolResult) {
76
+ const routerKeyId = request.routerKey?.id ?? null;
77
+ if (toolResult.selectedModel) {
78
+ const resolvedClientModel = resolveProviderModel(db, toolResult.selectedModel);
79
+ if (resolvedClientModel) {
80
+ modelState.set(routerKeyId, toolResult.selectedModel, sessionId, clientModel, "command");
81
+ return {
82
+ effectiveModel: toolResult.selectedModel,
83
+ originalModel: null,
84
+ interceptResponse: {
85
+ ...buildTextResponse("model-selected", `已选择模型: ${toolResult.selectedModel}`),
86
+ meta: { action: "模型选择", detail: toolResult.selectedModel },
87
+ },
88
+ };
89
+ }
90
+ return {
91
+ effectiveModel: clientModel,
92
+ originalModel: null,
93
+ interceptResponse: {
94
+ ...buildTextResponse("error", `未找到模型: ${toolResult.selectedModel}`),
95
+ meta: { action: "模型选择失败", detail: toolResult.selectedModel },
96
+ },
97
+ };
98
+ }
99
+ // tool_result 匹配前缀但无法提取模型名,降级处理
100
+ return nullResult;
101
+ }
37
102
  // 清理历史消息中的 <router-response> 标签
38
103
  const cleaned = cleanRouterResponses(request.body);
39
104
  request.body.messages = cleaned.messages;
@@ -67,6 +132,27 @@ export function applyEnhancement(db, request, clientModel, sessionId) {
67
132
  };
68
133
  }
69
134
  // 无参数:返回模型列表
135
+ if (hasAskUserQuestion(request.body)) {
136
+ const displayModels = buildDisplayModels(db, request.routerKey?.allowed_models ?? null);
137
+ if (displayModels.length === 0) {
138
+ return {
139
+ effectiveModel: clientModel,
140
+ originalModel: null,
141
+ interceptResponse: {
142
+ ...buildTextResponse("model-list", "(无可用模型)"),
143
+ meta: { action: "模型列表" },
144
+ },
145
+ };
146
+ }
147
+ return {
148
+ effectiveModel: clientModel,
149
+ originalModel: null,
150
+ interceptResponse: {
151
+ ...buildAskUserQuestionResponse(displayModels),
152
+ meta: { action: "模型列表(AskUserQuestion)" },
153
+ },
154
+ };
155
+ }
70
156
  return {
71
157
  effectiveModel: clientModel,
72
158
  originalModel: null,
@@ -119,34 +205,9 @@ function buildTextResponse(type, inner) {
119
205
  };
120
206
  return { statusCode: 200, body };
121
207
  }
122
- /** 查询所有可用的 provider_model 并构造响应 */
208
+ /** 查询所有可用的 provider_model 并构造文本列表响应 */
123
209
  function buildSelectModelResponse(db, allowedModelsRaw, selectedModel) {
124
- const providerModels = getActiveProviderModels(db);
125
- // 按 allowed_models 过滤(allowed_models 存储的是 client_model 列表)
126
- let allowedSet = null;
127
- if (allowedModelsRaw) {
128
- try {
129
- const parsed = JSON.parse(allowedModelsRaw).filter((s) => s.trim() !== "");
130
- if (parsed.length > 0)
131
- allowedSet = new Set(parsed);
132
- }
133
- catch {
134
- return buildTextResponse("model-list", "(解析 allowed_models 失败)");
135
- }
136
- }
137
- const filtered = allowedSet
138
- ? providerModels.filter(m => allowedSet.has(m.backend_model))
139
- : providerModels;
140
- // 去重并格式化为 "provider_name/backend_model"
141
- const seen = new Set();
142
- const displayModels = [];
143
- for (const m of filtered) {
144
- const key = `${m.provider_name}/${m.backend_model}`;
145
- if (!seen.has(key)) {
146
- seen.add(key);
147
- displayModels.push(key);
148
- }
149
- }
210
+ const displayModels = buildDisplayModels(db, allowedModelsRaw);
150
211
  let inner;
151
212
  let responseType;
152
213
  if (selectedModel) {
@@ -163,6 +224,65 @@ function buildSelectModelResponse(db, allowedModelsRaw, selectedModel) {
163
224
  }
164
225
  return buildTextResponse(responseType, inner);
165
226
  }
227
+ /** 将模型列表分块为 AskUserQuestion 的 questions(每组 2-4 个选项,最多 4 组) */
228
+ function buildModelQuestions(models) {
229
+ if (models.length <= 4) {
230
+ const options = models.map(m => {
231
+ const sep = m.indexOf("/");
232
+ const provider = sep > 0 ? m.substring(0, sep) : "";
233
+ return { label: m, description: provider || "模型" };
234
+ });
235
+ if (options.length === 1) {
236
+ options.push({ label: "保持当前", description: "不切换模型" });
237
+ }
238
+ return [{
239
+ question: "请选择要使用的模型",
240
+ header: "模型选择",
241
+ options,
242
+ multiSelect: false,
243
+ }];
244
+ }
245
+ // 均匀分配到最多 4 个 question,每个 2-4 个选项
246
+ const numChunks = Math.min(4, Math.ceil(models.length / 4));
247
+ const chunks = Array.from({ length: numChunks }, () => []);
248
+ for (let i = 0; i < models.length; i++) {
249
+ chunks[i % numChunks].push(models[i]);
250
+ }
251
+ return chunks.map((chunk, idx) => ({
252
+ question: "请选择要使用的模型",
253
+ header: idx === 0 ? "模型选择" : "更多模型",
254
+ options: chunk.map(m => {
255
+ const sep = m.indexOf("/");
256
+ const provider = sep > 0 ? m.substring(0, sep) : "";
257
+ return { label: m, description: provider || "模型" };
258
+ }),
259
+ multiSelect: false,
260
+ }));
261
+ }
262
+ /** 构造 AskUserQuestion synthetic tool_use 响应 */
263
+ function buildAskUserQuestionResponse(displayModels) {
264
+ const capped = displayModels.slice(0, 16);
265
+ const questions = buildModelQuestions(capped);
266
+ const toolUseId = `${TOOL_USE_ID_PREFIX}${randomUUID()}`;
267
+ return {
268
+ statusCode: 200,
269
+ body: {
270
+ id: `msg-${randomUUID()}`,
271
+ type: "message",
272
+ role: "assistant",
273
+ content: [{
274
+ type: "tool_use",
275
+ id: toolUseId,
276
+ name: "AskUserQuestion",
277
+ input: { questions },
278
+ }],
279
+ model: "router",
280
+ stop_reason: "tool_use",
281
+ stop_sequence: null,
282
+ usage: { input_tokens: 0, output_tokens: 0 },
283
+ },
284
+ };
285
+ }
166
286
  /** 生成注入到非流式响应中的模型信息标签 */
167
287
  export function buildModelInfoTag(effectiveModel) {
168
288
  return `<router-response type="${MODEL_INFO_TAG_TYPE}">当前模型: ${effectiveModel}</router-response>`;
@@ -0,0 +1,3 @@
1
+ export { applyEnhancement, buildModelInfoTag } from "./enhancement-handler.js";
2
+ export { parseDirective, parseToolResult, TOOL_USE_ID_PREFIX } from "./directive-parser.js";
3
+ export { cleanRouterResponses } from "./response-cleaner.js";
@@ -0,0 +1,3 @@
1
+ export { applyEnhancement, buildModelInfoTag } from "./enhancement-handler.js";
2
+ export { parseDirective, parseToolResult, TOOL_USE_ID_PREFIX } from "./directive-parser.js";
3
+ export { cleanRouterResponses } from "./response-cleaner.js";
@@ -1,3 +1,4 @@
1
+ import { TOOL_USE_ID_PREFIX } from "./directive-parser.js";
1
2
  const RE_ROUTER_RESPONSE = /<router-response[^>]*>[\s\S]*?<\/router-response>/g;
2
3
  const RE_COMMAND = /\[router-command:/;
3
4
  /**
@@ -30,9 +31,22 @@ export function cleanRouterResponses(body) {
30
31
  return false;
31
32
  }
32
33
  }
34
+ // 清理 router synthetic AskUserQuestion 的 tool_result 回调
35
+ const toolResultBlocks = blocks.filter((b) => b && typeof b === "object" && b.type === "tool_result"
36
+ && typeof b.tool_use_id === "string"
37
+ && b.tool_use_id.startsWith(TOOL_USE_ID_PREFIX));
38
+ if (toolResultBlocks.length > 0 && toolResultBlocks.length === blocks.length)
39
+ return false;
33
40
  }
34
41
  if (msg.role === "assistant") {
35
42
  const blocks = Array.isArray(msg.content) ? msg.content : [msg.content];
43
+ // 清理 router synthetic AskUserQuestion 的 tool_use 消息
44
+ const toolUseBlocks = blocks.filter((b) => b && typeof b === "object" && b.type === "tool_use"
45
+ && b.name === "AskUserQuestion"
46
+ && typeof b.id === "string"
47
+ && b.id.startsWith(TOOL_USE_ID_PREFIX));
48
+ if (toolUseBlocks.length > 0 && toolUseBlocks.length === blocks.length)
49
+ return false;
36
50
  const texts = blocks
37
51
  .filter((b) => b?.type === "text" && typeof b.text === "string")
38
52
  .map((b) => b.text);
@@ -1,6 +1,6 @@
1
1
  import Database from "better-sqlite3";
2
2
  import type { Provider } from "../db/index.js";
3
- import type { RawHeaders } from "./proxy-core.js";
3
+ import type { RawHeaders } from "./types.js";
4
4
  export interface FailoverContext {
5
5
  isFailoverIteration: boolean;
6
6
  rootLogId: string;
@@ -3,7 +3,7 @@ import { ScheduledStrategy } from "./strategy/scheduled.js";
3
3
  import { RoundRobinStrategy } from "./strategy/round-robin.js";
4
4
  import { RandomStrategy } from "./strategy/random.js";
5
5
  import { FailoverStrategy } from "./strategy/failover.js";
6
- import { getMappingGroup } from "../db/index.js";
6
+ import { getMappingGroup, getActiveProviderByName, getActiveProvidersWithModels } from "../db/index.js";
7
7
  // 策略注册表:key 为数据库中 mapping_groups.strategy 字段的值。
8
8
  // 新增策略时:
9
9
  // 1. 在 src/proxy/strategy/ 下创建实现文件
@@ -21,7 +21,7 @@ export function resolveMapping(db, clientModel, context) {
21
21
  if (slashMatch) {
22
22
  const providerName = slashMatch[1];
23
23
  const backendModel = slashMatch[2];
24
- const provider = db.prepare("SELECT id, models FROM providers WHERE name = ? AND is_active = 1").get(providerName);
24
+ const provider = getActiveProviderByName(db, providerName);
25
25
  if (provider) {
26
26
  try {
27
27
  const models = JSON.parse(provider.models);
@@ -39,7 +39,7 @@ export function resolveMapping(db, clientModel, context) {
39
39
  const group = getMappingGroup(db, clientModel);
40
40
  if (!group) {
41
41
  // Fallback: 没有 mapping group 时,直接查 provider 的 models 字段
42
- const providers = db.prepare("SELECT id, models FROM providers WHERE is_active = 1").all();
42
+ const providers = getActiveProvidersWithModels(db);
43
43
  for (const p of providers) {
44
44
  try {
45
45
  const models = JSON.parse(p.models);
@@ -48,7 +48,7 @@ export function resolveMapping(db, clientModel, context) {
48
48
  }
49
49
  }
50
50
  catch {
51
- break;
51
+ continue;
52
52
  }
53
53
  }
54
54
  return null;
@@ -3,13 +3,14 @@ import Database from "better-sqlite3";
3
3
  import { RetryRuleMatcher } from "./retry-rules.js";
4
4
  import { ProviderSemaphoreManager } from "./semaphore.js";
5
5
  import type { RequestTracker } from "../monitor/request-tracker.js";
6
+ import type { UsageWindowTracker } from "./usage-window-tracker.js";
6
7
  export interface OpenaiProxyOptions {
7
8
  db: Database.Database;
8
9
  streamTimeoutMs: number;
9
- retryMaxAttempts: number;
10
10
  retryBaseDelayMs: number;
11
11
  matcher?: RetryRuleMatcher;
12
12
  semaphoreManager?: ProviderSemaphoreManager;
13
13
  tracker?: RequestTracker;
14
+ usageWindowTracker?: UsageWindowTracker;
14
15
  }
15
16
  export declare const openaiProxy: FastifyPluginCallback<OpenaiProxyOptions>;
@@ -19,15 +19,15 @@ const OPENAI_ERROR_META = {
19
19
  };
20
20
  const openaiErrors = createErrorFormatter((kind, message) => ({ error: { message, ...OPENAI_ERROR_META[kind] } }));
21
21
  function sendError(reply, e) {
22
- return reply.status(e.statusCode).send(e.body);
22
+ return reply.code(e.statusCode).send(e.body);
23
23
  }
24
24
  const openaiProxyRaw = (app, opts, done) => {
25
- const { db, streamTimeoutMs, retryMaxAttempts, retryBaseDelayMs, matcher, semaphoreManager, tracker } = opts;
25
+ const { db, streamTimeoutMs, retryBaseDelayMs, matcher, semaphoreManager, tracker, usageWindowTracker } = opts;
26
26
  const orchestrator = createOrchestrator(semaphoreManager, tracker);
27
27
  app.post(CHAT_COMPLETIONS_PATH, async (request, reply) => {
28
28
  if (!orchestrator)
29
29
  return sendError(reply, openaiErrors.providerUnavailable());
30
- const deps = { db, streamTimeoutMs, retryMaxAttempts, retryBaseDelayMs, matcher, tracker, orchestrator };
30
+ const deps = { db, streamTimeoutMs, retryBaseDelayMs, matcher, tracker, orchestrator, usageWindowTracker };
31
31
  return handleProxyRequest(request, reply, "openai", CHAT_COMPLETIONS_PATH, openaiErrors, deps, {
32
32
  beforeSendProxy: (body, isStream) => {
33
33
  if (isStream && !body.stream_options) {
@@ -50,7 +50,7 @@ const openaiProxyRaw = (app, opts, done) => {
50
50
  const result = await proxyGetRequest(provider, apiKey, cliHdrs, MODELS_PATH);
51
51
  for (const [k, v] of Object.entries(result.headers))
52
52
  reply.header(k, v);
53
- return reply.status(result.statusCode).send(result.body);
53
+ return reply.code(result.statusCode).send(result.body);
54
54
  }
55
55
  catch (err) {
56
56
  request.log.error({ err: err instanceof Error ? err.message : String(err) }, "Failed to reach OpenAI backend for /v1/models");
@@ -28,7 +28,6 @@ export interface OrchestratorConfig {
28
28
  }
29
29
  export interface HandleContext {
30
30
  streamTimeoutMs?: number;
31
- retryMaxAttempts?: number;
32
31
  retryBaseDelayMs?: number;
33
32
  failoverThreshold?: number;
34
33
  isFailover?: boolean;
@@ -1,7 +1,6 @@
1
1
  import { ResilienceLayer as ResilienceLayerClass } from "./resilience.js";
2
2
  import { SemaphoreScope as SemaphoreScopeClass } from "./scope.js";
3
3
  import { TrackerScope as TrackerScopeClass } from "./scope.js";
4
- const DEFAULT_MAX_RETRIES = 3;
5
4
  const DEFAULT_BASE_DELAY_MS = 1000;
6
5
  const DEFAULT_FAILOVER_THRESHOLD = 400;
7
6
  /**
@@ -66,7 +65,6 @@ export class ProxyOrchestrator {
66
65
  if (!ctx?.transportFn)
67
66
  throw new Error("HandleContext.transportFn is required");
68
67
  const resilienceConfig = {
69
- maxRetries: ctx.retryMaxAttempts ?? DEFAULT_MAX_RETRIES,
70
68
  baseDelayMs: ctx.retryBaseDelayMs ?? DEFAULT_BASE_DELAY_MS,
71
69
  failoverThreshold: ctx.failoverThreshold ?? DEFAULT_FAILOVER_THRESHOLD,
72
70
  isFailover: ctx.isFailover ?? false,
@@ -87,7 +85,7 @@ export class ProxyOrchestrator {
87
85
  reply.header(key, value);
88
86
  }
89
87
  }
90
- reply.status(result.statusCode).send(result.body);
88
+ reply.code(result.statusCode).send(result.body);
91
89
  }
92
90
  extractTrackStatus(result) {
93
91
  const transport = result.result;
@@ -1,8 +1,6 @@
1
1
  import type { Provider } from "../db/index.js";
2
2
  import type { GetTransportResult } from "./transport.js";
3
3
  import type { RawHeaders } from "./types.js";
4
- export { UPSTREAM_SUCCESS } from "./types.js";
5
- export type { RawHeaders } from "./types.js";
6
4
  export interface ProxyErrorResponse {
7
5
  statusCode: number;
8
6
  body: unknown;
@@ -16,8 +14,6 @@ export interface ProxyErrorFormatter {
16
14
  concurrencyQueueFull(providerId: string): ProxyErrorResponse;
17
15
  concurrencyTimeout(providerId: string, timeoutMs: number): ProxyErrorResponse;
18
16
  }
19
- export type { ProxyResult, StreamProxyResult } from "./transport.js";
20
- export type { GetTransportResult as GetProxyResult } from "./transport.js";
21
17
  export type ErrorKind = "modelNotFound" | "modelNotAllowed" | "providerUnavailable" | "providerTypeMismatch" | "upstreamConnectionFailed" | "concurrencyQueueFull" | "concurrencyTimeout";
22
18
  /**
23
19
  * 工厂函数,消除 openai/anthropic 错误格式化的重复代码。
@@ -1,6 +1,4 @@
1
1
  import { callGet as upstreamGet } from "./transport.js";
2
- // Re-export for external consumers (openai.ts, anthropic.ts, etc.)
3
- export { UPSTREAM_SUCCESS } from "./types.js";
4
2
  /**
5
3
  * 工厂函数,消除 openai/anthropic 错误格式化的重复代码。
6
4
  * statusCode 和 message 两个 provider 完全一致,仅 body 格式不同,
@@ -7,11 +7,11 @@ import type { ProxyErrorFormatter } from "./proxy-core.js";
7
7
  export interface RouteHandlerDeps {
8
8
  db: Database.Database;
9
9
  streamTimeoutMs: number;
10
- retryMaxAttempts: number;
11
10
  retryBaseDelayMs: number;
12
11
  matcher?: RetryRuleMatcher;
13
12
  tracker?: RequestTracker;
14
13
  orchestrator: ProxyOrchestrator;
14
+ usageWindowTracker?: import("./usage-window-tracker.js").UsageWindowTracker;
15
15
  }
16
16
  export declare function handleProxyRequest(request: FastifyRequest, reply: FastifyReply, apiType: "openai" | "anthropic", upstreamPath: string, errors: ProxyErrorFormatter, deps: RouteHandlerDeps, options?: {
17
17
  beforeSendProxy?: (body: Record<string, unknown>, isStream: boolean) => void;