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,35 +1,27 @@
1
1
  import { randomUUID } from "crypto";
2
- import { HTTP_UNPROCESSABLE_ENTITY } from "../constants.js";
3
- import { getProviderById, insertRequestLog } from "../db/index.js";
4
- import { decrypt } from "../utils/crypto.js";
5
- import { getSetting } from "../db/settings.js";
6
- import { resolveMapping } from "./mapping-resolver.js";
7
- import { applyEnhancement } from "./enhancement/enhancement-handler.js";
8
- import { SemaphoreQueueFullError, SemaphoreTimeoutError } from "./semaphore.js";
9
- import { logResilienceResult, collectTransportMetrics, handleIntercept, sanitizeHeadersForLog, } from "./proxy-logging.js";
10
- import { buildUpstreamHeaders, buildUpstreamUrl } from "./proxy-core.js";
11
- import { ProviderSwitchNeeded } from "./types.js";
12
- import { updateLogStreamContent, updateLogClientStatus } from "../db/index.js";
13
- import { insertRejectedLog } from "./log-helpers.js";
14
- import { ToolLoopGuard } from "./loop-prevention/tool-loop-guard.js";
15
- import { TOOL_USE_ID_PREFIX, TOOL_USE_ID_PROVIDER_PREFIX } from "./enhancement/directive-parser.js";
16
- import { buildTransportFn } from "./transport-fn.js";
17
- import { applyOverflowRedirect } from "./overflow.js";
18
- import { applyProviderPatches } from "./patch/index.js";
19
- import { loadEnhancementConfig } from "./enhancement-config.js";
2
+ import { HTTP_UNPROCESSABLE_ENTITY } from "../../core/constants.js";
3
+ import { getProviderById, insertRequestLog, updateLogPipelineSnapshot, updateLogStreamContent, updateLogClientStatus } from "../../db/index.js";
4
+ import { decrypt } from "../../utils/crypto.js";
5
+ import { getSetting } from "../../db/settings.js";
6
+ import { resolveMapping } from "../routing/mapping-resolver.js";
7
+ import { applyEnhancement } from "../enhancement/enhancement-handler.js";
8
+ import { SemaphoreQueueFullError, SemaphoreTimeoutError } from "../orchestration/semaphore.js";
9
+ import { logResilienceResult, collectTransportMetrics, handleIntercept, sanitizeHeadersForLog, } from "../proxy-logging.js";
10
+ import { buildUpstreamHeaders, buildUpstreamUrl } from "../proxy-core.js";
11
+ import { ProviderSwitchNeeded } from "../types.js";
12
+ import { insertRejectedLog } from "../log-helpers.js";
13
+ import { ToolLoopGuard } from "../loop-prevention/tool-loop-guard.js";
14
+ import { buildTransportFn } from "../transport/transport-fn.js";
15
+ import { applyOverflowRedirect } from "../routing/overflow.js";
16
+ import { applyProviderPatches } from "../patch/index.js";
17
+ import { PipelineSnapshot } from "../pipeline-snapshot.js";
18
+ import { maybeInjectModelInfoTag } from "../response-transform.js";
19
+ import { loadEnhancementConfig } from "../routing/enhancement-config.js";
20
+ import { getTransportStatusCode, serializeBlocksForStorage, extractLastToolUse } from "./proxy-handler-utils.js";
20
21
  const HTTP_ERROR_THRESHOLD = 400;
21
22
  const MAX_LOG_FIELD_LENGTH = 80;
22
23
  const UPSTREAM_ERROR_STATUS = 502;
23
24
  const TIER2_LOOP_THRESHOLD = 2;
24
- /** 从 TransportResult 中提取最终 HTTP status code */
25
- function getTransportStatusCode(result) {
26
- if (result.kind === "success" || result.kind === "error" || result.kind === "stream_error")
27
- return result.statusCode;
28
- if (result.kind === "stream_success" || result.kind === "stream_abort")
29
- return result.statusCode;
30
- // kind === "throw":无 HTTP 状态码
31
- return null;
32
- }
33
25
  function rejectAndReply(reply, params, error, errorMessage, providerId) {
34
26
  insertRejectedLog({
35
27
  db: params.db, logId: params.logId, apiType: params.apiType, model: params.model,
@@ -38,33 +30,13 @@ function rejectAndReply(reply, params, error, errorMessage, providerId) {
38
30
  originalBody: params.originalBody, clientHeaders: params.clientHeaders,
39
31
  providerId, originalModel: params.originalModel,
40
32
  isFailover: params.isFailover, originalRequestId: params.originalRequestId,
41
- sessionId: params.sessionId,
33
+ sessionId: params.sessionId, pipelineSnapshot: params.pipelineSnapshot,
34
+ matcher: params.matcher, logFileWriter: params.logFileWriter,
42
35
  });
43
36
  return reply.code(error.statusCode).send(error.body);
44
37
  }
45
- /** tracker blocks 序列化为前端 tryDirectParse 可解析的 JSON */
46
- function serializeBlocksForStorage(blocks, apiType) {
47
- if (!blocks || blocks.length === 0)
48
- return "";
49
- if (apiType === "anthropic") {
50
- const content = blocks.map(b => {
51
- if (b.type === "thinking")
52
- return { type: "thinking", thinking: b.content };
53
- if (b.type === "tool_use") {
54
- let input = {};
55
- try {
56
- input = JSON.parse(b.content || "{}");
57
- }
58
- catch { /* eslint-disable-line taste/no-silent-catch -- tool_use content 非合法 JSON 时保留空对象 */ }
59
- return { type: "tool_use", name: b.name ?? "", input };
60
- }
61
- return { type: "text", text: b.content };
62
- });
63
- return JSON.stringify({ content });
64
- }
65
- const text = blocks.filter(b => b.type === "text").map(b => b.content).join("");
66
- return JSON.stringify({ choices: [{ message: { content: text } }] });
67
- }
38
+ import { getConfig } from "../../config/index.js";
39
+ import { SERVICE_KEYS } from "../../core/container.js";
68
40
  // ---------- Main entry ----------
69
41
  export async function handleProxyRequest(request, reply, apiType, upstreamPath, errors, deps, options) {
70
42
  const socketErrorHandler = (err) => request.log.debug({ err }, "client socket error");
@@ -75,24 +47,36 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
75
47
  const clientModel = request.body.model || "unknown";
76
48
  const sessionId = request.headers["x-claude-code-session-id"];
77
49
  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) {
50
+ // 解析 matcher logFileWriter,传递给日志相关调用
51
+ const matcher = deps.container.resolve(SERVICE_KEYS.matcher);
52
+ const logFileWriter = deps.container.resolve(SERVICE_KEYS.logFileWriter);
53
+ // 在所有加工之前捕获原始 body
54
+ const reqBody = request.body;
55
+ const rawBody = reqBody ? JSON.parse(JSON.stringify(reqBody)) : {};
56
+ const snapshot = new PipelineSnapshot();
57
+ // enhancement 阶段
58
+ const { body: enhancedBody, effectiveModel, originalModel, interceptResponse, meta: enhMeta } = applyEnhancement(deps.db, request.body, clientModel, sessionId, request.routerKey);
59
+ snapshot.add({ stage: "enhancement", router_tags_stripped: enhMeta.router_tags_stripped, directive: enhMeta.directive });
60
+ // tool guard 阶段 — 使用 enhancedBody
61
+ let pipelineBody = enhancedBody;
62
+ const sessionTracker = deps.container.resolve(SERVICE_KEYS.sessionTracker);
63
+ if (enhancementConfig.tool_call_loop_enabled && sessionTracker && sessionId) {
81
64
  const routerKeyId = request.routerKey?.id ?? null;
82
65
  const sessionKey = routerKeyId ? `${routerKeyId}:${sessionId}` : sessionId;
83
- const lastToolUse = extractLastToolUse(request.body);
66
+ const lastToolUse = extractLastToolUse(enhancedBody);
84
67
  if (lastToolUse) {
85
- const toolGuard = new ToolLoopGuard(deps.sessionTracker, {
68
+ const toolGuard = new ToolLoopGuard(sessionTracker, {
86
69
  enabled: true,
87
70
  minConsecutiveCount: 3,
88
71
  detectorConfig: { n: 6, windowSize: 500, repeatThreshold: 5 },
89
72
  });
90
73
  const checkResult = toolGuard.check(sessionKey, lastToolUse);
91
74
  if (checkResult.detected) {
92
- const loopCount = deps.sessionTracker.getLoopCount(sessionKey);
75
+ const loopCount = sessionTracker.getLoopCount(sessionKey);
93
76
  if (loopCount === 1) {
94
77
  // 层级 1:透明重试 — 注入中断提示词
95
- toolGuard.injectLoopBreakPrompt(request.body, apiType, lastToolUse.toolName);
78
+ pipelineBody = toolGuard.injectLoopBreakPrompt(enhancedBody, apiType, lastToolUse.toolName);
79
+ snapshot.add({ stage: "tool_guard", action: "inject_break_prompt", tool: lastToolUse.toolName });
96
80
  request.log.warn({ sessionId, toolName: lastToolUse.toolName, loopCount }, "Tool call loop detected, injecting break prompt");
97
81
  }
98
82
  else if (loopCount === TIER2_LOOP_THRESHOLD) {
@@ -115,18 +99,24 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
115
99
  }
116
100
  }
117
101
  if (interceptResponse)
118
- return handleIntercept(deps.db, apiType, request, reply, interceptResponse, clientModel, sessionId);
102
+ return handleIntercept(deps.db, apiType, request, reply, interceptResponse, clientModel, sessionId, snapshot.toJSON(), matcher, logFileWriter);
119
103
  return executeFailoverLoop({
120
104
  request, reply, apiType, upstreamPath, errors, deps, options,
121
105
  effectiveModel, originalModel,
122
- originalBody: JSON.parse(JSON.stringify(request.body)),
106
+ pipelineBody,
107
+ rawBody,
108
+ baseStages: snapshot.getStages(),
123
109
  sessionId,
124
110
  streamLoopEnabled: enhancementConfig.stream_loop_enabled,
111
+ matcher, logFileWriter,
125
112
  });
126
113
  }
127
114
  // ---------- Failover loop ----------
128
115
  async function executeFailoverLoop(ctx) {
129
- const { request, reply, apiType, upstreamPath, errors, deps, options, effectiveModel, originalModel, originalBody, sessionId, streamLoopEnabled } = ctx;
116
+ const { request, reply, apiType, upstreamPath, errors, deps, options, effectiveModel, originalModel, pipelineBody, rawBody, baseStages, sessionId, streamLoopEnabled, matcher, logFileWriter } = ctx;
117
+ const tracker = deps.container.resolve(SERVICE_KEYS.tracker);
118
+ const usageWindowTracker = deps.container.resolve(SERVICE_KEYS.usageWindowTracker);
119
+ const config = getConfig();
130
120
  const excludeTargets = [];
131
121
  let rootLogId = null;
132
122
  while (true) {
@@ -136,13 +126,18 @@ async function executeFailoverLoop(ctx) {
136
126
  rootLogId = logId;
137
127
  const isFailoverIteration = rootLogId !== logId;
138
128
  const routerKeyId = request.routerKey?.id ?? null;
139
- const body = request.body;
140
- const isStream = body.stream === true;
129
+ // 每次迭代从 pipelineBody 重新开始(不修改 pipelineBody)
130
+ let currentBody = JSON.parse(JSON.stringify(pipelineBody));
131
+ const isStream = currentBody.stream === true;
141
132
  const cliHdrs = request.headers;
133
+ // 构建 per-iteration snapshot
134
+ const iterationSnapshot = new PipelineSnapshot(baseStages);
142
135
  const rCtx = {
143
136
  db: deps.db, logId, apiType, model: effectiveModel,
144
- startTime, isStream, routerKeyId, originalBody, clientHeaders: cliHdrs, originalModel,
137
+ startTime, isStream, routerKeyId, originalBody: rawBody, clientHeaders: cliHdrs, originalModel,
145
138
  isFailover: isFailoverIteration, originalRequestId: isFailoverIteration ? rootLogId : null, sessionId,
139
+ pipelineSnapshot: iterationSnapshot.toJSON(),
140
+ matcher, logFileWriter,
146
141
  };
147
142
  const resolveResult = resolveMapping(deps.db, effectiveModel, { now: new Date(), excludeTargets });
148
143
  request.log.debug({ logId, model: effectiveModel, apiType, isStream, action: "resolve_mapping", resolved: !!resolveResult });
@@ -177,47 +172,63 @@ async function executeFailoverLoop(ctx) {
177
172
  if (provider.api_type !== apiType) {
178
173
  return rejectAndReply(reply, rCtx, errors.providerTypeMismatch(), `API type mismatch: expected '${apiType}'`, resolved.provider_id);
179
174
  }
180
- body.model = resolved.backend_model;
175
+ // routing — 创建新对象而非 in-place mutation
176
+ currentBody = { ...currentBody, model: resolved.backend_model };
177
+ iterationSnapshot.add({ stage: "routing", client_model: effectiveModel, backend_model: resolved.backend_model, provider_id: resolved.provider_id, strategy: resolveResult.targetCount > 1 ? "failover" : "scheduled" });
181
178
  // --- 溢出重定向:上下文超出时切换到更大模型 ---
182
- const overflowResult = applyOverflowRedirect(resolved, deps.db, body);
179
+ const overflowResult = applyOverflowRedirect(resolved, deps.db, currentBody);
183
180
  if (overflowResult) {
184
181
  const overflowProvider = getProviderById(deps.db, overflowResult.provider_id);
185
182
  if (overflowProvider && overflowProvider.is_active && overflowProvider.api_type === apiType) {
186
183
  resolved = { ...resolved, provider_id: overflowResult.provider_id, backend_model: overflowResult.backend_model };
187
184
  provider = overflowProvider;
188
- body.model = overflowResult.backend_model;
185
+ currentBody = { ...currentBody, model: overflowResult.backend_model };
186
+ iterationSnapshot.add({ stage: "overflow", triggered: true, redirect_to: overflowResult.backend_model, redirect_provider: overflowResult.provider_id });
189
187
  }
190
188
  }
191
- applyProviderPatches(body, provider);
192
- const apiKey = decrypt(provider.api_key, getSetting(deps.db, "encryption_key"));
193
- options?.beforeSendProxy?.(body, isStream);
194
- const reqBodyStr = JSON.stringify(body);
195
- const clientReq = JSON.stringify({ headers: cliHdrs, body: originalBody });
189
+ else {
190
+ iterationSnapshot.add({ stage: "overflow", triggered: false });
191
+ }
192
+ // provider patches — 使用返回值
193
+ const { body: patchedBody, meta: patchMeta } = applyProviderPatches(currentBody, provider);
194
+ iterationSnapshot.add({ stage: "provider_patch", types: patchMeta.types });
195
+ const encryptionKey = getSetting(deps.db, "encryption_key");
196
+ if (!encryptionKey) {
197
+ return rejectAndReply(reply, rCtx, errors.providerUnavailable(), `Encryption key not configured`, provider.id);
198
+ }
199
+ const apiKey = decrypt(provider.api_key, encryptionKey);
200
+ options?.beforeSendProxy?.(patchedBody, isStream);
201
+ // logging — 使用 rawBody 作为 client_request,patchedBody 作为 upstream_request
202
+ const reqBodyStr = JSON.stringify(patchedBody);
203
+ const clientReq = JSON.stringify({ headers: cliHdrs, body: rawBody });
196
204
  const upstreamReqBase = JSON.stringify({
197
205
  url: buildUpstreamUrl(provider.base_url, upstreamPath),
198
206
  headers: sanitizeHeadersForLog(buildUpstreamHeaders(cliHdrs, apiKey, Buffer.byteLength(reqBodyStr), apiType)),
199
207
  body: reqBodyStr,
200
208
  });
201
209
  const transportFn = buildTransportFn({
202
- provider, apiKey, body, cliHdrs, reply, upstreamPath, apiType,
210
+ provider, apiKey, body: patchedBody, cliHdrs, reply, upstreamPath, apiType,
203
211
  isStream, startTime, logId, effectiveModel, originalModel,
204
- streamTimeoutMs: deps.streamTimeoutMs, tracker: deps.tracker, matcher: deps.matcher, request,
212
+ streamTimeoutMs: config.STREAM_TIMEOUT_MS, tracker, matcher, request,
205
213
  streamLoopEnabled,
206
214
  });
215
+ const pipelineSnapshot = iterationSnapshot.toJSON();
207
216
  try {
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 });
217
+ const resilienceResult = await deps.orchestrator.handle(request, reply, apiType, { resolved, provider, clientModel: effectiveModel, isStream, trackerId: logId, sessionId, clientRequest: clientReq, upstreamRequest: upstreamReqBase, concurrencyOverride }, { retryBaseDelayMs: config.RETRY_BASE_DELAY_MS, isFailover, ruleMatcher: matcher, transportFn });
209
218
  const lastLogId = logResilienceResult(deps.db, {
210
219
  apiType, model: effectiveModel, providerId: provider.id, isStream,
211
220
  clientReq, upstreamReqBase, logId, routerKeyId, originalModel, sessionId,
212
221
  failover: { isFailoverIteration, rootLogId: rootLogId },
222
+ pipelineSnapshot,
223
+ matcher, logFileWriter,
213
224
  }, resilienceResult.attempts, resilienceResult.result, startTime);
214
225
  collectTransportMetrics(deps.db, apiType, resilienceResult.result, isStream, lastLogId, provider.id, resolved.backend_model, request, routerKeyId, getTransportStatusCode(resilienceResult.result));
215
226
  const tr = resilienceResult.result;
216
227
  const succeeded = tr.kind === "success" || tr.kind === "stream_success" || tr.kind === "stream_abort";
217
228
  if (succeeded)
218
- deps.usageWindowTracker?.recordRequest(provider.id, routerKeyId ?? undefined);
219
- if (isStream && deps.tracker) {
220
- const sc = deps.tracker.get(logId)?.streamContent;
229
+ usageWindowTracker?.recordRequest(provider.id, routerKeyId ?? undefined);
230
+ if (isStream && tracker) {
231
+ const sc = tracker.get(logId)?.streamContent;
221
232
  const blocks = sc?.blocks;
222
233
  const hasStructured = blocks && blocks.length > 0 && blocks.some(b => b.type !== "text");
223
234
  const content = hasStructured
@@ -239,6 +250,15 @@ async function executeFailoverLoop(ctx) {
239
250
  // 对 failover 场景的错误也不发送——这些情况需要外层 proxy-handler 处理
240
251
  if (!reply.raw.headersSent) {
241
252
  const tr = resilienceResult.result;
253
+ if (tr.kind === "success") {
254
+ // response transform — 注入 model info tag
255
+ const { body: finalBody, meta: respMeta } = maybeInjectModelInfoTag(tr.body, originalModel, effectiveModel);
256
+ if (respMeta.model_info_tag_injected) {
257
+ iterationSnapshot.add({ stage: "response_transform", model_info_tag_injected: true });
258
+ updateLogPipelineSnapshot(deps.db, lastLogId, iterationSnapshot.toJSON());
259
+ }
260
+ return reply.code(tr.statusCode).send(finalBody);
261
+ }
242
262
  if (tr.kind === "throw" || (tr.kind === "error" && tr.statusCode >= HTTP_ERROR_THRESHOLD)) {
243
263
  const err = errors.upstreamConnectionFailed();
244
264
  updateLogClientStatus(deps.db, lastLogId, err.statusCode);
@@ -260,6 +280,8 @@ async function executeFailoverLoop(ctx) {
260
280
  apiType, model: effectiveModel, providerId: provider.id, isStream,
261
281
  clientReq, upstreamReqBase, logId, routerKeyId, originalModel, sessionId,
262
282
  failover: { isFailoverIteration, rootLogId: rootLogId },
283
+ pipelineSnapshot,
284
+ matcher, logFileWriter,
263
285
  }, e.attempts, fakeResult, startTime);
264
286
  }
265
287
  request.log.debug({ logId, action: "provider_switch", targetProviderId: e.targetProviderId });
@@ -282,50 +304,12 @@ async function executeFailoverLoop(ctx) {
282
304
  is_failover: isFailoverIteration ? 1 : 0, original_request_id: isFailoverIteration ? rootLogId : null,
283
305
  router_key_id: routerKeyId, original_model: originalModel,
284
306
  session_id: sessionId,
285
- });
307
+ pipeline_snapshot: pipelineSnapshot,
308
+ }, (matcher || logFileWriter) ? {
309
+ matcher, logFileWriter, responseBody: null,
310
+ } : undefined);
286
311
  const err = errors.upstreamConnectionFailed();
287
312
  return reply.code(err.statusCode).send(err.body);
288
313
  }
289
314
  }
290
315
  }
291
- function extractLastToolUse(body) {
292
- const messages = body.messages;
293
- if (!messages)
294
- return null;
295
- for (let i = messages.length - 1; i >= 0; i--) {
296
- const msg = messages[i];
297
- if (msg.role === "assistant") {
298
- const content = msg.content;
299
- if (Array.isArray(content)) {
300
- for (let j = content.length - 1; j >= 0; j--) {
301
- const block = content[j];
302
- if (block.type !== "tool_use")
303
- continue;
304
- const id = block.id;
305
- if (id && (id.startsWith(TOOL_USE_ID_PREFIX) || id.startsWith(TOOL_USE_ID_PROVIDER_PREFIX))) {
306
- continue;
307
- }
308
- const inputStr = JSON.stringify(block.input ?? {});
309
- return {
310
- toolName: block.name,
311
- toolUseId: typeof block.id === "string" ? block.id : undefined,
312
- inputHash: simpleHash(inputStr),
313
- inputText: inputStr,
314
- timestamp: Date.now(),
315
- };
316
- }
317
- }
318
- }
319
- }
320
- return null;
321
- }
322
- function simpleHash(s) {
323
- const HASH_SHIFT = 5;
324
- let hash = 0;
325
- for (let i = 0; i < s.length; i++) {
326
- const char = s.charCodeAt(i);
327
- hash = ((hash << HASH_SHIFT) - hash) + char;
328
- hash |= 0;
329
- }
330
- return String(hash);
331
- }
@@ -0,0 +1,12 @@
1
+ export interface RetryMatcher {
2
+ test: (statusCode: number, body: string) => boolean;
3
+ }
4
+ /**
5
+ * 判断一条日志是否需要保留全文详情到 DB。
6
+ * - hasFileWriter=false 时保守保留全文(避免数据丢失)
7
+ * - status >= 400 → 保留
8
+ * - matcher 为 null → 保守保留
9
+ * - matcher 命中 → 保留
10
+ * - 否则 → 只存摘要(文件已有全文备份)
11
+ */
12
+ export declare function shouldPreserveDetail(statusCode: number | null, responseBody: string | null, matcher: RetryMatcher | null, hasFileWriter?: boolean): boolean;
@@ -0,0 +1,21 @@
1
+ // src/proxy/log-detail-policy.ts
2
+ const HTTP_ERROR_THRESHOLD = 400;
3
+ /**
4
+ * 判断一条日志是否需要保留全文详情到 DB。
5
+ * - hasFileWriter=false 时保守保留全文(避免数据丢失)
6
+ * - status >= 400 → 保留
7
+ * - matcher 为 null → 保守保留
8
+ * - matcher 命中 → 保留
9
+ * - 否则 → 只存摘要(文件已有全文备份)
10
+ */
11
+ export function shouldPreserveDetail(statusCode, responseBody, matcher, hasFileWriter = true) {
12
+ if (!hasFileWriter)
13
+ return true;
14
+ if (statusCode !== null && statusCode >= HTTP_ERROR_THRESHOLD)
15
+ return true;
16
+ if (!matcher)
17
+ return true;
18
+ if (responseBody && matcher.test(statusCode ?? 0, responseBody))
19
+ return true;
20
+ return false;
21
+ }
@@ -1,5 +1,7 @@
1
1
  import Database from "better-sqlite3";
2
2
  import type { Provider } from "../db/index.js";
3
+ import type { LogFileWriter } from "../storage/log-file-writer.js";
4
+ import type { RetryMatcher } from "./log-detail-policy.js";
3
5
  import type { RawHeaders } from "./types.js";
4
6
  export interface FailoverContext {
5
7
  isFailoverIteration: boolean;
@@ -25,6 +27,9 @@ export interface RequestLogParams extends LogRetryMeta {
25
27
  routerKeyId?: string | null;
26
28
  originalModel?: string | null;
27
29
  sessionId?: string | null;
30
+ pipelineSnapshot?: string | null;
31
+ matcher?: RetryMatcher | null;
32
+ logFileWriter?: LogFileWriter | null;
28
33
  }
29
34
  /** 插入成功请求日志,供 openai/anthropic 插件共享 */
30
35
  export declare function insertSuccessLog(db: Database.Database, params: RequestLogParams): void;
@@ -43,6 +48,9 @@ export interface RejectedLogParams extends LogRetryMeta {
43
48
  providerId?: string | null;
44
49
  originalModel?: string | null;
45
50
  sessionId?: string | null;
51
+ pipelineSnapshot?: string | null;
52
+ matcher?: RetryMatcher | null;
53
+ logFileWriter?: LogFileWriter | null;
46
54
  }
47
55
  /** Log a request rejected before reaching upstream */
48
56
  export declare function insertRejectedLog(params: RejectedLogParams): void;
@@ -1,7 +1,12 @@
1
1
  import { insertRequestLog } from "../db/index.js";
2
2
  /** 插入成功请求日志,供 openai/anthropic 插件共享 */
3
3
  export function insertSuccessLog(db, params) {
4
- const { id: logId, apiType, model, provider, isStream, startTime, clientReq, upstreamReq, status, respBody, upHdrs, isRetry = false, isFailover = false, originalRequestId = null, routerKeyId = null, originalModel = null, sessionId = null } = params;
4
+ const { id: logId, apiType, model, provider, isStream, startTime, clientReq, upstreamReq, status, respBody, upHdrs, isRetry = false, isFailover = false, originalRequestId = null, routerKeyId = null, originalModel = null, sessionId = null, pipelineSnapshot = null, matcher, logFileWriter } = params;
5
+ const writeContext = (matcher || logFileWriter) ? {
6
+ matcher,
7
+ logFileWriter,
8
+ responseBody: respBody,
9
+ } : undefined;
5
10
  insertRequestLog(db, {
6
11
  id: logId, api_type: apiType, model, provider_id: provider.id,
7
12
  status_code: status, latency_ms: Date.now() - startTime,
@@ -12,11 +17,17 @@ export function insertSuccessLog(db, params) {
12
17
  is_retry: isRetry ? 1 : 0, is_failover: isFailover ? 1 : 0, original_request_id: originalRequestId,
13
18
  router_key_id: routerKeyId, original_model: originalModel,
14
19
  session_id: sessionId,
15
- });
20
+ pipeline_snapshot: pipelineSnapshot ?? null,
21
+ }, writeContext);
16
22
  }
17
23
  /** Log a request rejected before reaching upstream */
18
24
  export function insertRejectedLog(params) {
19
- const { db, logId, apiType, model, statusCode, errorMessage, startTime, isStream, routerKeyId, originalBody, clientHeaders, providerId = null, isFailover = false, originalRequestId = null, originalModel = null, sessionId = null } = params;
25
+ const { db, logId, apiType, model, statusCode, errorMessage, startTime, isStream, routerKeyId, originalBody, clientHeaders, providerId = null, isFailover = false, originalRequestId = null, originalModel = null, sessionId = null, pipelineSnapshot = null, matcher, logFileWriter } = params;
26
+ const writeContext = (matcher || logFileWriter) ? {
27
+ matcher,
28
+ logFileWriter,
29
+ responseBody: null,
30
+ } : undefined;
20
31
  insertRequestLog(db, {
21
32
  id: logId,
22
33
  api_type: apiType,
@@ -33,5 +44,6 @@ export function insertRejectedLog(params) {
33
44
  router_key_id: routerKeyId,
34
45
  original_model: originalModel,
35
46
  session_id: sessionId,
36
- });
47
+ pipeline_snapshot: pipelineSnapshot ?? null,
48
+ }, writeContext);
37
49
  }
@@ -9,5 +9,5 @@ export declare class ToolLoopGuard {
9
9
  * 如果 sessionKey 不可用(无 sessionId),返回 detected: false。
10
10
  */
11
11
  check(sessionKey: string | null, toolCall: ToolCallRecord | null): LoopCheckResult;
12
- injectLoopBreakPrompt(body: Record<string, unknown>, apiType: "openai" | "anthropic", toolName: string): void;
12
+ injectLoopBreakPrompt(body: Record<string, unknown>, apiType: "openai" | "anthropic", toolName: string): Record<string, unknown>;
13
13
  }
@@ -36,28 +36,25 @@ export class ToolLoopGuard {
36
36
  return { detected: false };
37
37
  }
38
38
  injectLoopBreakPrompt(body, apiType, toolName) {
39
- const prompt = `你正在重复调用同一个工具 "${toolName}"。` +
40
- `这很可能陷入了一个循环。请仔细回顾对话历史,` +
41
- `分析之前调用该工具的结果,停止重复调用,` +
42
- `改用其他方式完成任务或直接告知用户你遇到的问题。`;
39
+ const cloned = JSON.parse(JSON.stringify(body));
40
+ const prompt = `[系统提醒] 检测到你可能陷入了反复调用 "${toolName}" 工具的循环。请停下来,总结当前进展,直接告知用户。`;
43
41
  if (apiType === "anthropic") {
44
- const system = body.system;
42
+ const system = cloned.system;
45
43
  if (Array.isArray(system)) {
46
44
  system.push({ type: "text", text: prompt });
47
45
  }
48
46
  else if (typeof system === "string") {
49
- body.system = [{ type: "text", text: system }, { type: "text", text: prompt }];
47
+ cloned.system = [{ type: "text", text: system }, { type: "text", text: prompt }];
50
48
  }
51
49
  else {
52
- body.system = [{ type: "text", text: prompt }];
50
+ cloned.system = [{ type: "text", text: prompt }];
53
51
  }
54
52
  }
55
53
  else {
56
- // OpenAI: messages 开头插入 system message
57
- const messages = body.messages;
58
- if (messages) {
59
- messages.unshift({ role: "system", content: prompt });
60
- }
54
+ const messages = cloned.messages ?? [];
55
+ messages.unshift({ role: "system", content: prompt });
56
+ cloned.messages = messages;
61
57
  }
58
+ return cloned;
62
59
  }
63
60
  }
@@ -1,13 +1,13 @@
1
1
  import type { FastifyReply, FastifyRequest } from "fastify";
2
- import type { TransportResult } from "./types.js";
3
- import type { Target, ConcurrencyOverride } from "./strategy/types.js";
2
+ import type { TransportResult } from "../types.js";
3
+ import type { Target, ConcurrencyOverride } from "../../core/types.js";
4
4
  import type { ResilienceLayer, ResilienceResult } from "./resilience.js";
5
5
  import type { RetryRuleMatcher } from "./retry-rules.js";
6
6
  import type { SemaphoreScope } from "./scope.js";
7
7
  import type { TrackerScope } from "./scope.js";
8
8
  import type { ProviderSemaphoreManager } from "./semaphore.js";
9
- import type { RequestTracker } from "../monitor/request-tracker.js";
10
- import type { AdaptiveConcurrencyController } from "./adaptive-controller.js";
9
+ import type { RequestTracker } from "../../monitor/request-tracker.js";
10
+ import type { AdaptiveConcurrencyController } from "../adaptive-controller.js";
11
11
  export interface OrchestratorConfig {
12
12
  resolved: Target;
13
13
  provider: {
@@ -26,6 +26,8 @@ export interface OrchestratorConfig {
26
26
  sessionId?: string;
27
27
  /** 客户端请求的 JSON 字符串(headers + body),用于 Monitor 实时查看 */
28
28
  clientRequest?: string;
29
+ /** 上游请求的 JSON 字符串(url + headers + body),用于 Monitor 实时查看 */
30
+ upstreamRequest?: string;
29
31
  /** Schedule 层的并发覆盖配置,覆盖 Provider 默认并发限制 */
30
32
  concurrencyOverride?: ConcurrencyOverride;
31
33
  }
@@ -1,4 +1,4 @@
1
- import { ProviderSwitchNeeded } from "./types.js";
1
+ import { ProviderSwitchNeeded } from "../types.js";
2
2
  import { ResilienceLayer as ResilienceLayerClass } from "./resilience.js";
3
3
  import { SemaphoreScope as SemaphoreScopeClass } from "./scope.js";
4
4
  import { TrackerScope as TrackerScopeClass } from "./scope.js";
@@ -69,6 +69,7 @@ export class ProxyOrchestrator {
69
69
  clientIp: request.ip,
70
70
  sessionId: config.sessionId,
71
71
  clientRequest: config.clientRequest,
72
+ upstreamRequest: config.upstreamRequest,
72
73
  };
73
74
  }
74
75
  createAbortSignal(request) {
@@ -1,6 +1,6 @@
1
1
  import type { RetryRuleMatcher } from "./retry-rules.js";
2
- import type { TransportResult } from "./types.js";
3
- import type { Target } from "./strategy/types.js";
2
+ import type { TransportResult } from "../types.js";
3
+ import type { Target, ResilienceAttempt } from "../../core/types.js";
4
4
  export interface RetryStrategy {
5
5
  getDelay(attempt: number): number;
6
6
  }
@@ -28,18 +28,6 @@ export interface ResilienceConfig {
28
28
  /** 全局迭代上限,防止极端配置导致 while(true) 循环过多 */
29
29
  iterationCap?: number;
30
30
  }
31
- export interface ResilienceAttempt {
32
- target: Target;
33
- attemptIndex: number;
34
- statusCode: number | null;
35
- error: string | null;
36
- latencyMs: number;
37
- responseBody: string | null;
38
- /** 上游响应 headers(throw 和 stream_success/stream_abort 时为 null) */
39
- responseHeaders: Record<string, string> | null;
40
- /** TransportResult.kind,用于区分 stream_error 等特殊类型 */
41
- resultKind: TransportResult["kind"];
42
- }
43
31
  export interface ResilienceResult {
44
32
  result: TransportResult;
45
33
  attempts: ResilienceAttempt[];
@@ -1,5 +1,5 @@
1
- import { MS_PER_SECOND } from "../constants.js";
2
- import { ProviderSwitchNeeded } from "./types.js";
1
+ import { MS_PER_SECOND } from "../../core/constants.js";
2
+ import { ProviderSwitchNeeded } from "../types.js";
3
3
  export class FixedIntervalStrategy {
4
4
  delayMs;
5
5
  constructor(delayMs) {
@@ -1,5 +1,5 @@
1
1
  import Database from "better-sqlite3";
2
- import { type RetryRule } from "../db/index.js";
2
+ import { type RetryRule } from "../../db/retry-rules.js";
3
3
  export declare class RetryRuleMatcher {
4
4
  private cache;
5
5
  private raw;
@@ -1,4 +1,4 @@
1
- import { getActiveRetryRules } from "../db/index.js";
1
+ import { getActiveRetryRules } from "../../db/retry-rules.js";
2
2
  export class RetryRuleMatcher {
3
3
  cache = new Map();
4
4
  raw = [];
@@ -1,7 +1,7 @@
1
1
  import type { ProviderSemaphoreManager } from "./semaphore.js";
2
- import type { ConcurrencyOverride } from "./strategy/types.js";
3
- import type { RequestTracker } from "../monitor/request-tracker.js";
4
- import type { ActiveRequest, AttemptSnapshot } from "../monitor/types.js";
2
+ import type { ConcurrencyOverride } from "../../core/types.js";
3
+ import type { RequestTracker } from "../../monitor/request-tracker.js";
4
+ import type { ActiveRequest, AttemptSnapshot } from "../../monitor/types.js";
5
5
  export declare class SemaphoreScope {
6
6
  private manager;
7
7
  constructor(manager: ProviderSemaphoreManager);