@jsonstudio/llms 0.6.954 → 0.6.1172
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/conversion/hub/operation-table/operation-table-runner.d.ts +18 -0
- package/dist/conversion/hub/operation-table/operation-table-runner.js +158 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.d.ts +8 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +303 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.d.ts +8 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +413 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.d.ts +7 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +841 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.d.ts +21 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +535 -0
- package/dist/conversion/hub/ops/operations.d.ts +19 -0
- package/dist/conversion/hub/ops/operations.js +126 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +9 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +489 -19
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +6 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +11 -0
- package/dist/conversion/hub/policy/policy-engine.js +41 -9
- package/dist/conversion/hub/policy/protocol-spec.d.ts +25 -0
- package/dist/conversion/hub/policy/protocol-spec.js +73 -23
- package/dist/conversion/hub/process/chat-process.js +252 -41
- package/dist/conversion/hub/response/provider-response.js +175 -2
- package/dist/conversion/hub/response/response-runtime.js +1 -1
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +1 -8
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +1 -365
- package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +1 -8
- package/dist/conversion/hub/semantic-mappers/chat-mapper.js +1 -467
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +1 -7
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +1 -903
- package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +1 -21
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +1 -593
- package/dist/conversion/hub/tool-surface/tool-surface-engine.d.ts +18 -0
- package/dist/conversion/hub/tool-surface/tool-surface-engine.js +571 -0
- package/dist/conversion/responses/responses-openai-bridge.js +14 -2
- package/dist/conversion/shared/bridge-message-utils.js +2 -8
- package/dist/conversion/shared/bridge-policies.js +5 -105
- package/dist/conversion/shared/gemini-tool-utils.js +89 -15
- package/dist/conversion/shared/protocol-field-allowlists.d.ts +7 -0
- package/dist/conversion/shared/protocol-field-allowlists.js +145 -0
- package/dist/conversion/shared/reasoning-tool-normalizer.js +4 -2
- package/dist/conversion/shared/snapshot-hooks.js +166 -3
- package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
- package/dist/conversion/shared/text-markup-normalizer.js +345 -9
- package/dist/conversion/shared/thought-signature-validator.d.ts +52 -0
- package/dist/conversion/shared/thought-signature-validator.js +170 -0
- package/dist/conversion/shared/tool-argument-repairer.d.ts +39 -0
- package/dist/conversion/shared/tool-argument-repairer.js +56 -0
- package/dist/conversion/shared/tool-call-id-manager.d.ts +113 -0
- package/dist/conversion/shared/tool-call-id-manager.js +231 -0
- package/dist/conversion/shared/tool-canonicalizer.js +2 -11
- package/dist/router/virtual-router/bootstrap.js +70 -5
- package/dist/router/virtual-router/context-advisor.d.ts +4 -0
- package/dist/router/virtual-router/context-advisor.js +3 -0
- package/dist/router/virtual-router/context-weighted.d.ts +31 -0
- package/dist/router/virtual-router/context-weighted.js +54 -0
- package/dist/router/virtual-router/engine-selection.js +284 -47
- package/dist/router/virtual-router/engine.d.ts +3 -0
- package/dist/router/virtual-router/engine.js +142 -33
- package/dist/router/virtual-router/health-weighted.d.ts +25 -0
- package/dist/router/virtual-router/health-weighted.js +63 -0
- package/dist/router/virtual-router/load-balancer.d.ts +2 -0
- package/dist/router/virtual-router/load-balancer.js +45 -16
- package/dist/router/virtual-router/routing-instructions.js +17 -1
- package/dist/router/virtual-router/sticky-session-store.js +136 -24
- package/dist/router/virtual-router/stop-message-file-resolver.d.ts +1 -0
- package/dist/router/virtual-router/stop-message-file-resolver.js +74 -0
- package/dist/router/virtual-router/stop-message-state-sync.d.ts +15 -0
- package/dist/router/virtual-router/stop-message-state-sync.js +57 -0
- package/dist/router/virtual-router/types.d.ts +98 -0
- package/dist/servertool/clock/config.d.ts +7 -0
- package/dist/servertool/clock/config.js +27 -0
- package/dist/servertool/clock/daemon.d.ts +3 -0
- package/dist/servertool/clock/daemon.js +79 -0
- package/dist/servertool/clock/io.d.ts +2 -0
- package/dist/servertool/clock/io.js +13 -0
- package/dist/servertool/clock/paths.d.ts +4 -0
- package/dist/servertool/clock/paths.js +25 -0
- package/dist/servertool/clock/session-store.d.ts +3 -0
- package/dist/servertool/clock/session-store.js +56 -0
- package/dist/servertool/clock/state.d.ts +5 -0
- package/dist/servertool/clock/state.js +62 -0
- package/dist/servertool/clock/task-store.d.ts +5 -0
- package/dist/servertool/clock/task-store.js +4 -0
- package/dist/servertool/clock/tasks.d.ts +17 -0
- package/dist/servertool/clock/tasks.js +221 -0
- package/dist/servertool/clock/types.d.ts +36 -0
- package/dist/servertool/clock/types.js +1 -0
- package/dist/servertool/engine.d.ts +2 -0
- package/dist/servertool/engine.js +161 -7
- package/dist/servertool/followup-shadow.d.ts +16 -0
- package/dist/servertool/followup-shadow.js +145 -0
- package/dist/servertool/handlers/apply-patch-guard.js +1 -265
- package/dist/servertool/handlers/clock-auto.d.ts +1 -0
- package/dist/servertool/handlers/clock-auto.js +160 -0
- package/dist/servertool/handlers/clock.d.ts +1 -0
- package/dist/servertool/handlers/clock.js +197 -0
- package/dist/servertool/handlers/exec-command-guard.js +7 -555
- package/dist/servertool/handlers/followup-request-builder.d.ts +15 -7
- package/dist/servertool/handlers/followup-request-builder.js +248 -28
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +62 -169
- package/dist/servertool/handlers/iflow-model-error-retry.js +18 -28
- package/dist/servertool/handlers/recursive-detection-guard.d.ts +1 -0
- package/dist/servertool/handlers/recursive-detection-guard.js +333 -0
- package/dist/servertool/handlers/stop-message-auto.js +47 -175
- package/dist/servertool/handlers/vision.d.ts +7 -1
- package/dist/servertool/handlers/vision.js +61 -117
- package/dist/servertool/handlers/web-search.d.ts +7 -1
- package/dist/servertool/handlers/web-search.js +122 -105
- package/dist/servertool/reenter-backend.d.ts +23 -0
- package/dist/servertool/reenter-backend.js +18 -0
- package/dist/servertool/server-side-tools.d.ts +3 -2
- package/dist/servertool/server-side-tools.js +64 -10
- package/dist/servertool/types.d.ts +92 -3
- package/dist/sse/json-to-sse/event-generators/responses.js +3 -21
- package/dist/sse/shared/serializers/responses-event-serializer.d.ts +8 -0
- package/dist/sse/shared/serializers/responses-event-serializer.js +19 -0
- package/dist/sse/shared/writer.js +24 -7
- package/dist/tools/apply-patch/execution-capturer.js +3 -1
- package/dist/tools/apply-patch/json/parse-loose.d.ts +3 -0
- package/dist/tools/apply-patch/json/parse-loose.js +139 -0
- package/dist/tools/apply-patch/patch-text/context-diff.d.ts +1 -0
- package/dist/tools/apply-patch/patch-text/context-diff.js +173 -0
- package/dist/tools/apply-patch/patch-text/git-diff.d.ts +1 -0
- package/dist/tools/apply-patch/patch-text/git-diff.js +138 -0
- package/dist/tools/apply-patch/patch-text/looks-like-patch.d.ts +1 -0
- package/dist/tools/apply-patch/patch-text/looks-like-patch.js +13 -0
- package/dist/tools/apply-patch/patch-text/normalize.d.ts +3 -0
- package/dist/tools/apply-patch/patch-text/normalize.js +262 -0
- package/dist/tools/apply-patch/structured/coercion.d.ts +3 -0
- package/dist/tools/apply-patch/structured/coercion.js +82 -0
- package/dist/tools/apply-patch/validation/shared.d.ts +3 -0
- package/dist/tools/apply-patch/validation/shared.js +6 -0
- package/dist/tools/apply-patch/validator.d.ts +2 -2
- package/dist/tools/apply-patch/validator.js +6 -556
- package/package.json +1 -1
|
@@ -3,6 +3,8 @@ export type ToolCallLite = {
|
|
|
3
3
|
name: string;
|
|
4
4
|
args: string;
|
|
5
5
|
};
|
|
6
|
+
export declare function extractParameterXmlToolsFromText(text: string): ToolCallLite[] | null;
|
|
7
|
+
export declare function extractBareExecCommandFromText(text: string): ToolCallLite[] | null;
|
|
6
8
|
export declare function extractApplyPatchCallsFromText(text: string): ToolCallLite[] | null;
|
|
7
9
|
export declare function extractExecuteBlocksFromText(text: string): ToolCallLite[] | null;
|
|
8
10
|
export declare function extractXMLToolCallsFromText(text: string): ToolCallLite[] | null;
|
|
@@ -6,6 +6,8 @@ import { isStructuredApplyPatchPayload } from '../../tools/apply-patch-structure
|
|
|
6
6
|
// stray markup or free-form text as JSON keys (reduces false positives).
|
|
7
7
|
const KNOWN_TOOLS = new Set([
|
|
8
8
|
'shell',
|
|
9
|
+
'exec_command',
|
|
10
|
+
'write_stdin',
|
|
9
11
|
'apply_patch',
|
|
10
12
|
'update_plan',
|
|
11
13
|
'view_image',
|
|
@@ -17,7 +19,22 @@ const KNOWN_TOOLS = new Set([
|
|
|
17
19
|
]);
|
|
18
20
|
const ALLOWED_KEYS = {
|
|
19
21
|
shell: new Set(['command', 'justification', 'timeout_ms', 'with_escalated_permissions', 'workdir']),
|
|
20
|
-
|
|
22
|
+
exec_command: new Set([
|
|
23
|
+
'cmd',
|
|
24
|
+
'command',
|
|
25
|
+
'workdir',
|
|
26
|
+
'justification',
|
|
27
|
+
'login',
|
|
28
|
+
'tty',
|
|
29
|
+
'shell',
|
|
30
|
+
'yield_time_ms',
|
|
31
|
+
'max_output_tokens',
|
|
32
|
+
'sandbox_permissions',
|
|
33
|
+
'timeout_ms'
|
|
34
|
+
]),
|
|
35
|
+
write_stdin: new Set(['session_id', 'chars', 'text', 'yield_time_ms', 'max_output_tokens']),
|
|
36
|
+
// apply_patch supports both freeform patch text (patch/input/instructions/text) and structured payloads (file/changes)
|
|
37
|
+
apply_patch: new Set(['patch', 'input', 'instructions', 'text', 'file', 'changes']),
|
|
21
38
|
update_plan: new Set(['explanation', 'plan']),
|
|
22
39
|
view_image: new Set(['path']),
|
|
23
40
|
list_mcp_resources: new Set(['server', 'cursor', 'filter', 'root']),
|
|
@@ -38,13 +55,40 @@ function normalizeKey(raw) {
|
|
|
38
55
|
return String(raw || '');
|
|
39
56
|
}
|
|
40
57
|
}
|
|
58
|
+
function readDefaultWorkdirFromEnv() {
|
|
59
|
+
try {
|
|
60
|
+
const env = String(process?.env?.ROUTECODEX_WORKDIR || '').trim() ||
|
|
61
|
+
String(process?.env?.RCC_WORKDIR || '').trim() ||
|
|
62
|
+
String(process?.env?.CLAUDE_WORKDIR || '').trim();
|
|
63
|
+
return env ? env : undefined;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
41
69
|
function filterArgsForTool(name, args) {
|
|
42
70
|
try {
|
|
43
71
|
const nm = String(name || '').toLowerCase();
|
|
44
72
|
const allow = ALLOWED_KEYS[nm];
|
|
45
73
|
if (!allow) {
|
|
46
74
|
// Unknown tool; be conservative: keep only simple safe keys if present
|
|
47
|
-
const safe = new Set([
|
|
75
|
+
const safe = new Set([
|
|
76
|
+
'cmd',
|
|
77
|
+
'command',
|
|
78
|
+
'workdir',
|
|
79
|
+
'patch',
|
|
80
|
+
'input',
|
|
81
|
+
'instructions',
|
|
82
|
+
'text',
|
|
83
|
+
'plan',
|
|
84
|
+
'explanation',
|
|
85
|
+
'path',
|
|
86
|
+
'server',
|
|
87
|
+
'uri',
|
|
88
|
+
'cursor',
|
|
89
|
+
'filter',
|
|
90
|
+
'root'
|
|
91
|
+
]);
|
|
48
92
|
const out = {};
|
|
49
93
|
for (const [k, v] of Object.entries(args || {})) {
|
|
50
94
|
const key = normalizeKey(k);
|
|
@@ -102,6 +146,266 @@ function extractStructuredApplyPatchPayloads(text) {
|
|
|
102
146
|
catch { /* ignore */ }
|
|
103
147
|
return payloads;
|
|
104
148
|
}
|
|
149
|
+
function tryParseJsonValue(text) {
|
|
150
|
+
try {
|
|
151
|
+
const trimmed = String(text || '').trim();
|
|
152
|
+
if (!trimmed)
|
|
153
|
+
return null;
|
|
154
|
+
if (!(trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('"'))) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return JSON.parse(trimmed);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function coerceCommandValueToString(value) {
|
|
164
|
+
try {
|
|
165
|
+
if (value === null || value === undefined)
|
|
166
|
+
return '';
|
|
167
|
+
if (typeof value === 'string')
|
|
168
|
+
return value.trim();
|
|
169
|
+
if (Array.isArray(value)) {
|
|
170
|
+
return value.map((v) => String(v)).join(' ').trim();
|
|
171
|
+
}
|
|
172
|
+
if (typeof value === 'object') {
|
|
173
|
+
const rec = value;
|
|
174
|
+
const direct = typeof rec.cmd === 'string'
|
|
175
|
+
? String(rec.cmd)
|
|
176
|
+
: typeof rec.command === 'string'
|
|
177
|
+
? String(rec.command)
|
|
178
|
+
: '';
|
|
179
|
+
if (direct.trim().length)
|
|
180
|
+
return direct.trim();
|
|
181
|
+
try {
|
|
182
|
+
return JSON.stringify(value);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return String(value);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return String(value).trim();
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return '';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function looksLikeBrokenToolMarkup(text) {
|
|
195
|
+
try {
|
|
196
|
+
const t = String(text || '');
|
|
197
|
+
if (!t)
|
|
198
|
+
return false;
|
|
199
|
+
return (t.includes('<exec_command') ||
|
|
200
|
+
t.includes('<parameter') ||
|
|
201
|
+
t.includes('<tool_call') ||
|
|
202
|
+
t.includes('</tool_call') ||
|
|
203
|
+
t.includes('<arg_value') ||
|
|
204
|
+
t.includes('</arg_value') ||
|
|
205
|
+
t.includes('</func_call') ||
|
|
206
|
+
/\bRan\b\s+\[/.test(t));
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function stripTrailingXmlGarbage(line) {
|
|
213
|
+
try {
|
|
214
|
+
const idx = line.indexOf('<');
|
|
215
|
+
if (idx >= 0) {
|
|
216
|
+
return line.slice(0, idx).trim();
|
|
217
|
+
}
|
|
218
|
+
return line.trim();
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return String(line || '').trim();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function normalizePossibleCommandLine(raw) {
|
|
225
|
+
try {
|
|
226
|
+
let line = String(raw || '');
|
|
227
|
+
line = line.replace(/^\s*(?:[•*+-]\s*)?/, '');
|
|
228
|
+
line = line.replace(/^\s*\$\s*/, '');
|
|
229
|
+
return stripTrailingXmlGarbage(line);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return '';
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function looksLikeShellCommand(line) {
|
|
236
|
+
try {
|
|
237
|
+
const t = String(line || '').trim();
|
|
238
|
+
if (!t)
|
|
239
|
+
return false;
|
|
240
|
+
if (t.startsWith('```') || t.startsWith('>'))
|
|
241
|
+
return false;
|
|
242
|
+
if (t.startsWith('zsh:') || t.startsWith('bash:'))
|
|
243
|
+
return false;
|
|
244
|
+
// Conservative allowlist: commands commonly emitted by Codex/Claude in tool-mode.
|
|
245
|
+
return /^(?:rg|ls|cat|node|npm|pnpm|yarn|git|find|sed|head|tail|wc|mkdir|rm|cp|mv|pwd)\b/.test(t);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Extract Anthropic/GLM-style XML tool tags that use <parameter name="...">...</parameter>, e.g.:
|
|
252
|
+
// <exec_command>
|
|
253
|
+
// <parameter name="command">["cd","/repo","&&","npm","run","build"]</parameter>
|
|
254
|
+
// <parameter name="workdir">/repo</parameter>
|
|
255
|
+
// </exec_command>
|
|
256
|
+
export function extractParameterXmlToolsFromText(text) {
|
|
257
|
+
try {
|
|
258
|
+
if (typeof text !== 'string' || !text)
|
|
259
|
+
return null;
|
|
260
|
+
const out = [];
|
|
261
|
+
// Some providers/models emit mismatched closing tags like </func_call>; accept a small set of known variants.
|
|
262
|
+
const toolRe = /<\s*(exec_command)\s*>([\s\S]*?)<\/\s*(?:\1|func_call|tool_call)\s*>/gi;
|
|
263
|
+
let tm;
|
|
264
|
+
while ((tm = toolRe.exec(text)) !== null) {
|
|
265
|
+
const rawName = (tm[1] || '').trim();
|
|
266
|
+
const inner = (tm[2] || '').trim();
|
|
267
|
+
if (!rawName || !inner)
|
|
268
|
+
continue;
|
|
269
|
+
const lname = rawName.toLowerCase();
|
|
270
|
+
const argsObj = {};
|
|
271
|
+
const paramRe = /<\s*parameter\s+name\s*=\s*"([^"]+)"\s*>([\s\S]*?)<\/\s*parameter\s*>/gi;
|
|
272
|
+
let pm;
|
|
273
|
+
while ((pm = paramRe.exec(inner)) !== null) {
|
|
274
|
+
const rawKey = (pm[1] || '').trim();
|
|
275
|
+
const key = normalizeKey(rawKey);
|
|
276
|
+
if (!key)
|
|
277
|
+
continue;
|
|
278
|
+
const rawVal = (pm[2] || '').trim();
|
|
279
|
+
if (!rawVal)
|
|
280
|
+
continue;
|
|
281
|
+
const parsed = tryParseJsonValue(rawVal);
|
|
282
|
+
if (key.toLowerCase() === 'command' || key.toLowerCase() === 'cmd') {
|
|
283
|
+
const cmd = coerceCommandValueToString(parsed ?? rawVal);
|
|
284
|
+
if (cmd.trim().length) {
|
|
285
|
+
argsObj.cmd = cmd;
|
|
286
|
+
argsObj.command = cmd;
|
|
287
|
+
}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (key.toLowerCase() === 'workdir') {
|
|
291
|
+
argsObj.workdir = rawVal;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
argsObj[key] = parsed ?? rawVal;
|
|
295
|
+
}
|
|
296
|
+
const filtered = filterArgsForTool(lname, argsObj);
|
|
297
|
+
if (!filtered || typeof filtered !== 'object' || Array.isArray(filtered))
|
|
298
|
+
continue;
|
|
299
|
+
const cmd = typeof filtered.cmd === 'string' ? String(filtered.cmd).trim() : '';
|
|
300
|
+
if (!cmd)
|
|
301
|
+
continue;
|
|
302
|
+
// If caller didn't provide workdir, fallback to a conservative env-derived workdir.
|
|
303
|
+
try {
|
|
304
|
+
if (!filtered.workdir) {
|
|
305
|
+
const envWorkdir = readDefaultWorkdirFromEnv();
|
|
306
|
+
if (envWorkdir)
|
|
307
|
+
filtered.workdir = envWorkdir;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
/* ignore */
|
|
312
|
+
}
|
|
313
|
+
let argsStr = '{}';
|
|
314
|
+
try {
|
|
315
|
+
argsStr = JSON.stringify(filtered);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
argsStr = '{}';
|
|
319
|
+
}
|
|
320
|
+
out.push({
|
|
321
|
+
id: `call_${Math.random().toString(36).slice(2, 10)}`,
|
|
322
|
+
name: lname,
|
|
323
|
+
args: argsStr
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return out.length ? out : null;
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function tryParseJsonArrayOnLine(line) {
|
|
333
|
+
try {
|
|
334
|
+
const trimmed = String(line || '').trim();
|
|
335
|
+
if (!trimmed.startsWith('[') || !trimmed.endsWith(']'))
|
|
336
|
+
return null;
|
|
337
|
+
const parsed = JSON.parse(trimmed);
|
|
338
|
+
if (!Array.isArray(parsed))
|
|
339
|
+
return null;
|
|
340
|
+
const out = [];
|
|
341
|
+
for (const entry of parsed) {
|
|
342
|
+
if (entry == null)
|
|
343
|
+
continue;
|
|
344
|
+
out.push(String(entry));
|
|
345
|
+
}
|
|
346
|
+
return out.length ? out : null;
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
export function extractBareExecCommandFromText(text) {
|
|
353
|
+
try {
|
|
354
|
+
if (typeof text !== 'string' || !text)
|
|
355
|
+
return null;
|
|
356
|
+
// Only attempt this when we see clear evidence of a broken tool markup (avoid turning examples into tool calls).
|
|
357
|
+
if (!looksLikeBrokenToolMarkup(text))
|
|
358
|
+
return null;
|
|
359
|
+
const candidates = [];
|
|
360
|
+
const lines = text.split(/\r?\n/);
|
|
361
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
362
|
+
const raw = String(lines[i] || '');
|
|
363
|
+
const line = normalizePossibleCommandLine(raw);
|
|
364
|
+
if (!line)
|
|
365
|
+
continue;
|
|
366
|
+
// Pattern A: "Ran [\"ls\", \"-l\", ...]"
|
|
367
|
+
const ran = line.match(/\bRan\b\s+(\[[\s\S]*\])\s*$/i);
|
|
368
|
+
if (ran && ran[1]) {
|
|
369
|
+
const arr = tryParseJsonArrayOnLine(ran[1]);
|
|
370
|
+
if (arr) {
|
|
371
|
+
candidates.push({ cmd: arr.join(' ').trim() });
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// Pattern B: a single-line shell command (rg/ls/node/...)
|
|
376
|
+
if (looksLikeShellCommand(line)) {
|
|
377
|
+
candidates.push({ cmd: line.trim() });
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (!candidates.length)
|
|
382
|
+
return null;
|
|
383
|
+
const picked = candidates[candidates.length - 1];
|
|
384
|
+
const argsObj = { cmd: picked.cmd };
|
|
385
|
+
const envWorkdir = readDefaultWorkdirFromEnv();
|
|
386
|
+
if (envWorkdir) {
|
|
387
|
+
argsObj.workdir = envWorkdir;
|
|
388
|
+
}
|
|
389
|
+
const filtered = filterArgsForTool('exec_command', argsObj);
|
|
390
|
+
let argsStr = '{}';
|
|
391
|
+
try {
|
|
392
|
+
argsStr = JSON.stringify(filtered);
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
argsStr = '{}';
|
|
396
|
+
}
|
|
397
|
+
return [
|
|
398
|
+
{
|
|
399
|
+
id: `call_${Math.random().toString(36).slice(2, 10)}`,
|
|
400
|
+
name: 'exec_command',
|
|
401
|
+
args: argsStr
|
|
402
|
+
}
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
105
409
|
export function extractApplyPatchCallsFromText(text) {
|
|
106
410
|
try {
|
|
107
411
|
if (typeof text !== 'string' || !text)
|
|
@@ -392,14 +696,46 @@ export function normalizeAssistantTextToToolCalls(message) {
|
|
|
392
696
|
if (Array.isArray(message.tool_calls) && message.tool_calls.length)
|
|
393
697
|
return message;
|
|
394
698
|
const content = message.content;
|
|
395
|
-
const
|
|
396
|
-
if (
|
|
699
|
+
const candidates = [];
|
|
700
|
+
if (typeof content === 'string') {
|
|
701
|
+
candidates.push(content);
|
|
702
|
+
}
|
|
703
|
+
else if (Array.isArray(content)) {
|
|
704
|
+
for (const part of content) {
|
|
705
|
+
if (!part || typeof part !== 'object')
|
|
706
|
+
continue;
|
|
707
|
+
const p = part;
|
|
708
|
+
if (typeof p.text === 'string' && p.text.trim()) {
|
|
709
|
+
candidates.push(p.text);
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
if (typeof p.content === 'string' && p.content.trim()) {
|
|
713
|
+
candidates.push(p.content);
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (typeof message.reasoning === 'string' && message.reasoning.trim()) {
|
|
719
|
+
candidates.push(String(message.reasoning));
|
|
720
|
+
}
|
|
721
|
+
if (typeof message.thinking === 'string' && message.thinking.trim()) {
|
|
722
|
+
candidates.push(String(message.thinking));
|
|
723
|
+
}
|
|
724
|
+
if (!candidates.length)
|
|
397
725
|
return message;
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
726
|
+
let calls = null;
|
|
727
|
+
for (const text of candidates) {
|
|
728
|
+
// Order: xml-like <tool_call> → apply_patch → execute blocks → 简单 XML 工具(list_directory 等) → bare command salvage
|
|
729
|
+
calls =
|
|
730
|
+
extractParameterXmlToolsFromText(text) ||
|
|
731
|
+
extractXMLToolCallsFromText(text) ||
|
|
732
|
+
extractApplyPatchCallsFromText(text) ||
|
|
733
|
+
extractExecuteBlocksFromText(text) ||
|
|
734
|
+
extractSimpleXmlToolsFromText(text) ||
|
|
735
|
+
extractBareExecCommandFromText(text);
|
|
736
|
+
if (calls && calls.length)
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
403
739
|
if (calls && calls.length) {
|
|
404
740
|
const toolCalls = calls.map((c) => ({ id: c.id, type: 'function', function: { name: c.name, arguments: c.args } }));
|
|
405
741
|
const copy = { ...message };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThoughtSignature Validator
|
|
3
|
+
*
|
|
4
|
+
* 提供严格的 thoughtSignature 验证功能,用于 Claude/Gemini thinking 块的签名验证。
|
|
5
|
+
* 参考 gcli2api 的实现,确保与上游 API 的兼容性。
|
|
6
|
+
*/
|
|
7
|
+
import type { JsonObject, JsonValue } from '../hub/types/json.js';
|
|
8
|
+
export interface ThoughtSignatureValidationOptions {
|
|
9
|
+
/**
|
|
10
|
+
* 最小签名长度(默认 10 个字符)
|
|
11
|
+
*/
|
|
12
|
+
minLength?: number;
|
|
13
|
+
/**
|
|
14
|
+
* 是否允许空 thinking + 任意签名(trailing signature case)
|
|
15
|
+
* 默认 true
|
|
16
|
+
*/
|
|
17
|
+
allowEmptyThinkingWithSignature?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* 是否在验证失败时将 thinking 转换为文本
|
|
20
|
+
* 默认 true
|
|
21
|
+
*/
|
|
22
|
+
convertToTextOnFailure?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 检查 thinking 块是否有有效的 thoughtSignature
|
|
26
|
+
*
|
|
27
|
+
* @param block - thinking 块对象
|
|
28
|
+
* @param options - 验证选项
|
|
29
|
+
* @returns 是否有效
|
|
30
|
+
*/
|
|
31
|
+
export declare function hasValidThoughtSignature(block: unknown, options?: ThoughtSignatureValidationOptions): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* 清理 thinking 块,移除额外字段,保留有效签名
|
|
34
|
+
*
|
|
35
|
+
* @param block - thinking 块对象
|
|
36
|
+
* @returns 清理后的块
|
|
37
|
+
*/
|
|
38
|
+
export declare function sanitizeThinkingBlock(block: unknown): JsonObject;
|
|
39
|
+
/**
|
|
40
|
+
* 过滤消息中的无效 thinking 块
|
|
41
|
+
*
|
|
42
|
+
* @param messages - Anthropic messages 列表(会被修改)
|
|
43
|
+
* @param options - 验证选项
|
|
44
|
+
*/
|
|
45
|
+
export declare function filterInvalidThinkingBlocks(messages: JsonValue[], options?: ThoughtSignatureValidationOptions): void;
|
|
46
|
+
/**
|
|
47
|
+
* 移除末尾未签名的 thinking 块
|
|
48
|
+
*
|
|
49
|
+
* @param blocks - content blocks 列表(会被修改)
|
|
50
|
+
* @param options - 验证选项
|
|
51
|
+
*/
|
|
52
|
+
export declare function removeTrailingUnsignedThinkingBlocks(blocks: JsonValue[], options?: ThoughtSignatureValidationOptions): void;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThoughtSignature Validator
|
|
3
|
+
*
|
|
4
|
+
* 提供严格的 thoughtSignature 验证功能,用于 Claude/Gemini thinking 块的签名验证。
|
|
5
|
+
* 参考 gcli2api 的实现,确保与上游 API 的兼容性。
|
|
6
|
+
*/
|
|
7
|
+
const DEFAULT_OPTIONS = {
|
|
8
|
+
minLength: 10,
|
|
9
|
+
allowEmptyThinkingWithSignature: true,
|
|
10
|
+
convertToTextOnFailure: true
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* 检查 thinking 块是否有有效的 thoughtSignature
|
|
14
|
+
*
|
|
15
|
+
* @param block - thinking 块对象
|
|
16
|
+
* @param options - 验证选项
|
|
17
|
+
* @returns 是否有效
|
|
18
|
+
*/
|
|
19
|
+
export function hasValidThoughtSignature(block, options) {
|
|
20
|
+
if (!block || typeof block !== 'object') {
|
|
21
|
+
return true; // 非 thinking 块默认有效
|
|
22
|
+
}
|
|
23
|
+
const obj = block;
|
|
24
|
+
const blockType = obj.type;
|
|
25
|
+
if (blockType !== 'thinking' && blockType !== 'reasoning' && blockType !== 'redacted_thinking') {
|
|
26
|
+
return true; // 非 thinking 块默认有效
|
|
27
|
+
}
|
|
28
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
29
|
+
const thinking = String(obj.thinking || obj.text || '');
|
|
30
|
+
const signature = coerceThoughtSignature(obj.thoughtSignature || obj.signature);
|
|
31
|
+
// 空 thinking + 任意签名 = 有效 (trailing signature case)
|
|
32
|
+
if (!thinking.trim() && signature !== undefined && opts.allowEmptyThinkingWithSignature) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
// 有内容 + 足够长度的 signature = 有效
|
|
36
|
+
if (signature && signature.length >= opts.minLength) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 清理 thinking 块,移除额外字段,保留有效签名
|
|
43
|
+
*
|
|
44
|
+
* @param block - thinking 块对象
|
|
45
|
+
* @returns 清理后的块
|
|
46
|
+
*/
|
|
47
|
+
export function sanitizeThinkingBlock(block) {
|
|
48
|
+
if (!block || typeof block !== 'object') {
|
|
49
|
+
return block;
|
|
50
|
+
}
|
|
51
|
+
const obj = block;
|
|
52
|
+
const blockType = obj.type;
|
|
53
|
+
if (blockType !== 'thinking' && blockType !== 'reasoning' && blockType !== 'redacted_thinking') {
|
|
54
|
+
return obj;
|
|
55
|
+
}
|
|
56
|
+
const sanitized = {
|
|
57
|
+
type: blockType,
|
|
58
|
+
thinking: String(obj.thinking || obj.text || '')
|
|
59
|
+
};
|
|
60
|
+
const signature = coerceThoughtSignature(obj.thoughtSignature || obj.signature);
|
|
61
|
+
if (signature) {
|
|
62
|
+
sanitized.thoughtSignature = signature;
|
|
63
|
+
}
|
|
64
|
+
return sanitized;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 过滤消息中的无效 thinking 块
|
|
68
|
+
*
|
|
69
|
+
* @param messages - Anthropic messages 列表(会被修改)
|
|
70
|
+
* @param options - 验证选项
|
|
71
|
+
*/
|
|
72
|
+
export function filterInvalidThinkingBlocks(messages, options) {
|
|
73
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
74
|
+
let totalFiltered = 0;
|
|
75
|
+
for (const msg of messages) {
|
|
76
|
+
if (!msg || typeof msg !== 'object')
|
|
77
|
+
continue;
|
|
78
|
+
const msgObj = msg;
|
|
79
|
+
const role = msgObj.role;
|
|
80
|
+
// 只处理 assistant 和 model 消息
|
|
81
|
+
if (role !== 'assistant' && role !== 'model')
|
|
82
|
+
continue;
|
|
83
|
+
const content = msgObj.content;
|
|
84
|
+
if (!Array.isArray(content))
|
|
85
|
+
continue;
|
|
86
|
+
const originalLen = content.length;
|
|
87
|
+
const newBlocks = [];
|
|
88
|
+
for (const block of content) {
|
|
89
|
+
if (!block || typeof block !== 'object') {
|
|
90
|
+
newBlocks.push(block);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const blockType = block.type;
|
|
94
|
+
if (blockType !== 'thinking' && blockType !== 'reasoning' && blockType !== 'redacted_thinking') {
|
|
95
|
+
newBlocks.push(block);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// 检查 thinking 块的有效性
|
|
99
|
+
if (hasValidThoughtSignature(block, opts)) {
|
|
100
|
+
// 有效签名,清理后保留
|
|
101
|
+
newBlocks.push(sanitizeThinkingBlock(block));
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// 无效签名
|
|
105
|
+
const thinkingText = String(block.thinking || block.text || '');
|
|
106
|
+
if (thinkingText.trim() && opts.convertToTextOnFailure) {
|
|
107
|
+
// 有内容,转换为文本
|
|
108
|
+
newBlocks.push({ type: 'text', text: thinkingText });
|
|
109
|
+
}
|
|
110
|
+
// 否则丢弃(空 thinking + 无效签名)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
msgObj.content = newBlocks;
|
|
114
|
+
const filteredCount = originalLen - newBlocks.length;
|
|
115
|
+
totalFiltered += filteredCount;
|
|
116
|
+
// 如果过滤后为空,添加一个空文本块以保持消息有效
|
|
117
|
+
if (!newBlocks.length) {
|
|
118
|
+
msgObj.content = [{ type: 'text', text: '' }];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (totalFiltered > 0) {
|
|
122
|
+
// 可以在这里添加日志记录
|
|
123
|
+
console.debug(`[ThoughtSignatureValidator] Filtered ${totalFiltered} invalid thinking block(s)`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 移除末尾未签名的 thinking 块
|
|
128
|
+
*
|
|
129
|
+
* @param blocks - content blocks 列表(会被修改)
|
|
130
|
+
* @param options - 验证选项
|
|
131
|
+
*/
|
|
132
|
+
export function removeTrailingUnsignedThinkingBlocks(blocks, options) {
|
|
133
|
+
if (!Array.isArray(blocks) || !blocks.length)
|
|
134
|
+
return;
|
|
135
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
136
|
+
let end_index = blocks.length;
|
|
137
|
+
// 从末尾向前查找第一个有效签名的 thinking 块
|
|
138
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
139
|
+
const block = blocks[i];
|
|
140
|
+
if (!block || typeof block !== 'object')
|
|
141
|
+
continue;
|
|
142
|
+
const blockType = block.type;
|
|
143
|
+
if (blockType === 'thinking' || blockType === 'reasoning' || blockType === 'redacted_thinking') {
|
|
144
|
+
if (!hasValidThoughtSignature(block, opts)) {
|
|
145
|
+
end_index = i;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
break; // 遇到有效签名的 thinking 块,停止
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
break; // 遇到非 thinking 块,停止
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// 移除末尾未签名的 thinking 块
|
|
156
|
+
if (end_index < blocks.length) {
|
|
157
|
+
const removed = blocks.length - end_index;
|
|
158
|
+
blocks.splice(end_index);
|
|
159
|
+
console.debug(`[ThoughtSignatureValidator] Removed ${removed} trailing unsigned thinking block(s)`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* 提取并强制转换 thoughtSignature
|
|
164
|
+
*/
|
|
165
|
+
function coerceThoughtSignature(value) {
|
|
166
|
+
if (typeof value === 'string' && value.trim().length) {
|
|
167
|
+
return value.trim();
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Argument Repairer
|
|
3
|
+
*
|
|
4
|
+
* Tool calls require `function.arguments` to be a JSON string. Models often emit JSON-ish
|
|
5
|
+
* payloads (single quotes, fenced blocks, comments, trailing commas, etc.).
|
|
6
|
+
*
|
|
7
|
+
* This module provides a deterministic, best-effort repair surface that returns a JSON string
|
|
8
|
+
* or `{}` and never throws.
|
|
9
|
+
*/
|
|
10
|
+
export interface RepairResult {
|
|
11
|
+
repaired: string;
|
|
12
|
+
success: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class ToolArgumentRepairer {
|
|
16
|
+
repairToString(args: unknown): string;
|
|
17
|
+
validateAndRepair(toolName: string, args: unknown): RepairResult;
|
|
18
|
+
/**
|
|
19
|
+
* 批量修复工具参数
|
|
20
|
+
*
|
|
21
|
+
* @param toolCalls - 工具调用列表
|
|
22
|
+
* @returns 修复后的工具调用列表
|
|
23
|
+
*/
|
|
24
|
+
repairToolCalls(toolCalls: Array<{
|
|
25
|
+
name?: string;
|
|
26
|
+
arguments?: unknown;
|
|
27
|
+
}>): Array<{
|
|
28
|
+
name?: string;
|
|
29
|
+
arguments: string;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 快捷函数:修复工具参数
|
|
34
|
+
*/
|
|
35
|
+
export declare function repairToolArguments(args: unknown): string;
|
|
36
|
+
/**
|
|
37
|
+
* 快捷函数:验证并修复工具参数
|
|
38
|
+
*/
|
|
39
|
+
export declare function validateToolArguments(toolName: string, args: unknown): RepairResult;
|