llm-simple-router 0.6.7 → 0.7.1

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 (182) hide show
  1. package/README.md +69 -0
  2. package/dist/admin/constants.d.ts +1 -1
  3. package/dist/admin/constants.js +2 -2
  4. package/dist/admin/logs.d.ts +2 -0
  5. package/dist/admin/logs.js +17 -1
  6. package/dist/admin/providers.d.ts +2 -2
  7. package/dist/admin/providers.js +29 -16
  8. package/dist/admin/proxy-enhancement.d.ts +2 -0
  9. package/dist/admin/proxy-enhancement.js +4 -10
  10. package/dist/admin/retry-rules.d.ts +2 -2
  11. package/dist/admin/retry-rules.js +4 -8
  12. package/dist/admin/routes.d.ts +5 -4
  13. package/dist/admin/routes.js +7 -7
  14. package/dist/admin/settings-import-export.d.ts +2 -4
  15. package/dist/admin/settings-import-export.js +9 -19
  16. package/dist/admin/settings.d.ts +1 -0
  17. package/dist/admin/settings.js +29 -1
  18. package/dist/admin/upgrade.d.ts +1 -0
  19. package/dist/admin/upgrade.js +37 -4
  20. package/dist/{constants.d.ts → core/constants.d.ts} +4 -1
  21. package/dist/{constants.js → core/constants.js} +21 -1
  22. package/dist/core/container.d.ts +31 -0
  23. package/dist/core/container.js +41 -0
  24. package/dist/core/errors.d.ts +26 -0
  25. package/dist/core/errors.js +42 -0
  26. package/dist/core/registry.d.ts +43 -0
  27. package/dist/core/registry.js +3 -0
  28. package/dist/core/types.d.ts +105 -0
  29. package/dist/core/types.js +3 -0
  30. package/dist/db/index.d.ts +1 -1
  31. package/dist/db/index.js +1 -1
  32. package/dist/db/logs.d.ts +11 -24
  33. package/dist/db/logs.js +37 -38
  34. package/dist/db/metrics.js +1 -1
  35. package/dist/db/migrations/033_add_pipeline_snapshot.sql +1 -0
  36. package/dist/db/migrations/034_drop_redundant_log_columns.sql +13 -0
  37. package/dist/db/settings.d.ts +2 -0
  38. package/dist/db/settings.js +9 -0
  39. package/dist/index.d.ts +10 -2
  40. package/dist/index.js +196 -108
  41. package/dist/metrics/metrics-extractor.d.ts +1 -24
  42. package/dist/metrics/metrics-extractor.js +1 -1
  43. package/dist/metrics/sse-metrics-transform.d.ts +1 -1
  44. package/dist/middleware/admin-auth.js +4 -0
  45. package/dist/middleware/auth.js +1 -2
  46. package/dist/monitor/request-tracker.d.ts +3 -4
  47. package/dist/monitor/request-tracker.js +6 -16
  48. package/dist/monitor/runtime-collector.js +1 -1
  49. package/dist/monitor/types.d.ts +8 -0
  50. package/dist/proxy/adaptive-controller.d.ts +4 -1
  51. package/dist/proxy/adaptive-controller.js +5 -0
  52. package/dist/proxy/enhancement/enhancement-handler.d.ts +19 -3
  53. package/dist/proxy/enhancement/enhancement-handler.js +80 -28
  54. package/dist/proxy/enhancement/index.d.ts +1 -0
  55. package/dist/proxy/handler/anthropic.d.ts +7 -0
  56. package/dist/proxy/{anthropic.js → handler/anthropic.js} +8 -7
  57. package/dist/proxy/handler/openai.d.ts +7 -0
  58. package/dist/proxy/{openai.js → handler/openai.js} +10 -9
  59. package/dist/proxy/handler/proxy-handler-utils.d.ts +9 -0
  60. package/dist/proxy/handler/proxy-handler-utils.js +63 -0
  61. package/dist/proxy/handler/proxy-handler.d.ts +13 -0
  62. package/dist/proxy/{proxy-handler.js → handler/proxy-handler.js} +104 -120
  63. package/dist/proxy/log-detail-policy.d.ts +12 -0
  64. package/dist/proxy/log-detail-policy.js +21 -0
  65. package/dist/proxy/log-helpers.d.ts +8 -0
  66. package/dist/proxy/log-helpers.js +16 -4
  67. package/dist/proxy/loop-prevention/tool-loop-guard.d.ts +1 -1
  68. package/dist/proxy/loop-prevention/tool-loop-guard.js +9 -12
  69. package/dist/proxy/{orchestrator.d.ts → orchestration/orchestrator.d.ts} +6 -4
  70. package/dist/proxy/{orchestrator.js → orchestration/orchestrator.js} +2 -1
  71. package/dist/proxy/{resilience.d.ts → orchestration/resilience.d.ts} +2 -14
  72. package/dist/proxy/{resilience.js → orchestration/resilience.js} +2 -2
  73. package/dist/proxy/{retry-rules.d.ts → orchestration/retry-rules.d.ts} +1 -1
  74. package/dist/proxy/{retry-rules.js → orchestration/retry-rules.js} +1 -1
  75. package/dist/proxy/{scope.d.ts → orchestration/scope.d.ts} +3 -3
  76. package/dist/proxy/{semaphore.d.ts → orchestration/semaphore.d.ts} +7 -15
  77. package/dist/proxy/{semaphore.js → orchestration/semaphore.js} +12 -26
  78. package/dist/proxy/patch/index.d.ts +8 -2
  79. package/dist/proxy/patch/index.js +5 -2
  80. package/dist/proxy/pipeline-snapshot.d.ts +37 -0
  81. package/dist/proxy/pipeline-snapshot.js +15 -0
  82. package/dist/proxy/proxy-core.d.ts +1 -1
  83. package/dist/proxy/proxy-core.js +1 -1
  84. package/dist/proxy/proxy-logging.d.ts +10 -2
  85. package/dist/proxy/proxy-logging.js +23 -9
  86. package/dist/proxy/response-transform.d.ts +7 -0
  87. package/dist/proxy/response-transform.js +15 -0
  88. package/dist/proxy/{enhancement-config.js → routing/enhancement-config.js} +1 -1
  89. package/dist/proxy/{mapping-resolver.d.ts → routing/mapping-resolver.d.ts} +1 -1
  90. package/dist/proxy/{mapping-resolver.js → routing/mapping-resolver.js} +1 -1
  91. package/dist/proxy/{model-state.js → routing/model-state.js} +1 -1
  92. package/dist/proxy/{overflow.d.ts → routing/overflow.d.ts} +1 -1
  93. package/dist/proxy/{overflow.js → routing/overflow.js} +3 -3
  94. package/dist/proxy/{usage-window-tracker.js → routing/usage-window-tracker.js} +3 -3
  95. package/dist/proxy/{transport.d.ts → transport/http.d.ts} +2 -2
  96. package/dist/proxy/{transport.js → transport/http.js} +3 -3
  97. package/dist/proxy/{stream-proxy.d.ts → transport/stream.d.ts} +4 -4
  98. package/dist/proxy/{stream-proxy.js → transport/stream.js} +25 -7
  99. package/dist/proxy/{transport-fn.d.ts → transport/transport-fn.d.ts} +5 -5
  100. package/dist/proxy/{transport-fn.js → transport/transport-fn.js} +11 -9
  101. package/dist/proxy/types.d.ts +3 -64
  102. package/dist/proxy/types.js +5 -34
  103. package/dist/storage/log-file-compressor.d.ts +15 -0
  104. package/dist/storage/log-file-compressor.js +83 -0
  105. package/dist/storage/log-file-writer.d.ts +21 -0
  106. package/dist/storage/log-file-writer.js +103 -0
  107. package/dist/storage/types.d.ts +16 -0
  108. package/dist/storage/types.js +5 -0
  109. package/dist/upgrade/deployment.d.ts +13 -0
  110. package/dist/upgrade/deployment.js +40 -0
  111. package/frontend-dist/assets/{CardContent-jQcfCC7J.js → CardContent-CxOF1feY.js} +1 -1
  112. package/frontend-dist/assets/{CardTitle-BrCTvULL.js → CardTitle-BSEFcEOM.js} +1 -1
  113. package/frontend-dist/assets/{CascadingModelSelect-BFh67j5d.js → CascadingModelSelect-DTwksDPZ.js} +1 -1
  114. package/frontend-dist/assets/{Checkbox-Bbt7JpdE.js → Checkbox-RfsERG07.js} +1 -1
  115. package/frontend-dist/assets/{CollapsibleTrigger-DMnEA0qC.js → CollapsibleTrigger-Dsjo7QlC.js} +1 -1
  116. package/frontend-dist/assets/{Collection-CVk3TPHc.js → Collection-rQ4eIYfa.js} +1 -1
  117. package/frontend-dist/assets/{Dashboard-Coftbg4B.js → Dashboard-YejfAPiB.js} +1 -1
  118. package/frontend-dist/assets/{DialogTitle-BbOAZzPQ.js → DialogTitle-DeFTnmgC.js} +1 -1
  119. package/frontend-dist/assets/{Input-DdHY9q0w.js → Input-CENz_g9t.js} +1 -1
  120. package/frontend-dist/assets/{Label-DRQv_Dr_.js → Label-BAciBrrd.js} +1 -1
  121. package/frontend-dist/assets/{Login-SV3ctFnJ.js → Login-DQkYFq7R.js} +1 -1
  122. package/frontend-dist/assets/{Logs-BG45kX6E.js → Logs-Dol8AX7z.js} +1 -1
  123. package/frontend-dist/assets/{ModelMappings-DEaBnRU3.js → ModelMappings-VEYW1TrW.js} +1 -1
  124. package/frontend-dist/assets/{Monitor-ZHOt11n-.js → Monitor-C0r9WefB.js} +1 -1
  125. package/frontend-dist/assets/{PopoverTrigger-z-Z3EjBk.js → PopoverTrigger-Cyqik5SE.js} +1 -1
  126. package/frontend-dist/assets/{PopperContent-DPC-6a3n.js → PopperContent-B7IuAHeq.js} +1 -1
  127. package/frontend-dist/assets/{Providers-DpY6pAcg.js → Providers-D8Z97edN.js} +1 -1
  128. package/frontend-dist/assets/{ProxyEnhancement-D6KBDXMp.js → ProxyEnhancement-Kn8r2SN6.js} +1 -1
  129. package/frontend-dist/assets/{RetryRules-DWI7_WLZ.js → RetryRules-F0295m4_.js} +1 -1
  130. package/frontend-dist/assets/{RouterKeys-CZ1657eX.js → RouterKeys-CFbPtUE_.js} +1 -1
  131. package/frontend-dist/assets/{RovingFocusItem-BREE2YEV.js → RovingFocusItem-D291Vjh8.js} +1 -1
  132. package/frontend-dist/assets/{Schedules-BVPsBRPi.js → Schedules-DWhF3uod.js} +1 -1
  133. package/frontend-dist/assets/{SelectValue-H8hwQwbk.js → SelectValue-BWlgUZa3.js} +1 -1
  134. package/frontend-dist/assets/Settings-BnIzEF_k.js +6 -0
  135. package/frontend-dist/assets/{Setup-yOYNKkOG.js → Setup-BglKyQKq.js} +1 -1
  136. package/frontend-dist/assets/{Switch-CojD3rTH.js → Switch-DyCR-CPu.js} +1 -1
  137. package/frontend-dist/assets/{TableHeader-awoHTsWN.js → TableHeader-DVUlBL35.js} +1 -1
  138. package/frontend-dist/assets/{TabsTrigger-DTKSFj85.js → TabsTrigger-BU1DY-C8.js} +1 -1
  139. package/frontend-dist/assets/{Teleport-DehYAXud.js → Teleport-BQgusr9g.js} +1 -1
  140. package/frontend-dist/assets/{TooltipTrigger-C2dl_dml.js → TooltipTrigger-Bv_QoBns.js} +1 -1
  141. package/frontend-dist/assets/{UnifiedRequestDialog-C8A-uSTR.js → UnifiedRequestDialog-f_evI835.js} +2 -2
  142. package/frontend-dist/assets/{VisuallyHidden-C8oaGi2S.js → VisuallyHidden-Con10z4F.js} +1 -1
  143. package/frontend-dist/assets/{VisuallyHiddenInput-BMc813t2.js → VisuallyHiddenInput-yrDtxucb.js} +1 -1
  144. package/frontend-dist/assets/{alert-dialog-C8TZQmU6.js → alert-dialog-2Db6Z7JQ.js} +1 -1
  145. package/frontend-dist/assets/arrow-down-WyouvE7T.js +1 -0
  146. package/frontend-dist/assets/{badge-BVh2WpA5.js → badge-DEhZfeI0.js} +1 -1
  147. package/frontend-dist/assets/button-Cnkbp_6J.js +12 -0
  148. package/frontend-dist/assets/check-BuqB5Nyb.js +1 -0
  149. package/frontend-dist/assets/{copy-DTOecxa9.js → copy-CwqZSuIG.js} +1 -1
  150. package/frontend-dist/assets/{dialog-kA7AUNoc.js → dialog-CVMKSdPr.js} +1 -1
  151. package/frontend-dist/assets/{file-text-DzZCFO7y.js → file-text-D0K8Hovo.js} +1 -1
  152. package/frontend-dist/assets/index-Ct718O93.js +1 -0
  153. package/frontend-dist/assets/{lib-ClDokUbt.js → lib-H3YI7EK4.js} +1 -1
  154. package/frontend-dist/assets/loader-circle-Be82FnVY.js +1 -0
  155. package/frontend-dist/assets/{useClipboard-DU1ne-Jw.js → useClipboard-Cd7k-5Yq.js} +1 -1
  156. package/frontend-dist/assets/{useFocusGuards-Btmdbg_F.js → useFocusGuards-luoLXnwV.js} +1 -1
  157. package/frontend-dist/assets/useFormControl-Da4ViGZF.js +1 -0
  158. package/frontend-dist/assets/{useLogRetention--EGNWXig.js → useLogRetention-DB4Iu6o_.js} +1 -1
  159. package/frontend-dist/assets/useNonce-DvAdQ48J.js +1 -0
  160. package/frontend-dist/assets/x-DB22csQl.js +1 -0
  161. package/frontend-dist/index.html +19 -19
  162. package/package.json +1 -1
  163. package/dist/proxy/anthropic.d.ts +0 -19
  164. package/dist/proxy/openai.d.ts +0 -19
  165. package/dist/proxy/proxy-handler.d.ts +0 -19
  166. package/dist/proxy/strategy/types.d.ts +0 -21
  167. package/dist/proxy/strategy/types.js +0 -1
  168. package/frontend-dist/assets/Settings-DHYaYRgU.js +0 -6
  169. package/frontend-dist/assets/arrow-down-D-cQXxau.js +0 -1
  170. package/frontend-dist/assets/button-N59D1BGa.js +0 -12
  171. package/frontend-dist/assets/check-dDgrw3T3.js +0 -1
  172. package/frontend-dist/assets/index-DVTeNVaa.js +0 -1
  173. package/frontend-dist/assets/loader-circle-DVHRL-38.js +0 -1
  174. package/frontend-dist/assets/useFormControl-C5Kjziuj.js +0 -1
  175. package/frontend-dist/assets/useNonce-Cp31yRzV.js +0 -1
  176. package/frontend-dist/assets/x-DMktsI_w.js +0 -1
  177. /package/dist/{config.d.ts → config/index.d.ts} +0 -0
  178. /package/dist/{config.js → config/index.js} +0 -0
  179. /package/dist/proxy/{scope.js → orchestration/scope.js} +0 -0
  180. /package/dist/proxy/{enhancement-config.d.ts → routing/enhancement-config.d.ts} +0 -0
  181. /package/dist/proxy/{model-state.d.ts → routing/model-state.d.ts} +0 -0
  182. /package/dist/proxy/{usage-window-tracker.d.ts → routing/usage-window-tracker.d.ts} +0 -0
@@ -1,9 +1,9 @@
1
1
  import { randomUUID } from "crypto";
2
- import { loadEnhancementConfig } from "../enhancement-config.js";
2
+ import { loadEnhancementConfig } from "../routing/enhancement-config.js";
3
3
  import { getActiveProviderModels, resolveByProviderModel } from "../../db/index.js";
4
- import { resolveMapping } from "../mapping-resolver.js";
4
+ import { resolveMapping } from "../routing/mapping-resolver.js";
5
5
  import { parseDirective, parseToolResult, TOOL_USE_ID_PREFIX, TOOL_USE_ID_PROVIDER_PREFIX } from "./directive-parser.js";
6
- import { modelState } from "../model-state.js";
6
+ import { modelState } from "../routing/model-state.js";
7
7
  import { cleanRouterResponses } from "./response-cleaner.js";
8
8
  const MODEL_INFO_TAG_TYPE = "model-info";
9
9
  const SKIP_LABEL = "不选择";
@@ -56,66 +56,79 @@ function buildDisplayModels(db, allowedModelsRaw) {
56
56
  }
57
57
  return displayModels;
58
58
  }
59
+ const EMPTY_META = { router_tags_stripped: 0, directive: null };
59
60
  /**
60
61
  * 在代理转发前应用代理增强逻辑(指令解析 + 会话记忆 + 模型替换 + 命令拦截)。
61
62
  * 仅当 proxy_enhancement.claude_code_enabled 开启时生效。
63
+ *
64
+ * 纯函数:不修改输入 body,返回变换后的新 body + 元数据。
62
65
  */
63
- export function applyEnhancement(db, request, clientModel, sessionId, enhancementConfig) {
64
- const nullResult = { effectiveModel: clientModel, originalModel: null, interceptResponse: null };
66
+ export function applyEnhancement(db, body, clientModel, sessionId, routerKey, enhancementConfig) {
67
+ const earlyReturn = {
68
+ body, effectiveModel: clientModel, originalModel: null, interceptResponse: null, meta: EMPTY_META,
69
+ };
65
70
  const enhancement = enhancementConfig ?? loadEnhancementConfig(db);
66
71
  if (!enhancement.claude_code_enabled) {
67
- return nullResult;
72
+ return earlyReturn;
68
73
  }
69
74
  // 检测 AskUserQuestion 的 tool_result 回调(用户在 UI 上选择了模型或 provider)
70
- const toolResult = parseToolResult(request.body);
75
+ const toolResult = parseToolResult(body);
71
76
  if (toolResult.isRouterToolResult) {
72
- const routerKeyId = request.routerKey?.id ?? null;
77
+ const routerKeyId = routerKey?.id ?? null;
73
78
  const nonSkipAnswers = toolResult.allAnswers.filter(a => a !== SKIP_LABEL);
74
79
  // 所有回答都是"不选择" → 取消
75
80
  if (nonSkipAnswers.length === 0) {
76
81
  return {
82
+ body,
77
83
  effectiveModel: clientModel,
78
84
  originalModel: null,
79
85
  interceptResponse: {
80
86
  ...buildTextResponse("model-select-cancelled", "已取消选择"),
81
87
  meta: { action: "取消模型选择" },
82
88
  },
89
+ meta: EMPTY_META,
83
90
  };
84
91
  }
85
92
  // 选择了多个 → 提示错误
86
93
  if (nonSkipAnswers.length > 1) {
87
94
  return {
95
+ body,
88
96
  effectiveModel: clientModel,
89
97
  originalModel: null,
90
98
  interceptResponse: {
91
99
  ...buildTextResponse("model-select-error", "选择错误:只能选择一个模型或提供商,请重新输入 /select-model 选择"),
92
100
  meta: { action: "选择错误" },
93
101
  },
102
+ meta: EMPTY_META,
94
103
  };
95
104
  }
96
105
  const answer = nonSkipAnswers[0];
97
106
  // 两步式:用户选择了 provider → 返回该 provider 的模型列表
98
107
  if (toolResult.isProviderSelection) {
99
- const allModels = buildDisplayModels(db, request.routerKey?.allowed_models ?? null);
108
+ const allModels = buildDisplayModels(db, routerKey?.allowed_models ?? null);
100
109
  const providerModels = getModelsForProvider(allModels, answer);
101
110
  if (providerModels.length === 0) {
102
111
  return {
112
+ body,
103
113
  effectiveModel: clientModel,
104
114
  originalModel: null,
105
115
  interceptResponse: {
106
116
  ...buildTextResponse("error", `未找到 provider: ${answer}`),
107
117
  meta: { action: "模型选择失败", detail: answer },
108
118
  },
119
+ meta: EMPTY_META,
109
120
  };
110
121
  }
111
122
  const questions = buildModelQuestions(providerModels);
112
123
  return {
124
+ body,
113
125
  effectiveModel: clientModel,
114
126
  originalModel: null,
115
127
  interceptResponse: {
116
128
  ...buildAskUserQuestionPayload(questions, false, providerModels),
117
129
  meta: { action: `模型列表(provider=${answer})` },
118
130
  },
131
+ meta: EMPTY_META,
119
132
  };
120
133
  }
121
134
  // 模型选择(直接或两步式第二步)
@@ -123,30 +136,37 @@ export function applyEnhancement(db, request, clientModel, sessionId, enhancemen
123
136
  if (resolvedClientModel) {
124
137
  modelState.set(routerKeyId, answer, sessionId, clientModel, "command");
125
138
  return {
139
+ body,
126
140
  effectiveModel: answer,
127
141
  originalModel: null,
128
142
  interceptResponse: {
129
143
  ...buildTextResponse("model-selected", `已选择模型: ${answer}`),
130
144
  meta: { action: "模型选择", detail: answer },
131
145
  },
146
+ meta: EMPTY_META,
132
147
  };
133
148
  }
134
149
  return {
150
+ body,
135
151
  effectiveModel: clientModel,
136
152
  originalModel: null,
137
153
  interceptResponse: {
138
154
  ...buildTextResponse("error", `未找到模型: ${answer}`),
139
155
  meta: { action: "模型选择失败", detail: answer },
140
156
  },
157
+ meta: EMPTY_META,
141
158
  };
142
159
  }
143
- // 清理历史消息中的 <router-response> 标签
144
- const cleaned = cleanRouterResponses(request.body);
145
- request.body.messages = cleaned.messages;
146
- const directive = parseDirective(request.body);
160
+ // 清理历史消息中的 <router-response> 标签(纯函数,返回新对象)
161
+ const originalMessages = body.messages?.length ?? 0;
162
+ const cleaned = cleanRouterResponses(body);
163
+ const cleanedMessages = cleaned.messages?.length ?? 0;
164
+ const tagsStripped = originalMessages - cleanedMessages;
165
+ const currentBody = { ...body, messages: cleaned.messages };
166
+ const directive = parseDirective(currentBody);
147
167
  // 命令拦截:select-model → 返回可用模型列表
148
168
  if (directive.isCommandMessage && directive.command?.startsWith("select-model")) {
149
- const routerKeyId = request.routerKey?.id ?? null;
169
+ const routerKeyId = routerKey?.id ?? null;
150
170
  const parts = directive.command.trim().split(/\s+/);
151
171
  const arg = parts.length > 1 ? parts.slice(1).join(" ") : null;
152
172
  // 带参数:设置模型并返回确认
@@ -154,35 +174,41 @@ export function applyEnhancement(db, request, clientModel, sessionId, enhancemen
154
174
  const resolvedClientModel = resolveProviderModel(db, arg);
155
175
  if (!resolvedClientModel) {
156
176
  return {
177
+ body: currentBody,
157
178
  effectiveModel: clientModel,
158
179
  originalModel: null,
159
180
  interceptResponse: {
160
181
  ...buildTextResponse("error", `未找到模型: ${arg}`),
161
182
  meta: { action: "模型选择失败", detail: arg },
162
183
  },
184
+ meta: { router_tags_stripped: tagsStripped, directive: { type: "router_command", value: `select-model ${arg}` } },
163
185
  };
164
186
  }
165
187
  modelState.set(routerKeyId, arg, sessionId, clientModel, "command");
166
188
  return {
189
+ body: currentBody,
167
190
  effectiveModel: arg,
168
191
  originalModel: null,
169
192
  interceptResponse: {
170
- ...buildSelectModelResponse(db, request.routerKey?.allowed_models ?? null, arg),
193
+ ...buildSelectModelResponse(db, routerKey?.allowed_models ?? null, arg),
171
194
  meta: { action: "模型选择", detail: arg },
172
195
  },
196
+ meta: { router_tags_stripped: tagsStripped, directive: { type: "router_command", value: `select-model ${arg}` } },
173
197
  };
174
198
  }
175
199
  // 无参数:返回模型列表
176
- if (hasAskUserQuestion(request.body)) {
177
- const displayModels = buildDisplayModels(db, request.routerKey?.allowed_models ?? null);
200
+ if (hasAskUserQuestion(currentBody)) {
201
+ const displayModels = buildDisplayModels(db, routerKey?.allowed_models ?? null);
178
202
  if (displayModels.length === 0) {
179
203
  return {
204
+ body: currentBody,
180
205
  effectiveModel: clientModel,
181
206
  originalModel: null,
182
207
  interceptResponse: {
183
208
  ...buildTextResponse("model-list", "(无可用模型)"),
184
209
  meta: { action: "模型列表" },
185
210
  },
211
+ meta: { router_tags_stripped: tagsStripped, directive: { type: "router_command", value: "select-model" } },
186
212
  };
187
213
  }
188
214
  // >= TWO_STEP_THRESHOLD 且多个 provider → 两步式:先选 provider
@@ -191,12 +217,14 @@ export function applyEnhancement(db, request, clientModel, sessionId, enhancemen
191
217
  if (providers.length >= 2) {
192
218
  const providerQs = buildProviderQuestions(providers);
193
219
  return {
220
+ body: currentBody,
194
221
  effectiveModel: clientModel,
195
222
  originalModel: null,
196
223
  interceptResponse: {
197
224
  ...buildAskUserQuestionPayload(providerQs, true, displayModels),
198
225
  meta: { action: "Provider列表(AskUserQuestion)" },
199
226
  },
227
+ meta: { router_tags_stripped: tagsStripped, directive: { type: "router_command", value: "select-model" } },
200
228
  };
201
229
  }
202
230
  // 单 provider 且模型过多 → AskUserQuestion 显示前 6 个 + 文本列出剩余
@@ -206,65 +234,89 @@ export function applyEnhancement(db, request, clientModel, sessionId, enhancemen
206
234
  if (displayModels.length > capped.length) {
207
235
  const extra = displayModels.slice(capped.length).map((m, i) => `${capped.length + i + 1}. ${m}`).join("\n");
208
236
  const textBlock = { type: "text", text: `更多模型:\n${extra}\n\n可输入 /select-model provider/model 选择` };
209
- const body = payload.body;
210
- body.content = [textBlock, ...body.content];
237
+ const payloadBody = payload.body;
238
+ payloadBody.content = [textBlock, ...payloadBody.content];
211
239
  }
212
240
  return {
241
+ body: currentBody,
213
242
  effectiveModel: clientModel,
214
243
  originalModel: null,
215
244
  interceptResponse: {
216
245
  ...payload,
217
246
  meta: { action: "模型列表(AskUserQuestion)" },
218
247
  },
248
+ meta: { router_tags_stripped: tagsStripped, directive: { type: "router_command", value: "select-model" } },
219
249
  };
220
250
  }
221
251
  // < TWO_STEP_THRESHOLD → AskUserQuestion 2 组
222
252
  const questions = buildModelQuestions(displayModels);
223
253
  return {
254
+ body: currentBody,
224
255
  effectiveModel: clientModel,
225
256
  originalModel: null,
226
257
  interceptResponse: {
227
258
  ...buildAskUserQuestionPayload(questions, false),
228
259
  meta: { action: "模型列表(AskUserQuestion)" },
229
260
  },
261
+ meta: { router_tags_stripped: tagsStripped, directive: { type: "router_command", value: "select-model" } },
230
262
  };
231
263
  }
232
264
  return {
265
+ body: currentBody,
233
266
  effectiveModel: clientModel,
234
267
  originalModel: null,
235
268
  interceptResponse: {
236
- ...buildSelectModelResponse(db, request.routerKey?.allowed_models ?? null),
269
+ ...buildSelectModelResponse(db, routerKey?.allowed_models ?? null),
237
270
  meta: { action: "模型列表" },
238
271
  },
272
+ meta: { router_tags_stripped: tagsStripped, directive: { type: "router_command", value: "select-model" } },
239
273
  };
240
274
  }
241
275
  if (directive.modelName) {
242
276
  // 内联模型指令 → resolveMapping 验证(client_model 格式)
243
277
  const resolvedDirective = resolveMapping(db, directive.modelName, { now: new Date() });
244
278
  if (resolvedDirective) {
245
- modelState.set(request.routerKey?.id ?? null, directive.modelName, sessionId, clientModel, "directive");
246
- request.body.messages = directive.cleanedBody.messages;
247
- return { effectiveModel: directive.modelName, originalModel: clientModel, interceptResponse: null };
279
+ modelState.set(routerKey?.id ?? null, directive.modelName, sessionId, clientModel, "directive");
280
+ const directiveBody = { ...currentBody, messages: directive.cleanedBody.messages };
281
+ return {
282
+ body: directiveBody,
283
+ effectiveModel: directive.modelName,
284
+ originalModel: clientModel,
285
+ interceptResponse: null,
286
+ meta: { router_tags_stripped: tagsStripped, directive: { type: directive.isCommandMessage ? "router_command" : "router_model", value: directive.modelName } },
287
+ };
248
288
  }
249
289
  // 映射失败时保留原始请求(降级策略)
250
- return nullResult;
290
+ return { body: currentBody, effectiveModel: clientModel, originalModel: null, interceptResponse: null, meta: { router_tags_stripped: tagsStripped, directive: null } };
251
291
  }
252
292
  // 无指令 → 查询会话记忆
253
- const remembered = modelState.get(request.routerKey?.id ?? null, sessionId);
293
+ const remembered = modelState.get(routerKey?.id ?? null, sessionId);
254
294
  if (remembered) {
255
295
  // 优先尝试 provider_name/backend_model 格式(select-model 命令存储)
256
296
  // 直接保留该格式,resolveMapping 会解析出 provider + model
257
297
  const providerResolved = resolveProviderModel(db, remembered);
258
298
  if (providerResolved) {
259
- return { effectiveModel: remembered, originalModel: clientModel, interceptResponse: null };
299
+ return {
300
+ body: currentBody,
301
+ effectiveModel: remembered,
302
+ originalModel: clientModel,
303
+ interceptResponse: null,
304
+ meta: { router_tags_stripped: tagsStripped, directive: { type: "select_model", value: remembered } },
305
+ };
260
306
  }
261
307
  // 回退到 client_model 格式(内联指令存储)
262
308
  const resolvedRemembered = resolveMapping(db, remembered, { now: new Date() });
263
309
  if (resolvedRemembered) {
264
- return { effectiveModel: remembered, originalModel: clientModel, interceptResponse: null };
310
+ return {
311
+ body: currentBody,
312
+ effectiveModel: remembered,
313
+ originalModel: clientModel,
314
+ interceptResponse: null,
315
+ meta: { router_tags_stripped: tagsStripped, directive: { type: "router_model", value: remembered } },
316
+ };
265
317
  }
266
318
  }
267
- return nullResult;
319
+ return { body: currentBody, effectiveModel: clientModel, originalModel: null, interceptResponse: null, meta: { router_tags_stripped: tagsStripped, directive: null } };
268
320
  }
269
321
  /** 构造 Anthropic 格式的 router 文本响应 */
270
322
  function buildTextResponse(type, inner) {
@@ -1,3 +1,4 @@
1
1
  export { applyEnhancement, buildModelInfoTag } from "./enhancement-handler.js";
2
+ export type { EnhancementResult, EnhancementMeta, RouterKeyInfo } from "./enhancement-handler.js";
2
3
  export { parseDirective, parseToolResult, TOOL_USE_ID_PREFIX, TOOL_USE_ID_PROVIDER_PREFIX } from "./directive-parser.js";
3
4
  export { cleanRouterResponses } from "./response-cleaner.js";
@@ -0,0 +1,7 @@
1
+ import Database from "better-sqlite3";
2
+ import type { FastifyPluginCallback } from "fastify";
3
+ export interface AnthropicProxyOptions {
4
+ db: Database.Database;
5
+ container: import("../../core/container.js").ServiceContainer;
6
+ }
7
+ export declare const anthropicProxy: FastifyPluginCallback<AnthropicProxyOptions>;
@@ -1,10 +1,11 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import fp from "fastify-plugin";
3
- import { insertRequestLog } from "../db/index.js";
4
- import { createErrorFormatter } from "./proxy-core.js";
3
+ import { insertRequestLog } from "../../db/index.js";
4
+ import { createErrorFormatter } from "../proxy-core.js";
5
5
  import { handleProxyRequest } from "./proxy-handler.js";
6
- import { createOrchestrator } from "./orchestrator.js";
7
- import { HTTP_BAD_GATEWAY } from "../constants.js";
6
+ import { createOrchestrator } from "../orchestration/orchestrator.js";
7
+ import { HTTP_BAD_GATEWAY } from "../../core/constants.js";
8
+ import { SERVICE_KEYS } from "../../core/container.js";
8
9
  const MESSAGES_PATH = "/v1/messages";
9
10
  const ANTHROPIC_ERROR_TYPE = {
10
11
  modelNotFound: "not_found_error",
@@ -18,8 +19,8 @@ const ANTHROPIC_ERROR_TYPE = {
18
19
  };
19
20
  const anthropicErrors = createErrorFormatter((kind, message) => ({ type: "error", error: { type: ANTHROPIC_ERROR_TYPE[kind], message } }));
20
21
  const anthropicProxyRaw = (app, opts, done) => {
21
- const { db, streamTimeoutMs, retryBaseDelayMs, matcher, semaphoreManager, tracker, usageWindowTracker, sessionTracker, adaptiveController } = opts;
22
- const orchestrator = createOrchestrator(semaphoreManager, tracker, adaptiveController);
22
+ const { db, container } = opts;
23
+ const orchestrator = createOrchestrator(container.resolve(SERVICE_KEYS.semaphoreManager), container.resolve(SERVICE_KEYS.tracker), container.resolve(SERVICE_KEYS.adaptiveController));
23
24
  app.post(MESSAGES_PATH, async (request, reply) => {
24
25
  if (!orchestrator) {
25
26
  const body = request.body;
@@ -34,7 +35,7 @@ const anthropicProxyRaw = (app, opts, done) => {
34
35
  const e = anthropicErrors.providerUnavailable();
35
36
  return reply.code(e.statusCode).send(e.body);
36
37
  }
37
- const deps = { db, streamTimeoutMs, retryBaseDelayMs, matcher, tracker, orchestrator, usageWindowTracker, sessionTracker };
38
+ const deps = { db, orchestrator, container };
38
39
  return handleProxyRequest(request, reply, "anthropic", MESSAGES_PATH, anthropicErrors, deps);
39
40
  });
40
41
  done();
@@ -0,0 +1,7 @@
1
+ import type { FastifyPluginCallback } from "fastify";
2
+ import Database from "better-sqlite3";
3
+ export interface OpenaiProxyOptions {
4
+ db: Database.Database;
5
+ container: import("../../core/container.js").ServiceContainer;
6
+ }
7
+ export declare const openaiProxy: FastifyPluginCallback<OpenaiProxyOptions>;
@@ -1,12 +1,13 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import fp from "fastify-plugin";
3
- import { getActiveProviders, insertRequestLog } from "../db/index.js";
4
- import { getSetting } from "../db/settings.js";
5
- import { decrypt } from "../utils/crypto.js";
6
- import { proxyGetRequest, createErrorFormatter } from "./proxy-core.js";
3
+ import { getActiveProviders, insertRequestLog } from "../../db/index.js";
4
+ import { getSetting } from "../../db/settings.js";
5
+ import { decrypt } from "../../utils/crypto.js";
6
+ import { proxyGetRequest, createErrorFormatter } from "../proxy-core.js";
7
7
  import { handleProxyRequest } from "./proxy-handler.js";
8
- import { createOrchestrator } from "./orchestrator.js";
9
- import { HTTP_NOT_FOUND, HTTP_BAD_GATEWAY } from "../constants.js";
8
+ import { createOrchestrator } from "../orchestration/orchestrator.js";
9
+ import { HTTP_NOT_FOUND, HTTP_BAD_GATEWAY } from "../../core/constants.js";
10
+ import { SERVICE_KEYS } from "../../core/container.js";
10
11
  const CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
11
12
  const MODELS_PATH = "/v1/models";
12
13
  const OPENAI_ERROR_META = {
@@ -24,8 +25,8 @@ function sendError(reply, e) {
24
25
  return reply.code(e.statusCode).send(e.body);
25
26
  }
26
27
  const openaiProxyRaw = (app, opts, done) => {
27
- const { db, streamTimeoutMs, retryBaseDelayMs, matcher, semaphoreManager, tracker, usageWindowTracker, sessionTracker, adaptiveController } = opts;
28
- const orchestrator = createOrchestrator(semaphoreManager, tracker, adaptiveController);
28
+ const { db, container } = opts;
29
+ const orchestrator = createOrchestrator(container.resolve(SERVICE_KEYS.semaphoreManager), container.resolve(SERVICE_KEYS.tracker), container.resolve(SERVICE_KEYS.adaptiveController));
29
30
  app.post(CHAT_COMPLETIONS_PATH, async (request, reply) => {
30
31
  if (!orchestrator) {
31
32
  const body = request.body;
@@ -39,7 +40,7 @@ const openaiProxyRaw = (app, opts, done) => {
39
40
  });
40
41
  return sendError(reply, openaiErrors.providerUnavailable());
41
42
  }
42
- const deps = { db, streamTimeoutMs, retryBaseDelayMs, matcher, tracker, orchestrator, usageWindowTracker, sessionTracker };
43
+ const deps = { db, orchestrator, container };
43
44
  return handleProxyRequest(request, reply, "openai", CHAT_COMPLETIONS_PATH, openaiErrors, deps, {
44
45
  beforeSendProxy: (body, isStream) => {
45
46
  if (isStream && !body.stream_options) {
@@ -0,0 +1,9 @@
1
+ import type { ContentBlock } from "../../monitor/types.js";
2
+ import type { ToolCallRecord } from "../loop-prevention/types.js";
3
+ import type { TransportResult } from "../types.js";
4
+ /** 从 TransportResult 中提取最终 HTTP status code */
5
+ export declare function getTransportStatusCode(result: TransportResult): number | null;
6
+ /** 将 tracker blocks 序列化为前端 tryDirectParse 可解析的 JSON */
7
+ export declare function serializeBlocksForStorage(blocks: ContentBlock[] | undefined, apiType: "openai" | "anthropic"): string;
8
+ /** 从请求体中提取最后一次工具调用记录 */
9
+ export declare function extractLastToolUse(body: Record<string, unknown>): ToolCallRecord | null;
@@ -0,0 +1,63 @@
1
+ import { createHash } from "crypto";
2
+ const HASH_DIGEST_LENGTH = 16;
3
+ /** 从 TransportResult 中提取最终 HTTP status code */
4
+ export function getTransportStatusCode(result) {
5
+ if (result.kind === "success" || result.kind === "error" || result.kind === "stream_error")
6
+ return result.statusCode;
7
+ if (result.kind === "stream_success" || result.kind === "stream_abort")
8
+ return result.statusCode;
9
+ return null;
10
+ }
11
+ /** 将 tracker blocks 序列化为前端 tryDirectParse 可解析的 JSON */
12
+ export function serializeBlocksForStorage(blocks, apiType) {
13
+ if (!blocks || blocks.length === 0)
14
+ return "";
15
+ if (apiType === "anthropic") {
16
+ const content = blocks.map(b => {
17
+ if (b.type === "thinking")
18
+ return { type: "thinking", thinking: b.content };
19
+ if (b.type === "tool_use") {
20
+ let input = {};
21
+ // eslint-disable-next-line taste/no-silent-catch
22
+ try {
23
+ input = JSON.parse(b.content || "{}");
24
+ }
25
+ catch { /* tool_use content 非合法 JSON 时保留空对象 */ }
26
+ return { type: "tool_use", name: b.name ?? "", input };
27
+ }
28
+ return { type: "text", text: b.content };
29
+ });
30
+ return JSON.stringify({ content });
31
+ }
32
+ const text = blocks.filter(b => b.type === "text").map(b => b.content).join("");
33
+ return JSON.stringify({ choices: [{ message: { content: text } }] });
34
+ }
35
+ /** 从请求体中提取最后一次工具调用记录 */
36
+ export function extractLastToolUse(body) {
37
+ const messages = body.messages;
38
+ if (!messages)
39
+ return null;
40
+ for (let i = messages.length - 1; i >= 0; i--) {
41
+ const msg = messages[i];
42
+ if (msg.role !== "assistant")
43
+ continue;
44
+ const content = msg.content;
45
+ if (!Array.isArray(content))
46
+ continue;
47
+ for (let j = content.length - 1; j >= 0; j--) {
48
+ const block = content[j];
49
+ if (block.type === "tool_use") {
50
+ const inputText = JSON.stringify(block.input ?? {});
51
+ const inputHash = createHash("sha256").update(inputText).digest("hex").slice(0, HASH_DIGEST_LENGTH);
52
+ return {
53
+ toolName: block.name ?? "unknown",
54
+ toolUseId: block.id,
55
+ inputHash,
56
+ inputText,
57
+ timestamp: Date.now(),
58
+ };
59
+ }
60
+ }
61
+ }
62
+ return null;
63
+ }
@@ -0,0 +1,13 @@
1
+ import type { FastifyReply, FastifyRequest } from "fastify";
2
+ import Database from "better-sqlite3";
3
+ import type { ProxyOrchestrator } from "../orchestration/orchestrator.js";
4
+ import type { ProxyErrorFormatter } from "../proxy-core.js";
5
+ export interface RouteHandlerDeps {
6
+ db: Database.Database;
7
+ orchestrator: ProxyOrchestrator;
8
+ container: ServiceContainer;
9
+ }
10
+ import type { ServiceContainer } from "../../core/container.js";
11
+ export declare function handleProxyRequest(request: FastifyRequest, reply: FastifyReply, apiType: "openai" | "anthropic", upstreamPath: string, errors: ProxyErrorFormatter, deps: RouteHandlerDeps, options?: {
12
+ beforeSendProxy?: (body: Record<string, unknown>, isStream: boolean) => void;
13
+ }): Promise<FastifyReply>;