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.
- package/README.md +69 -0
- package/dist/admin/constants.d.ts +1 -1
- package/dist/admin/constants.js +2 -2
- package/dist/admin/logs.d.ts +2 -0
- package/dist/admin/logs.js +17 -1
- package/dist/admin/providers.d.ts +2 -2
- package/dist/admin/providers.js +29 -16
- package/dist/admin/proxy-enhancement.d.ts +2 -0
- package/dist/admin/proxy-enhancement.js +4 -10
- package/dist/admin/retry-rules.d.ts +2 -2
- package/dist/admin/retry-rules.js +4 -8
- package/dist/admin/routes.d.ts +5 -4
- package/dist/admin/routes.js +7 -7
- package/dist/admin/settings-import-export.d.ts +2 -4
- package/dist/admin/settings-import-export.js +9 -19
- package/dist/admin/settings.d.ts +1 -0
- package/dist/admin/settings.js +29 -1
- package/dist/admin/upgrade.d.ts +1 -0
- package/dist/admin/upgrade.js +37 -4
- package/dist/{constants.d.ts → core/constants.d.ts} +4 -1
- package/dist/{constants.js → core/constants.js} +21 -1
- package/dist/core/container.d.ts +31 -0
- package/dist/core/container.js +41 -0
- package/dist/core/errors.d.ts +26 -0
- package/dist/core/errors.js +42 -0
- package/dist/core/registry.d.ts +43 -0
- package/dist/core/registry.js +3 -0
- package/dist/core/types.d.ts +105 -0
- package/dist/core/types.js +3 -0
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/logs.d.ts +11 -24
- package/dist/db/logs.js +37 -38
- package/dist/db/metrics.js +1 -1
- package/dist/db/migrations/033_add_pipeline_snapshot.sql +1 -0
- package/dist/db/migrations/034_drop_redundant_log_columns.sql +13 -0
- package/dist/db/settings.d.ts +2 -0
- package/dist/db/settings.js +9 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +196 -108
- package/dist/metrics/metrics-extractor.d.ts +1 -24
- package/dist/metrics/metrics-extractor.js +1 -1
- package/dist/metrics/sse-metrics-transform.d.ts +1 -1
- package/dist/middleware/admin-auth.js +4 -0
- package/dist/middleware/auth.js +1 -2
- package/dist/monitor/request-tracker.d.ts +3 -4
- package/dist/monitor/request-tracker.js +6 -16
- package/dist/monitor/runtime-collector.js +1 -1
- package/dist/monitor/types.d.ts +8 -0
- package/dist/proxy/adaptive-controller.d.ts +4 -1
- package/dist/proxy/adaptive-controller.js +5 -0
- package/dist/proxy/enhancement/enhancement-handler.d.ts +19 -3
- package/dist/proxy/enhancement/enhancement-handler.js +80 -28
- package/dist/proxy/enhancement/index.d.ts +1 -0
- package/dist/proxy/handler/anthropic.d.ts +7 -0
- package/dist/proxy/{anthropic.js → handler/anthropic.js} +8 -7
- package/dist/proxy/handler/openai.d.ts +7 -0
- package/dist/proxy/{openai.js → handler/openai.js} +10 -9
- package/dist/proxy/handler/proxy-handler-utils.d.ts +9 -0
- package/dist/proxy/handler/proxy-handler-utils.js +63 -0
- package/dist/proxy/handler/proxy-handler.d.ts +13 -0
- package/dist/proxy/{proxy-handler.js → handler/proxy-handler.js} +104 -120
- package/dist/proxy/log-detail-policy.d.ts +12 -0
- package/dist/proxy/log-detail-policy.js +21 -0
- package/dist/proxy/log-helpers.d.ts +8 -0
- package/dist/proxy/log-helpers.js +16 -4
- package/dist/proxy/loop-prevention/tool-loop-guard.d.ts +1 -1
- package/dist/proxy/loop-prevention/tool-loop-guard.js +9 -12
- package/dist/proxy/{orchestrator.d.ts → orchestration/orchestrator.d.ts} +6 -4
- package/dist/proxy/{orchestrator.js → orchestration/orchestrator.js} +2 -1
- package/dist/proxy/{resilience.d.ts → orchestration/resilience.d.ts} +2 -14
- package/dist/proxy/{resilience.js → orchestration/resilience.js} +2 -2
- package/dist/proxy/{retry-rules.d.ts → orchestration/retry-rules.d.ts} +1 -1
- package/dist/proxy/{retry-rules.js → orchestration/retry-rules.js} +1 -1
- package/dist/proxy/{scope.d.ts → orchestration/scope.d.ts} +3 -3
- package/dist/proxy/{semaphore.d.ts → orchestration/semaphore.d.ts} +7 -15
- package/dist/proxy/{semaphore.js → orchestration/semaphore.js} +12 -26
- package/dist/proxy/patch/index.d.ts +8 -2
- package/dist/proxy/patch/index.js +5 -2
- package/dist/proxy/pipeline-snapshot.d.ts +37 -0
- package/dist/proxy/pipeline-snapshot.js +15 -0
- package/dist/proxy/proxy-core.d.ts +1 -1
- package/dist/proxy/proxy-core.js +1 -1
- package/dist/proxy/proxy-logging.d.ts +10 -2
- package/dist/proxy/proxy-logging.js +23 -9
- package/dist/proxy/response-transform.d.ts +7 -0
- package/dist/proxy/response-transform.js +15 -0
- package/dist/proxy/{enhancement-config.js → routing/enhancement-config.js} +1 -1
- package/dist/proxy/{mapping-resolver.d.ts → routing/mapping-resolver.d.ts} +1 -1
- package/dist/proxy/{mapping-resolver.js → routing/mapping-resolver.js} +1 -1
- package/dist/proxy/{model-state.js → routing/model-state.js} +1 -1
- package/dist/proxy/{overflow.d.ts → routing/overflow.d.ts} +1 -1
- package/dist/proxy/{overflow.js → routing/overflow.js} +3 -3
- package/dist/proxy/{usage-window-tracker.js → routing/usage-window-tracker.js} +3 -3
- package/dist/proxy/{transport.d.ts → transport/http.d.ts} +2 -2
- package/dist/proxy/{transport.js → transport/http.js} +3 -3
- package/dist/proxy/{stream-proxy.d.ts → transport/stream.d.ts} +4 -4
- package/dist/proxy/{stream-proxy.js → transport/stream.js} +25 -7
- package/dist/proxy/{transport-fn.d.ts → transport/transport-fn.d.ts} +5 -5
- package/dist/proxy/{transport-fn.js → transport/transport-fn.js} +11 -9
- package/dist/proxy/types.d.ts +3 -64
- package/dist/proxy/types.js +5 -34
- package/dist/storage/log-file-compressor.d.ts +15 -0
- package/dist/storage/log-file-compressor.js +83 -0
- package/dist/storage/log-file-writer.d.ts +21 -0
- package/dist/storage/log-file-writer.js +103 -0
- package/dist/storage/types.d.ts +16 -0
- package/dist/storage/types.js +5 -0
- package/dist/upgrade/deployment.d.ts +13 -0
- package/dist/upgrade/deployment.js +40 -0
- package/frontend-dist/assets/{CardContent-jQcfCC7J.js → CardContent-CxOF1feY.js} +1 -1
- package/frontend-dist/assets/{CardTitle-BrCTvULL.js → CardTitle-BSEFcEOM.js} +1 -1
- package/frontend-dist/assets/{CascadingModelSelect-BFh67j5d.js → CascadingModelSelect-DTwksDPZ.js} +1 -1
- package/frontend-dist/assets/{Checkbox-Bbt7JpdE.js → Checkbox-RfsERG07.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-DMnEA0qC.js → CollapsibleTrigger-Dsjo7QlC.js} +1 -1
- package/frontend-dist/assets/{Collection-CVk3TPHc.js → Collection-rQ4eIYfa.js} +1 -1
- package/frontend-dist/assets/{Dashboard-Coftbg4B.js → Dashboard-YejfAPiB.js} +1 -1
- package/frontend-dist/assets/{DialogTitle-BbOAZzPQ.js → DialogTitle-DeFTnmgC.js} +1 -1
- package/frontend-dist/assets/{Input-DdHY9q0w.js → Input-CENz_g9t.js} +1 -1
- package/frontend-dist/assets/{Label-DRQv_Dr_.js → Label-BAciBrrd.js} +1 -1
- package/frontend-dist/assets/{Login-SV3ctFnJ.js → Login-DQkYFq7R.js} +1 -1
- package/frontend-dist/assets/{Logs-BG45kX6E.js → Logs-Dol8AX7z.js} +1 -1
- package/frontend-dist/assets/{ModelMappings-DEaBnRU3.js → ModelMappings-VEYW1TrW.js} +1 -1
- package/frontend-dist/assets/{Monitor-ZHOt11n-.js → Monitor-C0r9WefB.js} +1 -1
- package/frontend-dist/assets/{PopoverTrigger-z-Z3EjBk.js → PopoverTrigger-Cyqik5SE.js} +1 -1
- package/frontend-dist/assets/{PopperContent-DPC-6a3n.js → PopperContent-B7IuAHeq.js} +1 -1
- package/frontend-dist/assets/{Providers-DpY6pAcg.js → Providers-D8Z97edN.js} +1 -1
- package/frontend-dist/assets/{ProxyEnhancement-D6KBDXMp.js → ProxyEnhancement-Kn8r2SN6.js} +1 -1
- package/frontend-dist/assets/{RetryRules-DWI7_WLZ.js → RetryRules-F0295m4_.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-CZ1657eX.js → RouterKeys-CFbPtUE_.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-BREE2YEV.js → RovingFocusItem-D291Vjh8.js} +1 -1
- package/frontend-dist/assets/{Schedules-BVPsBRPi.js → Schedules-DWhF3uod.js} +1 -1
- package/frontend-dist/assets/{SelectValue-H8hwQwbk.js → SelectValue-BWlgUZa3.js} +1 -1
- package/frontend-dist/assets/Settings-BnIzEF_k.js +6 -0
- package/frontend-dist/assets/{Setup-yOYNKkOG.js → Setup-BglKyQKq.js} +1 -1
- package/frontend-dist/assets/{Switch-CojD3rTH.js → Switch-DyCR-CPu.js} +1 -1
- package/frontend-dist/assets/{TableHeader-awoHTsWN.js → TableHeader-DVUlBL35.js} +1 -1
- package/frontend-dist/assets/{TabsTrigger-DTKSFj85.js → TabsTrigger-BU1DY-C8.js} +1 -1
- package/frontend-dist/assets/{Teleport-DehYAXud.js → Teleport-BQgusr9g.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-C2dl_dml.js → TooltipTrigger-Bv_QoBns.js} +1 -1
- package/frontend-dist/assets/{UnifiedRequestDialog-C8A-uSTR.js → UnifiedRequestDialog-f_evI835.js} +2 -2
- package/frontend-dist/assets/{VisuallyHidden-C8oaGi2S.js → VisuallyHidden-Con10z4F.js} +1 -1
- package/frontend-dist/assets/{VisuallyHiddenInput-BMc813t2.js → VisuallyHiddenInput-yrDtxucb.js} +1 -1
- package/frontend-dist/assets/{alert-dialog-C8TZQmU6.js → alert-dialog-2Db6Z7JQ.js} +1 -1
- package/frontend-dist/assets/arrow-down-WyouvE7T.js +1 -0
- package/frontend-dist/assets/{badge-BVh2WpA5.js → badge-DEhZfeI0.js} +1 -1
- package/frontend-dist/assets/button-Cnkbp_6J.js +12 -0
- package/frontend-dist/assets/check-BuqB5Nyb.js +1 -0
- package/frontend-dist/assets/{copy-DTOecxa9.js → copy-CwqZSuIG.js} +1 -1
- package/frontend-dist/assets/{dialog-kA7AUNoc.js → dialog-CVMKSdPr.js} +1 -1
- package/frontend-dist/assets/{file-text-DzZCFO7y.js → file-text-D0K8Hovo.js} +1 -1
- package/frontend-dist/assets/index-Ct718O93.js +1 -0
- package/frontend-dist/assets/{lib-ClDokUbt.js → lib-H3YI7EK4.js} +1 -1
- package/frontend-dist/assets/loader-circle-Be82FnVY.js +1 -0
- package/frontend-dist/assets/{useClipboard-DU1ne-Jw.js → useClipboard-Cd7k-5Yq.js} +1 -1
- package/frontend-dist/assets/{useFocusGuards-Btmdbg_F.js → useFocusGuards-luoLXnwV.js} +1 -1
- package/frontend-dist/assets/useFormControl-Da4ViGZF.js +1 -0
- package/frontend-dist/assets/{useLogRetention--EGNWXig.js → useLogRetention-DB4Iu6o_.js} +1 -1
- package/frontend-dist/assets/useNonce-DvAdQ48J.js +1 -0
- package/frontend-dist/assets/x-DB22csQl.js +1 -0
- package/frontend-dist/index.html +19 -19
- package/package.json +1 -1
- package/dist/proxy/anthropic.d.ts +0 -19
- package/dist/proxy/openai.d.ts +0 -19
- package/dist/proxy/proxy-handler.d.ts +0 -19
- package/dist/proxy/strategy/types.d.ts +0 -21
- package/dist/proxy/strategy/types.js +0 -1
- package/frontend-dist/assets/Settings-DHYaYRgU.js +0 -6
- package/frontend-dist/assets/arrow-down-D-cQXxau.js +0 -1
- package/frontend-dist/assets/button-N59D1BGa.js +0 -12
- package/frontend-dist/assets/check-dDgrw3T3.js +0 -1
- package/frontend-dist/assets/index-DVTeNVaa.js +0 -1
- package/frontend-dist/assets/loader-circle-DVHRL-38.js +0 -1
- package/frontend-dist/assets/useFormControl-C5Kjziuj.js +0 -1
- package/frontend-dist/assets/useNonce-Cp31yRzV.js +0 -1
- package/frontend-dist/assets/x-DMktsI_w.js +0 -1
- /package/dist/{config.d.ts → config/index.d.ts} +0 -0
- /package/dist/{config.js → config/index.js} +0 -0
- /package/dist/proxy/{scope.js → orchestration/scope.js} +0 -0
- /package/dist/proxy/{enhancement-config.d.ts → routing/enhancement-config.d.ts} +0 -0
- /package/dist/proxy/{model-state.d.ts → routing/model-state.d.ts} +0 -0
- /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 "
|
|
3
|
-
import { getProviderById, insertRequestLog } from "
|
|
4
|
-
import { decrypt } from "
|
|
5
|
-
import { getSetting } from "
|
|
6
|
-
import { resolveMapping } from "
|
|
7
|
-
import { applyEnhancement } from "
|
|
8
|
-
import { SemaphoreQueueFullError, SemaphoreTimeoutError } from "
|
|
9
|
-
import { logResilienceResult, collectTransportMetrics, handleIntercept, sanitizeHeadersForLog, } from "
|
|
10
|
-
import { buildUpstreamHeaders, buildUpstreamUrl } from "
|
|
11
|
-
import { ProviderSwitchNeeded } from "
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { loadEnhancementConfig } from "
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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(
|
|
66
|
+
const lastToolUse = extractLastToolUse(enhancedBody);
|
|
84
67
|
if (lastToolUse) {
|
|
85
|
-
const toolGuard = new ToolLoopGuard(
|
|
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 =
|
|
75
|
+
const loopCount = sessionTracker.getLoopCount(sessionKey);
|
|
93
76
|
if (loopCount === 1) {
|
|
94
77
|
// 层级 1:透明重试 — 注入中断提示词
|
|
95
|
-
toolGuard.injectLoopBreakPrompt(
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
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:
|
|
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:
|
|
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
|
-
|
|
219
|
-
if (isStream &&
|
|
220
|
-
const sc =
|
|
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):
|
|
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
|
|
40
|
-
|
|
41
|
-
`分析之前调用该工具的结果,停止重复调用,` +
|
|
42
|
-
`改用其他方式完成任务或直接告知用户你遇到的问题。`;
|
|
39
|
+
const cloned = JSON.parse(JSON.stringify(body));
|
|
40
|
+
const prompt = `[系统提醒] 检测到你可能陷入了反复调用 "${toolName}" 工具的循环。请停下来,总结当前进展,直接告知用户。`;
|
|
43
41
|
if (apiType === "anthropic") {
|
|
44
|
-
const 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
|
-
|
|
47
|
+
cloned.system = [{ type: "text", text: system }, { type: "text", text: prompt }];
|
|
50
48
|
}
|
|
51
49
|
else {
|
|
52
|
-
|
|
50
|
+
cloned.system = [{ type: "text", text: prompt }];
|
|
53
51
|
}
|
|
54
52
|
}
|
|
55
53
|
else {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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 "
|
|
3
|
-
import type { Target, ConcurrencyOverride } from "
|
|
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 "
|
|
10
|
-
import type { AdaptiveConcurrencyController } from "
|
|
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 "
|
|
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 "
|
|
3
|
-
import type { Target } from "
|
|
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 "
|
|
2
|
-
import { ProviderSwitchNeeded } from "
|
|
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,7 +1,7 @@
|
|
|
1
1
|
import type { ProviderSemaphoreManager } from "./semaphore.js";
|
|
2
|
-
import type { ConcurrencyOverride } from "
|
|
3
|
-
import type { RequestTracker } from "
|
|
4
|
-
import type { ActiveRequest, AttemptSnapshot } from "
|
|
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);
|