llm-simple-router 0.7.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/proxy-enhancement.js +3 -1
- package/dist/admin/routes.d.ts +1 -0
- package/dist/admin/routes.js +3 -1
- package/dist/admin/settings-import-export.d.ts +1 -0
- package/dist/admin/settings-import-export.js +7 -0
- package/dist/admin/transform-rules.d.ts +8 -0
- package/dist/admin/transform-rules.js +38 -0
- package/dist/admin/usage.js +1 -1
- package/dist/core/container.d.ts +1 -0
- package/dist/core/container.js +1 -0
- package/dist/db/migrations/034_create_provider_transform_rules.sql +11 -0
- package/dist/db/transform-rules.d.ts +16 -0
- package/dist/db/transform-rules.js +51 -0
- package/dist/index.js +30 -1
- package/dist/metrics/sse-parser.d.ts +2 -0
- package/dist/metrics/sse-parser.js +4 -0
- package/dist/monitor/request-tracker.d.ts +2 -0
- package/dist/monitor/request-tracker.js +22 -1
- package/dist/monitor/types.d.ts +1 -1
- package/dist/proxy/enhancement/response-cleaner.js +14 -6
- package/dist/proxy/handler/openai.js +13 -4
- package/dist/proxy/handler/proxy-handler-utils.js +2 -7
- package/dist/proxy/handler/proxy-handler.js +85 -18
- package/dist/proxy/patch/deepseek/index.d.ts +15 -3
- package/dist/proxy/patch/deepseek/index.js +29 -6
- package/dist/proxy/patch/deepseek/patch-cache-control.d.ts +6 -0
- package/dist/proxy/patch/deepseek/patch-cache-control.js +30 -0
- package/dist/proxy/patch/deepseek/patch-non-deepseek-tools.d.ts +16 -0
- package/dist/proxy/patch/deepseek/patch-non-deepseek-tools.js +74 -0
- package/dist/proxy/patch/deepseek/patch-orphan-tool-results.d.ts +10 -1
- package/dist/proxy/patch/deepseek/patch-orphan-tool-results.js +58 -15
- package/dist/proxy/patch/deepseek/patch-thinking-blocks.d.ts +5 -1
- package/dist/proxy/patch/deepseek/patch-thinking-blocks.js +37 -4
- package/dist/proxy/patch/deepseek/patch-thinking-param.d.ts +6 -0
- package/dist/proxy/patch/deepseek/patch-thinking-param.js +32 -0
- package/dist/proxy/patch/deepseek/utils.d.ts +8 -0
- package/dist/proxy/patch/deepseek/utils.js +38 -0
- package/dist/proxy/patch/index.d.ts +2 -2
- package/dist/proxy/patch/index.js +50 -4
- package/dist/proxy/patch/router-cleanup.js +1 -24
- package/dist/proxy/patch/safe-sse-parser.d.ts +9 -0
- package/dist/proxy/patch/safe-sse-parser.js +16 -0
- package/dist/proxy/patch/tool-round-limiter.d.ts +38 -0
- package/dist/proxy/patch/tool-round-limiter.js +115 -0
- package/dist/proxy/pipeline-snapshot.d.ts +4 -0
- package/dist/proxy/proxy-core.js +1 -0
- package/dist/proxy/proxy-logging.d.ts +1 -1
- package/dist/proxy/proxy-logging.js +3 -3
- package/dist/proxy/routing/enhancement-config.d.ts +1 -0
- package/dist/proxy/routing/enhancement-config.js +2 -0
- package/dist/proxy/transform/id-utils.d.ts +3 -0
- package/dist/proxy/transform/id-utils.js +9 -0
- package/dist/proxy/transform/message-mapper.d.ts +15 -0
- package/dist/proxy/transform/message-mapper.js +173 -0
- package/dist/proxy/transform/plugin-registry.d.ts +23 -0
- package/dist/proxy/transform/plugin-registry.js +130 -0
- package/dist/proxy/transform/plugin-types.d.ts +46 -0
- package/dist/proxy/transform/plugin-types.js +15 -0
- package/dist/proxy/transform/provider-meta.d.ts +29 -0
- package/dist/proxy/transform/provider-meta.js +72 -0
- package/dist/proxy/transform/request-transform.d.ts +4 -0
- package/dist/proxy/transform/request-transform.js +151 -0
- package/dist/proxy/transform/response-transform.d.ts +4 -0
- package/dist/proxy/transform/response-transform.js +99 -0
- package/dist/proxy/transform/sanitize.d.ts +3 -0
- package/dist/proxy/transform/sanitize.js +24 -0
- package/dist/proxy/transform/stream-ant2oa.d.ts +20 -0
- package/dist/proxy/transform/stream-ant2oa.js +200 -0
- package/dist/proxy/transform/stream-oa2ant.d.ts +25 -0
- package/dist/proxy/transform/stream-oa2ant.js +201 -0
- package/dist/proxy/transform/stream-transform-base.d.ts +19 -0
- package/dist/proxy/transform/stream-transform-base.js +61 -0
- package/dist/proxy/transform/thinking-mapper.d.ts +4 -0
- package/dist/proxy/transform/thinking-mapper.js +15 -0
- package/dist/proxy/transform/tool-mapper.d.ts +8 -0
- package/dist/proxy/transform/tool-mapper.js +67 -0
- package/dist/proxy/transform/transform-coordinator.d.ts +11 -0
- package/dist/proxy/transform/transform-coordinator.js +32 -0
- package/dist/proxy/transform/types.d.ts +43 -0
- package/dist/proxy/transform/types.js +1 -0
- package/dist/proxy/transform/usage-mapper.d.ts +8 -0
- package/dist/proxy/transform/usage-mapper.js +46 -0
- package/dist/proxy/transport/stream.d.ts +1 -1
- package/dist/proxy/transport/stream.js +19 -10
- package/dist/proxy/transport/transport-fn.d.ts +3 -0
- package/dist/proxy/transport/transport-fn.js +11 -4
- package/dist/storage/log-file-compressor.js +5 -6
- package/dist/storage/log-file-writer.js +11 -13
- package/dist/storage/types.d.ts +2 -0
- package/dist/storage/types.js +7 -0
- package/frontend-dist/assets/{CardContent-CxOF1feY.js → CardContent-BVMQ2_pg.js} +1 -1
- package/frontend-dist/assets/{CardTitle-BSEFcEOM.js → CardTitle-GLv7QyIY.js} +1 -1
- package/frontend-dist/assets/{CascadingModelSelect-DTwksDPZ.js → CascadingModelSelect-CBhqKFDX.js} +1 -1
- package/frontend-dist/assets/{Checkbox-RfsERG07.js → Checkbox-HPVDmEdV.js} +1 -1
- package/frontend-dist/assets/{CollapsibleTrigger-Dsjo7QlC.js → CollapsibleTrigger-DhxD9tpM.js} +1 -1
- package/frontend-dist/assets/{Collection-rQ4eIYfa.js → Collection-BRt7YxN8.js} +1 -1
- package/frontend-dist/assets/{Dashboard-YejfAPiB.js → Dashboard-D1Ys8Zog.js} +1 -1
- package/frontend-dist/assets/{DialogTitle-DeFTnmgC.js → DialogTitle-23q73lwF.js} +1 -1
- package/frontend-dist/assets/{Input-CENz_g9t.js → Input-CAnKUBBK.js} +1 -1
- package/frontend-dist/assets/{Label-BAciBrrd.js → Label-DWdYtVMI.js} +1 -1
- package/frontend-dist/assets/{Login-DQkYFq7R.js → Login-w5WFOinP.js} +1 -1
- package/frontend-dist/assets/{Logs-Dol8AX7z.js → Logs-C1F1ZmWF.js} +1 -1
- package/frontend-dist/assets/{ModelMappings-VEYW1TrW.js → ModelMappings-BzmecWEH.js} +1 -1
- package/frontend-dist/assets/{Monitor-C0r9WefB.js → Monitor-DrAZFTKR.js} +1 -1
- package/frontend-dist/assets/{PopoverTrigger-Cyqik5SE.js → PopoverTrigger-Bj65uUbv.js} +1 -1
- package/frontend-dist/assets/{PopperContent-B7IuAHeq.js → PopperContent-gzzf1XHe.js} +1 -1
- package/frontend-dist/assets/Providers-DSgf4mb6.js +1 -0
- package/frontend-dist/assets/ProxyEnhancement-Bb1cCP6d.js +5 -0
- package/frontend-dist/assets/{RetryRules-F0295m4_.js → RetryRules-BwPfEZtm.js} +1 -1
- package/frontend-dist/assets/{RouterKeys-CFbPtUE_.js → RouterKeys-CzTSq1Mx.js} +1 -1
- package/frontend-dist/assets/{RovingFocusItem-D291Vjh8.js → RovingFocusItem-CXM_Yfkm.js} +1 -1
- package/frontend-dist/assets/{Schedules-DWhF3uod.js → Schedules-DVilCXrC.js} +1 -1
- package/frontend-dist/assets/{SelectValue-BWlgUZa3.js → SelectValue-C0-LzGQY.js} +1 -1
- package/frontend-dist/assets/{Settings-BnIzEF_k.js → Settings-Bpk53zVX.js} +1 -1
- package/frontend-dist/assets/{Setup-BglKyQKq.js → Setup-Dn7EgC49.js} +1 -1
- package/frontend-dist/assets/{Switch-DyCR-CPu.js → Switch-BO8Ooae6.js} +1 -1
- package/frontend-dist/assets/{TableHeader-DVUlBL35.js → TableHeader-Bded9VTC.js} +1 -1
- package/frontend-dist/assets/{TabsTrigger-BU1DY-C8.js → TabsTrigger-BzKMi9AF.js} +1 -1
- package/frontend-dist/assets/{Teleport-BQgusr9g.js → Teleport-DizRK5O3.js} +1 -1
- package/frontend-dist/assets/{TooltipTrigger-Bv_QoBns.js → TooltipTrigger-EiIy2zn8.js} +1 -1
- package/frontend-dist/assets/{UnifiedRequestDialog-f_evI835.js → UnifiedRequestDialog-BABsTaGb.js} +1 -1
- package/frontend-dist/assets/{VisuallyHidden-Con10z4F.js → VisuallyHidden-5AozJQza.js} +1 -1
- package/frontend-dist/assets/{VisuallyHiddenInput-yrDtxucb.js → VisuallyHiddenInput-DdiZrV2i.js} +1 -1
- package/frontend-dist/assets/{alert-dialog-2Db6Z7JQ.js → alert-dialog-DlKUuTPe.js} +1 -1
- package/frontend-dist/assets/arrow-down-CxWKmZ2I.js +1 -0
- package/frontend-dist/assets/{badge-DEhZfeI0.js → badge-9KJEMa53.js} +1 -1
- package/frontend-dist/assets/button-Ul8WlrM5.js +12 -0
- package/frontend-dist/assets/check-7ahK--N4.js +1 -0
- package/frontend-dist/assets/{copy-CwqZSuIG.js → copy-DzU2pAMG.js} +1 -1
- package/frontend-dist/assets/{dialog-CVMKSdPr.js → dialog-B9j-FMrd.js} +1 -1
- package/frontend-dist/assets/{file-text-D0K8Hovo.js → file-text-Bj3ZIo-E.js} +1 -1
- package/frontend-dist/assets/index-Bz_ZaXNn.css +1 -0
- package/frontend-dist/assets/{index-Ct718O93.js → index-MedWZMHB.js} +1 -1
- package/frontend-dist/assets/{lib-H3YI7EK4.js → lib-Hhs3NqfD.js} +1 -1
- package/frontend-dist/assets/loader-circle-5TJUukEe.js +1 -0
- package/frontend-dist/assets/{useClipboard-Cd7k-5Yq.js → useClipboard-BmmsNSGV.js} +1 -1
- package/frontend-dist/assets/{useFocusGuards-luoLXnwV.js → useFocusGuards-A-9V2Y-b.js} +1 -1
- package/frontend-dist/assets/useFormControl-DEO19lRe.js +1 -0
- package/frontend-dist/assets/{useLogRetention-DB4Iu6o_.js → useLogRetention-BfnBFZ5K.js} +1 -1
- package/frontend-dist/assets/useNonce-BfwUJ1Ci.js +1 -0
- package/frontend-dist/assets/x-Cfopt3QL.js +1 -0
- package/frontend-dist/index.html +20 -20
- package/package.json +1 -1
- package/frontend-dist/assets/Providers-D8Z97edN.js +0 -1
- package/frontend-dist/assets/ProxyEnhancement-Kn8r2SN6.js +0 -5
- package/frontend-dist/assets/arrow-down-WyouvE7T.js +0 -1
- package/frontend-dist/assets/button-Cnkbp_6J.js +0 -12
- package/frontend-dist/assets/check-BuqB5Nyb.js +0 -1
- package/frontend-dist/assets/index-xjdbFKXJ.css +0 -1
- package/frontend-dist/assets/loader-circle-Be82FnVY.js +0 -1
- package/frontend-dist/assets/useFormControl-Da4ViGZF.js +0 -1
- package/frontend-dist/assets/useNonce-DvAdQ48J.js +0 -1
- package/frontend-dist/assets/x-DB22csQl.js +0 -1
|
@@ -16,6 +16,7 @@ import { applyOverflowRedirect } from "../routing/overflow.js";
|
|
|
16
16
|
import { applyProviderPatches } from "../patch/index.js";
|
|
17
17
|
import { PipelineSnapshot } from "../pipeline-snapshot.js";
|
|
18
18
|
import { maybeInjectModelInfoTag } from "../response-transform.js";
|
|
19
|
+
import { applyToolRoundLimit } from "../patch/tool-round-limiter.js";
|
|
19
20
|
import { loadEnhancementConfig } from "../routing/enhancement-config.js";
|
|
20
21
|
import { getTransportStatusCode, serializeBlocksForStorage, extractLastToolUse } from "./proxy-handler-utils.js";
|
|
21
22
|
const HTTP_ERROR_THRESHOLD = 400;
|
|
@@ -37,6 +38,7 @@ function rejectAndReply(reply, params, error, errorMessage, providerId) {
|
|
|
37
38
|
}
|
|
38
39
|
import { getConfig } from "../../config/index.js";
|
|
39
40
|
import { SERVICE_KEYS } from "../../core/container.js";
|
|
41
|
+
import { TransformCoordinator } from "../transform/transform-coordinator.js";
|
|
40
42
|
// ---------- Main entry ----------
|
|
41
43
|
export async function handleProxyRequest(request, reply, apiType, upstreamPath, errors, deps, options) {
|
|
42
44
|
const socketErrorHandler = (err) => request.log.debug({ err }, "client socket error");
|
|
@@ -57,13 +59,22 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
|
|
|
57
59
|
// enhancement 阶段
|
|
58
60
|
const { body: enhancedBody, effectiveModel, originalModel, interceptResponse, meta: enhMeta } = applyEnhancement(deps.db, request.body, clientModel, sessionId, request.routerKey);
|
|
59
61
|
snapshot.add({ stage: "enhancement", router_tags_stripped: enhMeta.router_tags_stripped, directive: enhMeta.directive });
|
|
60
|
-
// tool
|
|
62
|
+
// tool round limiter 阶段 — 检测连续工具调用轮数,超阈值时注入提示词
|
|
61
63
|
let pipelineBody = enhancedBody;
|
|
64
|
+
if (enhancementConfig.tool_round_limit_enabled) {
|
|
65
|
+
const roundResult = applyToolRoundLimit(enhancedBody, apiType);
|
|
66
|
+
if (roundResult.injected) {
|
|
67
|
+
pipelineBody = roundResult.body;
|
|
68
|
+
snapshot.add({ stage: "tool_round_limit", action: "inject_warning", rounds: roundResult.rounds });
|
|
69
|
+
request.log.info({ sessionId, rounds: roundResult.rounds }, "Tool round limit reached, injecting warning prompt");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// tool guard 阶段 — 使用 pipelineBody(可能已被 round limiter 修改)
|
|
62
73
|
const sessionTracker = deps.container.resolve(SERVICE_KEYS.sessionTracker);
|
|
63
74
|
if (enhancementConfig.tool_call_loop_enabled && sessionTracker && sessionId) {
|
|
64
75
|
const routerKeyId = request.routerKey?.id ?? null;
|
|
65
76
|
const sessionKey = routerKeyId ? `${routerKeyId}:${sessionId}` : sessionId;
|
|
66
|
-
const lastToolUse = extractLastToolUse(
|
|
77
|
+
const lastToolUse = extractLastToolUse(pipelineBody);
|
|
67
78
|
if (lastToolUse) {
|
|
68
79
|
const toolGuard = new ToolLoopGuard(sessionTracker, {
|
|
69
80
|
enabled: true,
|
|
@@ -75,7 +86,7 @@ export async function handleProxyRequest(request, reply, apiType, upstreamPath,
|
|
|
75
86
|
const loopCount = sessionTracker.getLoopCount(sessionKey);
|
|
76
87
|
if (loopCount === 1) {
|
|
77
88
|
// 层级 1:透明重试 — 注入中断提示词
|
|
78
|
-
pipelineBody = toolGuard.injectLoopBreakPrompt(
|
|
89
|
+
pipelineBody = toolGuard.injectLoopBreakPrompt(pipelineBody, apiType, lastToolUse.toolName);
|
|
79
90
|
snapshot.add({ stage: "tool_guard", action: "inject_break_prompt", tool: lastToolUse.toolName });
|
|
80
91
|
request.log.warn({ sessionId, toolName: lastToolUse.toolName, loopCount }, "Tool call loop detected, injecting break prompt");
|
|
81
92
|
}
|
|
@@ -119,6 +130,8 @@ async function executeFailoverLoop(ctx) {
|
|
|
119
130
|
const config = getConfig();
|
|
120
131
|
const excludeTargets = [];
|
|
121
132
|
let rootLogId = null;
|
|
133
|
+
// TransformCoordinator 无状态,只需创建一次
|
|
134
|
+
const coordinator = new TransformCoordinator();
|
|
122
135
|
while (true) {
|
|
123
136
|
const startTime = Date.now();
|
|
124
137
|
const logId = randomUUID();
|
|
@@ -169,25 +182,46 @@ async function executeFailoverLoop(ctx) {
|
|
|
169
182
|
if (!provider || !provider.is_active) {
|
|
170
183
|
return rejectAndReply(reply, rCtx, errors.providerUnavailable(), `Provider '${resolved.provider_id}' unavailable`, resolved.provider_id);
|
|
171
184
|
}
|
|
172
|
-
|
|
173
|
-
return rejectAndReply(reply, rCtx, errors.providerTypeMismatch(), `API type mismatch: expected '${apiType}'`, resolved.provider_id);
|
|
174
|
-
}
|
|
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" });
|
|
178
|
-
// --- 溢出重定向:上下文超出时切换到更大模型 ---
|
|
185
|
+
// --- 溢出重定向:上下文超出时切换到更大模型(必须在 transform 之前,确保使用正确的 api_type) ---
|
|
179
186
|
const overflowResult = applyOverflowRedirect(resolved, deps.db, currentBody);
|
|
180
187
|
if (overflowResult) {
|
|
181
188
|
const overflowProvider = getProviderById(deps.db, overflowResult.provider_id);
|
|
182
|
-
if (overflowProvider && overflowProvider.is_active
|
|
189
|
+
if (overflowProvider && overflowProvider.is_active) {
|
|
183
190
|
resolved = { ...resolved, provider_id: overflowResult.provider_id, backend_model: overflowResult.backend_model };
|
|
184
191
|
provider = overflowProvider;
|
|
185
192
|
currentBody = { ...currentBody, model: overflowResult.backend_model };
|
|
186
|
-
iterationSnapshot.add({ stage: "overflow", triggered: true, redirect_to: overflowResult.backend_model, redirect_provider: overflowResult.provider_id });
|
|
187
193
|
}
|
|
188
194
|
}
|
|
189
|
-
|
|
190
|
-
|
|
195
|
+
// 格式转换:apiType 不匹配时转换请求体和路径
|
|
196
|
+
const needsTransform = coordinator.needsTransform(apiType, provider.api_type);
|
|
197
|
+
let effectiveApiType = apiType;
|
|
198
|
+
let effectiveUpstreamPath = upstreamPath;
|
|
199
|
+
if (needsTransform) {
|
|
200
|
+
const transformed = coordinator.transformRequest(currentBody, apiType, provider.api_type, resolved.backend_model);
|
|
201
|
+
// 用转换后的结果替换 currentBody
|
|
202
|
+
currentBody = transformed.body;
|
|
203
|
+
effectiveUpstreamPath = transformed.upstreamPath;
|
|
204
|
+
effectiveApiType = provider.api_type;
|
|
205
|
+
}
|
|
206
|
+
// routing — 创建新对象而非 in-place mutation
|
|
207
|
+
currentBody = { ...currentBody, model: resolved.backend_model };
|
|
208
|
+
iterationSnapshot.add({ stage: "routing", client_model: effectiveModel, backend_model: resolved.backend_model, provider_id: resolved.provider_id, strategy: resolveResult.targetCount > 1 ? "failover" : "scheduled" });
|
|
209
|
+
// overflow redirect 已在 transform 之前完成,此处不再重复
|
|
210
|
+
iterationSnapshot.add({ stage: "overflow", triggered: overflowResult != null });
|
|
211
|
+
// Plugin 调整 body 和 headers(不受 needsTransform 限制,inject_headers 等同格式也需要)
|
|
212
|
+
let injectedHeaders = {};
|
|
213
|
+
const pluginRegistry = deps.container.resolve(SERVICE_KEYS.pluginRegistry);
|
|
214
|
+
if (pluginRegistry) {
|
|
215
|
+
const pluginCtx = {
|
|
216
|
+
body: currentBody,
|
|
217
|
+
headers: {},
|
|
218
|
+
sourceApiType: apiType,
|
|
219
|
+
targetApiType: provider.api_type,
|
|
220
|
+
provider: { id: provider.id, name: provider.name, base_url: provider.base_url, api_type: provider.api_type },
|
|
221
|
+
};
|
|
222
|
+
pluginRegistry.applyBeforeRequest(pluginCtx);
|
|
223
|
+
pluginRegistry.applyAfterRequest(pluginCtx);
|
|
224
|
+
injectedHeaders = pluginCtx.headers;
|
|
191
225
|
}
|
|
192
226
|
// provider patches — 使用返回值
|
|
193
227
|
const { body: patchedBody, meta: patchMeta } = applyProviderPatches(currentBody, provider);
|
|
@@ -202,15 +236,48 @@ async function executeFailoverLoop(ctx) {
|
|
|
202
236
|
const reqBodyStr = JSON.stringify(patchedBody);
|
|
203
237
|
const clientReq = JSON.stringify({ headers: cliHdrs, body: rawBody });
|
|
204
238
|
const upstreamReqBase = JSON.stringify({
|
|
205
|
-
url: buildUpstreamUrl(provider.base_url,
|
|
206
|
-
headers: sanitizeHeadersForLog(buildUpstreamHeaders(cliHdrs, apiKey, Buffer.byteLength(reqBodyStr),
|
|
239
|
+
url: buildUpstreamUrl(provider.base_url, effectiveUpstreamPath),
|
|
240
|
+
headers: sanitizeHeadersForLog(buildUpstreamHeaders(cliHdrs, apiKey, Buffer.byteLength(reqBodyStr), effectiveApiType)),
|
|
207
241
|
body: reqBodyStr,
|
|
208
242
|
});
|
|
243
|
+
const formatTransform = needsTransform ? coordinator.createFormatTransform(apiType, provider.api_type, resolved.backend_model) : undefined;
|
|
244
|
+
if (formatTransform) {
|
|
245
|
+
formatTransform.on("warning", (err) => request.log.warn({ err, logId }, "formatTransform warning"));
|
|
246
|
+
}
|
|
247
|
+
const responseTransform = needsTransform ? (bodyStr) => {
|
|
248
|
+
try {
|
|
249
|
+
const parsed = JSON.parse(bodyStr);
|
|
250
|
+
if (parsed.type === "error" || parsed.error) {
|
|
251
|
+
return coordinator.transformErrorResponse(bodyStr, provider.api_type, apiType);
|
|
252
|
+
}
|
|
253
|
+
let transformed = coordinator.transformResponse(bodyStr, provider.api_type, apiType);
|
|
254
|
+
if (pluginRegistry && !isStream) {
|
|
255
|
+
try {
|
|
256
|
+
const respObj = JSON.parse(transformed);
|
|
257
|
+
const respCtx = {
|
|
258
|
+
response: respObj,
|
|
259
|
+
sourceApiType: provider.api_type,
|
|
260
|
+
targetApiType: apiType,
|
|
261
|
+
provider: { id: provider.id, name: provider.name, base_url: provider.base_url, api_type: provider.api_type },
|
|
262
|
+
};
|
|
263
|
+
pluginRegistry.applyBeforeResponse(respCtx);
|
|
264
|
+
pluginRegistry.applyAfterResponse(respCtx);
|
|
265
|
+
transformed = JSON.stringify(respCtx.response);
|
|
266
|
+
}
|
|
267
|
+
catch { /* response hooks best-effort */ }
|
|
268
|
+
}
|
|
269
|
+
return transformed;
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
request.log.error({ err }, "responseTransform failed");
|
|
273
|
+
return bodyStr;
|
|
274
|
+
}
|
|
275
|
+
} : undefined;
|
|
209
276
|
const transportFn = buildTransportFn({
|
|
210
|
-
provider, apiKey, body: patchedBody, cliHdrs, reply, upstreamPath, apiType,
|
|
277
|
+
provider, apiKey, body: patchedBody, cliHdrs, reply, upstreamPath: effectiveUpstreamPath, apiType: effectiveApiType,
|
|
211
278
|
isStream, startTime, logId, effectiveModel, originalModel,
|
|
212
279
|
streamTimeoutMs: config.STREAM_TIMEOUT_MS, tracker, matcher, request,
|
|
213
|
-
streamLoopEnabled,
|
|
280
|
+
streamLoopEnabled, formatTransform, responseTransform, injectedHeaders,
|
|
214
281
|
});
|
|
215
282
|
const pipelineSnapshot = iterationSnapshot.toJSON();
|
|
216
283
|
try {
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 按序执行所有 DeepSeek 特定补丁。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Patch 在格式转换之后执行,body 已经是 provider 的 api_type 格式。
|
|
5
|
+
* DeepSeek 的 api_type 为 openai,但 Anthropic 端点也受支持,
|
|
6
|
+
* 因此按 apiType 分发不同的 patch 流程。
|
|
7
|
+
*
|
|
8
|
+
* Anthropic 格式执行顺序:
|
|
9
|
+
* 1. patchThinkingParam — 注入 thinking 参数
|
|
10
|
+
* 2. stripCacheControl — 剥离 cache_control
|
|
11
|
+
* 3. patchMissingThinkingBlocks — 补 thinking block
|
|
12
|
+
* 4. patchOrphanToolResults — 清理孤儿 tool_result
|
|
13
|
+
*
|
|
14
|
+
* OpenAI 格式执行顺序(参考 docs/deepseek-patch-investigation.md §5.5):
|
|
15
|
+
* 1. patchNonDeepSeekToolMessages — 将非 DeepSeek 生成的 tool_calls 降级为 text
|
|
16
|
+
* 2. patchOrphanToolResultsOA — 处理孤儿 tool 消息
|
|
5
17
|
*/
|
|
6
|
-
export declare function applyDeepSeekPatches(body: Record<string, unknown
|
|
18
|
+
export declare function applyDeepSeekPatches(body: Record<string, unknown>, apiType: "openai" | "anthropic"): void;
|
|
@@ -1,11 +1,34 @@
|
|
|
1
|
+
import { patchThinkingParam } from "./patch-thinking-param.js";
|
|
2
|
+
import { stripCacheControl } from "./patch-cache-control.js";
|
|
1
3
|
import { patchMissingThinkingBlocks } from "./patch-thinking-blocks.js";
|
|
2
|
-
import {
|
|
4
|
+
import { patchNonDeepSeekToolMessages } from "./patch-non-deepseek-tools.js";
|
|
5
|
+
import { patchOrphanToolResults, patchOrphanToolResultsOA } from "./patch-orphan-tool-results.js";
|
|
3
6
|
/**
|
|
4
7
|
* 按序执行所有 DeepSeek 特定补丁。
|
|
5
|
-
*
|
|
6
|
-
*
|
|
8
|
+
*
|
|
9
|
+
* Patch 在格式转换之后执行,body 已经是 provider 的 api_type 格式。
|
|
10
|
+
* DeepSeek 的 api_type 为 openai,但 Anthropic 端点也受支持,
|
|
11
|
+
* 因此按 apiType 分发不同的 patch 流程。
|
|
12
|
+
*
|
|
13
|
+
* Anthropic 格式执行顺序:
|
|
14
|
+
* 1. patchThinkingParam — 注入 thinking 参数
|
|
15
|
+
* 2. stripCacheControl — 剥离 cache_control
|
|
16
|
+
* 3. patchMissingThinkingBlocks — 补 thinking block
|
|
17
|
+
* 4. patchOrphanToolResults — 清理孤儿 tool_result
|
|
18
|
+
*
|
|
19
|
+
* OpenAI 格式执行顺序(参考 docs/deepseek-patch-investigation.md §5.5):
|
|
20
|
+
* 1. patchNonDeepSeekToolMessages — 将非 DeepSeek 生成的 tool_calls 降级为 text
|
|
21
|
+
* 2. patchOrphanToolResultsOA — 处理孤儿 tool 消息
|
|
7
22
|
*/
|
|
8
|
-
export function applyDeepSeekPatches(body) {
|
|
9
|
-
|
|
10
|
-
|
|
23
|
+
export function applyDeepSeekPatches(body, apiType) {
|
|
24
|
+
if (apiType === "anthropic") {
|
|
25
|
+
patchThinkingParam(body, apiType);
|
|
26
|
+
stripCacheControl(body);
|
|
27
|
+
patchMissingThinkingBlocks(body);
|
|
28
|
+
patchOrphanToolResults(body);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
patchNonDeepSeekToolMessages(body);
|
|
32
|
+
patchOrphanToolResultsOA(body);
|
|
33
|
+
}
|
|
11
34
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek 的 Anthropic 兼容 API 不支持 cache_control。
|
|
3
|
+
* Claude Code 等客户端会在 content block 和 system prompt 上标注
|
|
4
|
+
* cache_control: { type: "ephemeral" },需要剥离以避免上游报错。
|
|
5
|
+
*/
|
|
6
|
+
export function stripCacheControl(body) {
|
|
7
|
+
// 处理顶级 system 字段(Anthropic 协议中 system 可以是 content block 数组)
|
|
8
|
+
if (Array.isArray(body.system)) {
|
|
9
|
+
for (const block of body.system) {
|
|
10
|
+
delete block.cache_control;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
// 处理 messages 中的 content block
|
|
14
|
+
if (!body.messages)
|
|
15
|
+
return;
|
|
16
|
+
const messages = body.messages;
|
|
17
|
+
for (const msg of messages) {
|
|
18
|
+
if (Array.isArray(msg.content)) {
|
|
19
|
+
for (const block of msg.content) {
|
|
20
|
+
delete block.cache_control;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// 处理 tools 上的 cache_control
|
|
25
|
+
if (Array.isArray(body.tools)) {
|
|
26
|
+
for (const tool of body.tools) {
|
|
27
|
+
delete tool.cache_control;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 方案 7(OpenAI 格式):将非 DeepSeek 生成的 tool 消息降级为 text。
|
|
3
|
+
*
|
|
4
|
+
* 当 agent 从其他模型切换到 DeepSeek 时,历史中的 tool_calls 消息
|
|
5
|
+
* 可能不包含 DeepSeek 要求的 reasoning_content,导致上游校验失败或
|
|
6
|
+
* 工具调用无限循环。
|
|
7
|
+
*
|
|
8
|
+
* 判断标准:assistant 消息有 tool_calls 但无 reasoning_content → 非 DeepSeek 生成。
|
|
9
|
+
*
|
|
10
|
+
* 转换:
|
|
11
|
+
* - assistant.tool_calls → JSON 序列化到 content,删除 tool_calls
|
|
12
|
+
* - role:"tool" → role:"user",内容 JSON 序列化,删除 tool_call_id
|
|
13
|
+
*
|
|
14
|
+
* 设计文档:docs/deepseek-patch-investigation.md §5
|
|
15
|
+
*/
|
|
16
|
+
export declare function patchNonDeepSeekToolMessages(body: Record<string, unknown>): void;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 方案 7(OpenAI 格式):将非 DeepSeek 生成的 tool 消息降级为 text。
|
|
3
|
+
*
|
|
4
|
+
* 当 agent 从其他模型切换到 DeepSeek 时,历史中的 tool_calls 消息
|
|
5
|
+
* 可能不包含 DeepSeek 要求的 reasoning_content,导致上游校验失败或
|
|
6
|
+
* 工具调用无限循环。
|
|
7
|
+
*
|
|
8
|
+
* 判断标准:assistant 消息有 tool_calls 但无 reasoning_content → 非 DeepSeek 生成。
|
|
9
|
+
*
|
|
10
|
+
* 转换:
|
|
11
|
+
* - assistant.tool_calls → JSON 序列化到 content,删除 tool_calls
|
|
12
|
+
* - role:"tool" → role:"user",内容 JSON 序列化,删除 tool_call_id
|
|
13
|
+
*
|
|
14
|
+
* 设计文档:docs/deepseek-patch-investigation.md §5
|
|
15
|
+
*/
|
|
16
|
+
export function patchNonDeepSeekToolMessages(body) {
|
|
17
|
+
const messages = body.messages;
|
|
18
|
+
if (!messages || !Array.isArray(messages))
|
|
19
|
+
return;
|
|
20
|
+
// Step 1: 收集需要降级的 tool_call IDs
|
|
21
|
+
const downgradeIds = new Set();
|
|
22
|
+
for (const msg of messages) {
|
|
23
|
+
if (msg.role !== "assistant")
|
|
24
|
+
continue;
|
|
25
|
+
const toolCalls = msg.tool_calls;
|
|
26
|
+
if (!toolCalls || toolCalls.length === 0)
|
|
27
|
+
continue;
|
|
28
|
+
// 有 tool_calls 但无 reasoning_content → 非 DeepSeek 生成
|
|
29
|
+
if (!msg.reasoning_content) {
|
|
30
|
+
for (const tc of toolCalls) {
|
|
31
|
+
if (typeof tc.id === "string")
|
|
32
|
+
downgradeIds.add(tc.id);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (downgradeIds.size === 0)
|
|
37
|
+
return;
|
|
38
|
+
// Step 2: 降级 assistant 消息 — tool_calls → text content
|
|
39
|
+
for (const msg of messages) {
|
|
40
|
+
if (msg.role !== "assistant")
|
|
41
|
+
continue;
|
|
42
|
+
const toolCalls = msg.tool_calls;
|
|
43
|
+
if (!toolCalls || toolCalls.length === 0)
|
|
44
|
+
continue;
|
|
45
|
+
if (msg.reasoning_content)
|
|
46
|
+
continue;
|
|
47
|
+
const serialized = JSON.stringify(toolCalls.map(tc => ({
|
|
48
|
+
id: tc.id,
|
|
49
|
+
type: "function",
|
|
50
|
+
function: {
|
|
51
|
+
name: tc.function?.name,
|
|
52
|
+
arguments: tc.function?.arguments,
|
|
53
|
+
},
|
|
54
|
+
})));
|
|
55
|
+
const existing = typeof msg.content === "string" ? msg.content : "";
|
|
56
|
+
msg.content = existing ? `${existing}\n[tool_calls]: ${serialized}` : `[tool_calls]: ${serialized}`;
|
|
57
|
+
delete msg.tool_calls;
|
|
58
|
+
}
|
|
59
|
+
// Step 3: 降级对应的 tool 消息 — role:"tool" → role:"user"
|
|
60
|
+
for (const msg of messages) {
|
|
61
|
+
if (msg.role !== "tool")
|
|
62
|
+
continue;
|
|
63
|
+
const toolCallId = String(msg.tool_call_id ?? "");
|
|
64
|
+
if (!downgradeIds.has(toolCallId))
|
|
65
|
+
continue;
|
|
66
|
+
msg.role = "user";
|
|
67
|
+
msg.content = JSON.stringify({
|
|
68
|
+
type: "tool_result",
|
|
69
|
+
tool_use_id: toolCallId,
|
|
70
|
+
content: msg.content,
|
|
71
|
+
});
|
|
72
|
+
delete msg.tool_call_id;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
* 2. 移除 tool_use_id 不在集合中的 tool_result 块
|
|
8
8
|
* 3. 移除清空后的空 user 消息
|
|
9
9
|
* 4. 合并相邻的 user 消息(Anthropic API 不允许连续 user 消息)
|
|
10
|
-
* 5. 合并相邻的 assistant
|
|
10
|
+
* 5. 合并相邻的 assistant 消息(带 tool_use 去重)
|
|
11
|
+
* 6. 移除 content 为空数组的 assistant 消息
|
|
12
|
+
* 7. 最终合并连续同角色消息
|
|
11
13
|
*/
|
|
12
14
|
export declare function patchOrphanToolResults(body: Record<string, unknown>): void;
|
|
15
|
+
/**
|
|
16
|
+
* OpenAI 格式版本的孤儿 tool 消息清理。
|
|
17
|
+
*
|
|
18
|
+
* 检测 role:"tool" 消息的 tool_call_id 是否有对应的 assistant tool_calls[].id。
|
|
19
|
+
* 移除孤儿 tool 消息后合并连续 user 消息。
|
|
20
|
+
*/
|
|
21
|
+
export declare function patchOrphanToolResultsOA(body: Record<string, unknown>): void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mergeConsecutive, mergeAssistantContent } from "./utils.js";
|
|
1
2
|
/**
|
|
2
3
|
* 修复孤儿 tool_result 块——Claude Code 的 context management 截断历史消息时
|
|
3
4
|
* 可能丢失 tool_use 块但保留对应的 tool_result,导致 DeepSeek 严格校验失败。
|
|
@@ -7,7 +8,9 @@
|
|
|
7
8
|
* 2. 移除 tool_use_id 不在集合中的 tool_result 块
|
|
8
9
|
* 3. 移除清空后的空 user 消息
|
|
9
10
|
* 4. 合并相邻的 user 消息(Anthropic API 不允许连续 user 消息)
|
|
10
|
-
* 5. 合并相邻的 assistant
|
|
11
|
+
* 5. 合并相邻的 assistant 消息(带 tool_use 去重)
|
|
12
|
+
* 6. 移除 content 为空数组的 assistant 消息
|
|
13
|
+
* 7. 最终合并连续同角色消息
|
|
11
14
|
*/
|
|
12
15
|
export function patchOrphanToolResults(body) {
|
|
13
16
|
if (!body.messages)
|
|
@@ -60,18 +63,65 @@ export function patchOrphanToolResults(body) {
|
|
|
60
63
|
}
|
|
61
64
|
// Step 4: 合并相邻的 user 消息
|
|
62
65
|
mergeConsecutive(messages, "user");
|
|
63
|
-
// Step 5: 合并相邻的 assistant
|
|
64
|
-
mergeConsecutive(messages, "assistant");
|
|
66
|
+
// Step 5: 合并相邻的 assistant 消息(带 tool_use 去重)
|
|
67
|
+
mergeConsecutive(messages, "assistant", mergeAssistantContent);
|
|
68
|
+
// Step 6: 移除 content 为空数组的 assistant 消息
|
|
69
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
70
|
+
const msg = messages[i];
|
|
71
|
+
if (msg.role === "assistant" && Array.isArray(msg.content) && msg.content.length === 0) {
|
|
72
|
+
messages.splice(i, 1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Step 7: 删除空 assistant 后可能产生连续同角色消息,再合并一次
|
|
76
|
+
mergeConsecutive(messages, "user");
|
|
77
|
+
mergeConsecutive(messages, "assistant", mergeAssistantContent);
|
|
65
78
|
}
|
|
66
|
-
|
|
79
|
+
/**
|
|
80
|
+
* OpenAI 格式版本的孤儿 tool 消息清理。
|
|
81
|
+
*
|
|
82
|
+
* 检测 role:"tool" 消息的 tool_call_id 是否有对应的 assistant tool_calls[].id。
|
|
83
|
+
* 移除孤儿 tool 消息后合并连续 user 消息。
|
|
84
|
+
*/
|
|
85
|
+
export function patchOrphanToolResultsOA(body) {
|
|
86
|
+
const messages = body.messages;
|
|
87
|
+
if (!messages || !Array.isArray(messages) || messages.length === 0)
|
|
88
|
+
return;
|
|
89
|
+
// Step 1: 收集所有 assistant tool_calls IDs
|
|
90
|
+
const knownToolCallIds = new Set();
|
|
91
|
+
for (const msg of messages) {
|
|
92
|
+
if (msg.role !== "assistant")
|
|
93
|
+
continue;
|
|
94
|
+
const toolCalls = msg.tool_calls;
|
|
95
|
+
if (!toolCalls)
|
|
96
|
+
continue;
|
|
97
|
+
for (const tc of toolCalls) {
|
|
98
|
+
if (typeof tc.id === "string")
|
|
99
|
+
knownToolCallIds.add(tc.id);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Step 2: 移除孤儿 tool 消息(逆序遍历避免索引偏移)
|
|
103
|
+
let removedAny = false;
|
|
104
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
105
|
+
const msg = messages[i];
|
|
106
|
+
if (msg.role !== "tool")
|
|
107
|
+
continue;
|
|
108
|
+
const toolCallId = String(msg.tool_call_id ?? "");
|
|
109
|
+
if (!knownToolCallIds.has(toolCallId)) {
|
|
110
|
+
messages.splice(i, 1);
|
|
111
|
+
removedAny = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!removedAny)
|
|
115
|
+
return;
|
|
116
|
+
// Step 3: 合并连续 user 消息
|
|
67
117
|
let i = 1;
|
|
68
118
|
while (i < messages.length) {
|
|
69
|
-
if (messages[i].role ===
|
|
119
|
+
if (messages[i].role === "user" && messages[i - 1].role === "user") {
|
|
70
120
|
const prev = messages[i - 1];
|
|
71
121
|
const curr = messages[i];
|
|
72
|
-
const prevContent =
|
|
73
|
-
const currContent =
|
|
74
|
-
prev.content =
|
|
122
|
+
const prevContent = typeof prev.content === "string" ? prev.content : JSON.stringify(prev.content ?? "");
|
|
123
|
+
const currContent = typeof curr.content === "string" ? curr.content : JSON.stringify(curr.content ?? "");
|
|
124
|
+
prev.content = prevContent + "\n" + currContent;
|
|
75
125
|
messages.splice(i, 1);
|
|
76
126
|
}
|
|
77
127
|
else {
|
|
@@ -79,10 +129,3 @@ function mergeConsecutive(messages, role) {
|
|
|
79
129
|
}
|
|
80
130
|
}
|
|
81
131
|
}
|
|
82
|
-
function normalizeToArray(content) {
|
|
83
|
-
if (Array.isArray(content))
|
|
84
|
-
return content;
|
|
85
|
-
if (typeof content === "string")
|
|
86
|
-
return [{ type: "text", text: content }];
|
|
87
|
-
return [{ type: "text", text: String(content ?? "") }];
|
|
88
|
-
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DeepSeek thinking 协议实现不完整:开启 thinking 模式后部分轮次不返回 thinking block,
|
|
3
3
|
* 但后续请求要求历史 assistant 消息必须携带 thinking block。
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* 处理:
|
|
6
|
+
* 1. 检测历史 thinking block 是否带 signature 字段,保持格式一致
|
|
7
|
+
* 2. 对缺少 thinking block 的 assistant 消息,在 content 数组开头补一个空 thinking block
|
|
8
|
+
* 3. 对 thinking block 不在首位的 assistant 消息,修正位置
|
|
5
9
|
*/
|
|
6
10
|
export declare function patchMissingThinkingBlocks(body: Record<string, unknown>): void;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DeepSeek thinking 协议实现不完整:开启 thinking 模式后部分轮次不返回 thinking block,
|
|
3
3
|
* 但后续请求要求历史 assistant 消息必须携带 thinking block。
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* 处理:
|
|
6
|
+
* 1. 检测历史 thinking block 是否带 signature 字段,保持格式一致
|
|
7
|
+
* 2. 对缺少 thinking block 的 assistant 消息,在 content 数组开头补一个空 thinking block
|
|
8
|
+
* 3. 对 thinking block 不在首位的 assistant 消息,修正位置
|
|
5
9
|
*/
|
|
6
10
|
export function patchMissingThinkingBlocks(body) {
|
|
7
11
|
if (!body.messages)
|
|
@@ -13,12 +17,41 @@ export function patchMissingThinkingBlocks(body) {
|
|
|
13
17
|
&& msg.content.some((b) => b && typeof b === "object" && b.type === "thinking"));
|
|
14
18
|
if (!thinkingActive)
|
|
15
19
|
return;
|
|
20
|
+
// 检测历史中 thinking block 是否带 signature 字段
|
|
21
|
+
const needsSignature = detectSignatureUsage(messages);
|
|
16
22
|
for (const msg of messages) {
|
|
17
23
|
if (msg.role !== "assistant" || !Array.isArray(msg.content))
|
|
18
24
|
continue;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
const blocks = msg.content;
|
|
26
|
+
const thinkingIdx = blocks.findIndex((b) => b && typeof b === "object" && b.type === "thinking");
|
|
27
|
+
if (thinkingIdx === -1) {
|
|
28
|
+
// 不存在 thinking block → 补一个
|
|
29
|
+
const emptyThinking = { type: "thinking", thinking: "" };
|
|
30
|
+
if (needsSignature)
|
|
31
|
+
emptyThinking.signature = "";
|
|
32
|
+
blocks.unshift(emptyThinking);
|
|
33
|
+
}
|
|
34
|
+
else if (thinkingIdx > 0) {
|
|
35
|
+
// thinking block 不在第一位 → 移到首位
|
|
36
|
+
const [thinkingBlock] = blocks.splice(thinkingIdx, 1);
|
|
37
|
+
blocks.unshift(thinkingBlock);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 扫描历史 assistant 消息中的 thinking block,
|
|
43
|
+
* 判断是否需要 signature 字段。
|
|
44
|
+
*/
|
|
45
|
+
function detectSignatureUsage(messages) {
|
|
46
|
+
for (const msg of messages) {
|
|
47
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.content))
|
|
48
|
+
continue;
|
|
49
|
+
for (const b of msg.content) {
|
|
50
|
+
if (b && typeof b === "object" && b.type === "thinking") {
|
|
51
|
+
return "signature" in b;
|
|
52
|
+
}
|
|
22
53
|
}
|
|
23
54
|
}
|
|
55
|
+
// 无历史 thinking block 时,默认带 signature(保持向后兼容)
|
|
56
|
+
return true;
|
|
24
57
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek 开启 thinking 后,后续请求必须显式传 thinking 参数。
|
|
3
|
+
* 客户端(如 Claude Code)可能在后续轮次省略此参数。
|
|
4
|
+
* 检测历史中是否存在 thinking 内容,自动补上参数。
|
|
5
|
+
*/
|
|
6
|
+
export function patchThinkingParam(body, apiType) {
|
|
7
|
+
if (body.thinking)
|
|
8
|
+
return;
|
|
9
|
+
const messages = body.messages;
|
|
10
|
+
if (!messages)
|
|
11
|
+
return;
|
|
12
|
+
const hasThinking = messages.some(msg => {
|
|
13
|
+
if (msg.role !== "assistant")
|
|
14
|
+
return false;
|
|
15
|
+
if (apiType === "openai") {
|
|
16
|
+
return msg.reasoning_content !== undefined;
|
|
17
|
+
}
|
|
18
|
+
// Anthropic 格式
|
|
19
|
+
return Array.isArray(msg.content) &&
|
|
20
|
+
msg.content
|
|
21
|
+
.some(b => b?.type === "thinking");
|
|
22
|
+
});
|
|
23
|
+
if (!hasThinking)
|
|
24
|
+
return;
|
|
25
|
+
if (apiType === "openai") {
|
|
26
|
+
body.thinking = { type: "enabled" };
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
// Anthropic 格式要求 budget_tokens
|
|
30
|
+
body.thinking = { type: "enabled", budget_tokens: 10000 };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type ContentBlock = Record<string, unknown>;
|
|
2
|
+
export type Message = {
|
|
3
|
+
role: string;
|
|
4
|
+
content: unknown;
|
|
5
|
+
};
|
|
6
|
+
export declare function normalizeToArray(content: unknown): ContentBlock[];
|
|
7
|
+
export declare function mergeConsecutive(messages: Message[], role: string, mergeAssistant?: (prev: ContentBlock[], curr: ContentBlock[]) => ContentBlock[]): void;
|
|
8
|
+
export declare function mergeAssistantContent(prev: ContentBlock[], curr: ContentBlock[]): ContentBlock[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function normalizeToArray(content) {
|
|
2
|
+
if (Array.isArray(content))
|
|
3
|
+
return content;
|
|
4
|
+
if (typeof content === "string")
|
|
5
|
+
return [{ type: "text", text: content }];
|
|
6
|
+
return [{ type: "text", text: String(content ?? "") }];
|
|
7
|
+
}
|
|
8
|
+
export function mergeConsecutive(messages, role, mergeAssistant) {
|
|
9
|
+
let i = 1;
|
|
10
|
+
while (i < messages.length) {
|
|
11
|
+
if (messages[i].role === role && messages[i - 1].role === role) {
|
|
12
|
+
const prev = messages[i - 1];
|
|
13
|
+
const curr = messages[i];
|
|
14
|
+
const prevContent = normalizeToArray(prev.content);
|
|
15
|
+
const currContent = normalizeToArray(curr.content);
|
|
16
|
+
if (role === "assistant" && mergeAssistant) {
|
|
17
|
+
prev.content = mergeAssistant(prevContent, currContent);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
prev.content = [...prevContent, ...currContent];
|
|
21
|
+
}
|
|
22
|
+
messages.splice(i, 1);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
i++;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function mergeAssistantContent(prev, curr) {
|
|
30
|
+
const seenToolIds = new Set();
|
|
31
|
+
for (const b of prev) {
|
|
32
|
+
if (b?.type === "tool_use" && typeof b.id === "string") {
|
|
33
|
+
seenToolIds.add(b.id);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const deduped = curr.filter(b => !(b?.type === "tool_use" && typeof b.id === "string" && seenToolIds.has(b.id)));
|
|
37
|
+
return [...prev, ...deduped];
|
|
38
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
interface ProviderInfo {
|
|
1
|
+
export interface ProviderInfo {
|
|
2
2
|
base_url: string;
|
|
3
|
+
api_type: string;
|
|
3
4
|
}
|
|
4
5
|
export interface ProviderPatchMeta {
|
|
5
6
|
types: string[];
|
|
@@ -12,4 +13,3 @@ export declare function applyProviderPatches(body: Record<string, unknown>, prov
|
|
|
12
13
|
body: Record<string, unknown>;
|
|
13
14
|
meta: ProviderPatchMeta;
|
|
14
15
|
};
|
|
15
|
-
export {};
|