@myassis/gateway 1.0.77 → 1.0.78
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.
|
@@ -12,7 +12,36 @@ const logger = (0, shared_1.getLogger)('LLMClient');
|
|
|
12
12
|
/** 历史工具输出在上下文中的最大保留长度 */
|
|
13
13
|
const HISTORY_TOOL_OUTPUT_LIMIT = index_js_2.appConfig.oldToolContentLimit;
|
|
14
14
|
/** 请求体字符硬上限,超过则视为上下文超限 */
|
|
15
|
-
const HARD_MAX_CHARS = 200000;
|
|
15
|
+
const HARD_MAX_CHARS = 200000;
|
|
16
|
+
/**
|
|
17
|
+
* 规范化工具调用参数。
|
|
18
|
+
*
|
|
19
|
+
* OpenAI 兼容接口要求 arguments 是「合法 JSON 字符串」;传入 null / undefined /
|
|
20
|
+
* 被截断的半截 JSON 都会导致 400 Invalid request body。
|
|
21
|
+
*/
|
|
22
|
+
function normalizeArguments(input) {
|
|
23
|
+
if (input == null)
|
|
24
|
+
return '{}';
|
|
25
|
+
if (typeof input === 'object') {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.stringify(input);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return '{}';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const text = String(input);
|
|
34
|
+
if (!text.trim())
|
|
35
|
+
return '{}';
|
|
36
|
+
try {
|
|
37
|
+
JSON.parse(text);
|
|
38
|
+
return text;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// 非法 JSON(例如历史裁剪产生的半截字符串)降级为带原文的合法对象
|
|
42
|
+
return JSON.stringify({ _raw: text });
|
|
43
|
+
}
|
|
44
|
+
} /**
|
|
16
45
|
* LLM 调用结果类型
|
|
17
46
|
*/
|
|
18
47
|
function convertBase64ToImage(base64Img) {
|
|
@@ -200,7 +229,7 @@ class LLMClient {
|
|
|
200
229
|
return [{
|
|
201
230
|
role: 'tool',
|
|
202
231
|
tool_call_id: m.tool_call_id,
|
|
203
|
-
content: m.content,
|
|
232
|
+
content: m.content || '(无输出)',
|
|
204
233
|
}];
|
|
205
234
|
}
|
|
206
235
|
}
|
|
@@ -213,7 +242,7 @@ class LLMClient {
|
|
|
213
242
|
id: tc.id,
|
|
214
243
|
function: {
|
|
215
244
|
name: tc.toolName,
|
|
216
|
-
arguments: tc.input
|
|
245
|
+
arguments: normalizeArguments(tc.input)
|
|
217
246
|
},
|
|
218
247
|
type: 'function'
|
|
219
248
|
})),
|
|
@@ -233,11 +262,13 @@ class LLMClient {
|
|
|
233
262
|
const toolCallItem = toolCall.toolCalls[i];
|
|
234
263
|
if (!toolCallItem?.id)
|
|
235
264
|
continue;
|
|
265
|
+
if (!toolCallItem.toolName)
|
|
266
|
+
continue;
|
|
236
267
|
item.tool_calls.push({
|
|
237
268
|
id: toolCallItem.id,
|
|
238
269
|
function: {
|
|
239
270
|
name: toolCallItem.toolName,
|
|
240
|
-
arguments: toolCallItem.input
|
|
271
|
+
arguments: normalizeArguments(toolCallItem.input)
|
|
241
272
|
},
|
|
242
273
|
type: 'function'
|
|
243
274
|
});
|
|
@@ -66,11 +66,53 @@ function clip(text, limit) {
|
|
|
66
66
|
return text;
|
|
67
67
|
return text.slice(0, limit) + '...(内容过长已截断)';
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* 截断工具调用参数,同时保证结果仍是合法 JSON。
|
|
71
|
+
*
|
|
72
|
+
* 不能直接对 arguments 字符串做 substring:截断后会变成
|
|
73
|
+
* `{"path":"a.ts","content":"xxx` 这类非法 JSON,严格校验的厂商
|
|
74
|
+
* (如豆包/Volcengine)会直接返回 400 Invalid request body。
|
|
75
|
+
* 因此这里解析后逐个截断字符串字段,再重新序列化。
|
|
76
|
+
*/
|
|
77
|
+
function clipJsonArguments(args, limit) {
|
|
78
|
+
if (typeof args !== 'string')
|
|
79
|
+
return args;
|
|
80
|
+
if (args.length <= limit)
|
|
81
|
+
return args;
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(args);
|
|
84
|
+
const shrink = (value) => {
|
|
85
|
+
if (typeof value === 'string') {
|
|
86
|
+
return value.length > limit ? value.slice(0, limit) + '...(已截断)' : value;
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(value))
|
|
89
|
+
return value.map(shrink);
|
|
90
|
+
if (value && typeof value === 'object') {
|
|
91
|
+
const out = {};
|
|
92
|
+
for (const key of Object.keys(value))
|
|
93
|
+
out[key] = shrink(value[key]);
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
};
|
|
98
|
+
return JSON.stringify(shrink(parsed));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// 无法解析(非 JSON 参数)时保持原样,宁可占用上下文也不发出非法请求
|
|
102
|
+
return args;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
69
105
|
/** 浅拷贝一条消息,保证裁剪不影响原对象 */
|
|
70
106
|
function cloneMessage(message) {
|
|
71
107
|
const copy = { ...message };
|
|
72
108
|
if (Array.isArray(message.tool_calls)) {
|
|
73
|
-
copy.tool_calls = message.tool_calls.map((tc) => ({ ...tc }));
|
|
109
|
+
copy.tool_calls = message.tool_calls.map((tc) => (tc?.function ? { ...tc, function: { ...tc.function } } : { ...tc }));
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(message.toolCalls)) {
|
|
112
|
+
copy.toolCalls = message.toolCalls.map((tc) => ({
|
|
113
|
+
...tc,
|
|
114
|
+
toolCalls: Array.isArray(tc?.toolCalls) ? tc.toolCalls.map((x) => ({ ...x })) : tc?.toolCalls,
|
|
115
|
+
}));
|
|
74
116
|
}
|
|
75
117
|
if (Array.isArray(message.attachments)) {
|
|
76
118
|
copy.attachments = message.attachments.map((att) => ({ ...att }));
|
|
@@ -112,10 +154,9 @@ function clipPair(messages, pair, limit) {
|
|
|
112
154
|
// 工具参数往往是上下文膨胀的主因(写文件 / patch 类调用)
|
|
113
155
|
if (Array.isArray(head.tool_calls)) {
|
|
114
156
|
for (const tc of head.tool_calls) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
tc.function.arguments = clip(tc.function.arguments, limit);
|
|
157
|
+
tc.input = clipJsonArguments(tc.input, limit);
|
|
158
|
+
if (tc.function) {
|
|
159
|
+
tc.function.arguments = clipJsonArguments(tc.function.arguments, limit);
|
|
119
160
|
}
|
|
120
161
|
}
|
|
121
162
|
}
|
|
@@ -180,12 +221,16 @@ function buildContext(messages, protectFrom, maxChars = index_js_1.appConfig.con
|
|
|
180
221
|
}
|
|
181
222
|
}
|
|
182
223
|
const dropped = working.filter((_, index) => !dropIndexes.has(index));
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
224
|
+
// 用提示说明被丢弃的轮次,避免模型认为自己没做过这些事。
|
|
225
|
+
// 注意:不能在对话中间插入 system 消息,部分厂商只允许 system 位于首位,
|
|
226
|
+
// 因此把提示并入首条 system 消息(没有则退化为 assistant 提示)。
|
|
227
|
+
const notice = `(已省略 ${dropCount} 轮较早的工具调用记录以控制上下文长度)`;
|
|
228
|
+
if (dropped[0]?.role === 'system' && typeof dropped[0].content === 'string') {
|
|
229
|
+
dropped[0] = { ...dropped[0], content: `${dropped[0].content}\n${notice}` };
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
dropped.splice(protectFrom, 0, { role: 'assistant', content: notice, attachments: [] });
|
|
233
|
+
}
|
|
189
234
|
working = dropped;
|
|
190
235
|
}
|
|
191
236
|
};
|