@jsonstudio/llms 0.6.633 → 0.6.743
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/codecs/anthropic-openai-codec.js +0 -5
- package/dist/conversion/codecs/openai-openai-codec.js +0 -6
- package/dist/conversion/codecs/responses-openai-codec.js +1 -7
- package/dist/conversion/hub/node-support.js +5 -4
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +14 -1
- package/dist/conversion/hub/pipeline/hub-pipeline.js +82 -18
- package/dist/conversion/hub/pipeline/session-identifiers.js +132 -2
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +23 -19
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +47 -0
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +4 -2
- package/dist/conversion/hub/process/chat-process.js +2 -0
- package/dist/conversion/hub/response/provider-response.js +6 -1
- package/dist/conversion/hub/snapshot-recorder.js +8 -1
- package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.js +0 -7
- package/dist/conversion/responses/responses-openai-bridge.js +47 -7
- package/dist/conversion/shared/compaction-detect.d.ts +2 -0
- package/dist/conversion/shared/compaction-detect.js +53 -0
- package/dist/conversion/shared/errors.d.ts +1 -1
- package/dist/conversion/shared/reasoning-tool-normalizer.js +7 -0
- package/dist/conversion/shared/snapshot-hooks.d.ts +2 -0
- package/dist/conversion/shared/snapshot-hooks.js +180 -4
- package/dist/conversion/shared/snapshot-utils.d.ts +4 -0
- package/dist/conversion/shared/snapshot-utils.js +4 -0
- package/dist/conversion/shared/tool-filter-pipeline.js +3 -9
- package/dist/conversion/shared/tool-governor.d.ts +2 -0
- package/dist/conversion/shared/tool-governor.js +101 -13
- package/dist/conversion/shared/tool-harvester.js +42 -2
- package/dist/filters/index.d.ts +0 -2
- package/dist/filters/index.js +0 -2
- package/dist/filters/special/request-tools-normalize.d.ts +11 -0
- package/dist/filters/special/request-tools-normalize.js +13 -50
- package/dist/filters/special/response-apply-patch-toon-decode.js +403 -82
- package/dist/filters/special/response-tool-arguments-toon-decode.js +6 -75
- package/dist/filters/utils/snapshot-writer.js +42 -4
- package/dist/guidance/index.js +8 -2
- package/dist/router/virtual-router/engine-health.js +0 -4
- package/dist/router/virtual-router/engine-selection.d.ts +2 -1
- package/dist/router/virtual-router/engine-selection.js +101 -9
- package/dist/router/virtual-router/engine.d.ts +5 -1
- package/dist/router/virtual-router/engine.js +188 -5
- package/dist/router/virtual-router/routing-instructions.d.ts +6 -0
- package/dist/router/virtual-router/routing-instructions.js +18 -3
- package/dist/router/virtual-router/sticky-session-store.d.ts +1 -0
- package/dist/router/virtual-router/sticky-session-store.js +36 -0
- package/dist/router/virtual-router/types.d.ts +22 -0
- package/dist/servertool/engine.js +335 -9
- package/dist/servertool/handlers/compaction-detect.d.ts +1 -0
- package/dist/servertool/handlers/compaction-detect.js +1 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +29 -5
- package/dist/servertool/handlers/iflow-model-error-retry.js +17 -0
- package/dist/servertool/handlers/stop-message-auto.js +199 -19
- package/dist/servertool/server-side-tools.d.ts +0 -1
- package/dist/servertool/server-side-tools.js +0 -1
- package/dist/servertool/types.d.ts +1 -0
- package/dist/tools/apply-patch-structured.js +52 -15
- package/dist/tools/tool-registry.js +537 -15
- package/dist/utils/toon.d.ts +4 -0
- package/dist/utils/toon.js +75 -0
- package/package.json +4 -2
- package/dist/test-output/virtual-router/results.json +0 -1
- package/dist/test-output/virtual-router/summary.json +0 -12
package/dist/filters/index.js
CHANGED
|
@@ -14,8 +14,6 @@ export * from './special/response-tool-arguments-stringify.js';
|
|
|
14
14
|
export * from './special/response-finish-invariants.js';
|
|
15
15
|
// TOON support (default ON via RCC_TOON_ENABLE unless explicitly disabled)
|
|
16
16
|
export * from './special/request-tools-normalize.js';
|
|
17
|
-
export * from './special/response-tool-arguments-toon-decode.js';
|
|
18
|
-
export * from './special/response-apply-patch-toon-decode.js';
|
|
19
17
|
// Arguments policy filters (synced)
|
|
20
18
|
export * from './special/response-tool-arguments-blacklist.js';
|
|
21
19
|
export * from './special/response-tool-arguments-schema-converge.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Filter, FilterContext, FilterResult, JsonObject } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Normalize OpenAI tools definitions at the final request stage.
|
|
4
|
+
* - Enforces { command: string | string[], workdir?: string } shape for shell-like tools.
|
|
5
|
+
* - Best-effort; never throws.
|
|
6
|
+
*/
|
|
7
|
+
export declare class RequestOpenAIToolsNormalizeFilter implements Filter<JsonObject> {
|
|
8
|
+
readonly name = "request_openai_tools_normalize";
|
|
9
|
+
readonly stage: FilterContext['stage'];
|
|
10
|
+
apply(input: JsonObject): Promise<FilterResult<JsonObject>>;
|
|
11
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildShellDescription, hasApplyPatchToolDeclared, isShellToolName } from '../../tools/tool-description-utils.js';
|
|
2
2
|
function isObject(v) {
|
|
3
3
|
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
6
|
* Normalize OpenAI tools definitions at the final request stage.
|
|
7
|
-
* -
|
|
8
|
-
* - Otherwise enforces { command: string | string[], workdir?: string } shape.
|
|
7
|
+
* - Enforces { command: string | string[], workdir?: string } shape for shell-like tools.
|
|
9
8
|
* - Best-effort; never throws.
|
|
10
9
|
*/
|
|
11
10
|
export class RequestOpenAIToolsNormalizeFilter {
|
|
@@ -14,23 +13,6 @@ export class RequestOpenAIToolsNormalizeFilter {
|
|
|
14
13
|
async apply(input) {
|
|
15
14
|
try {
|
|
16
15
|
const out = JSON.parse(JSON.stringify(input || {}));
|
|
17
|
-
const messages = Array.isArray(out.messages) ? out.messages : [];
|
|
18
|
-
const lastUser = [...messages].reverse().find((m) => m && m.role === 'user');
|
|
19
|
-
let lastText = '';
|
|
20
|
-
if (typeof lastUser?.content === 'string') {
|
|
21
|
-
lastText = lastUser.content;
|
|
22
|
-
}
|
|
23
|
-
else if (Array.isArray(lastUser?.content)) {
|
|
24
|
-
for (const p of lastUser.content) {
|
|
25
|
-
if (p && typeof p.text === 'string' && p.text.trim()) {
|
|
26
|
-
lastText = p.text;
|
|
27
|
-
break;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const looksHeredoc = /<<[-']?\w+/.test(lastText);
|
|
32
|
-
const toonEnv = String(process?.env?.RCC_TOON_ENABLE || process?.env?.ROUTECODEX_TOON_ENABLE || '').toLowerCase();
|
|
33
|
-
const toonEnabled = toonEnv ? !(toonEnv === '0' || toonEnv === 'false' || toonEnv === 'off') : true; // default ON
|
|
34
16
|
const tools = Array.isArray(out.tools) ? out.tools : [];
|
|
35
17
|
const hasApplyPatchTool = hasApplyPatchToolDeclared(tools);
|
|
36
18
|
if (!tools.length) {
|
|
@@ -79,37 +61,18 @@ export class RequestOpenAIToolsNormalizeFilter {
|
|
|
79
61
|
try {
|
|
80
62
|
const rawToolName = String(dst.function.name || '');
|
|
81
63
|
const isShell = isShellToolName(rawToolName);
|
|
82
|
-
const isApplyPatch = normalizeToolName(rawToolName) === 'apply_patch';
|
|
83
64
|
if (isShell) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
type: '
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
additionalProperties: false
|
|
96
|
-
};
|
|
97
|
-
const toonDescription = 'Use TOON: put arguments into arguments.toon as multi-line key: value (e.g., command / workdir).';
|
|
98
|
-
dst.function.description = appendApplyPatchReminder(toonDescription, hasApplyPatchTool);
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
dst.function.parameters = {
|
|
102
|
-
type: 'object',
|
|
103
|
-
properties: {
|
|
104
|
-
command: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] },
|
|
105
|
-
workdir: { type: 'string' }
|
|
106
|
-
},
|
|
107
|
-
required: ['command'],
|
|
108
|
-
additionalProperties: false
|
|
109
|
-
};
|
|
110
|
-
const label = rawToolName && rawToolName.trim().length > 0 ? rawToolName.trim() : 'shell';
|
|
111
|
-
dst.function.description = buildShellDescription(label, hasApplyPatchTool);
|
|
112
|
-
}
|
|
65
|
+
dst.function.parameters = {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
command: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] },
|
|
69
|
+
workdir: { type: 'string' }
|
|
70
|
+
},
|
|
71
|
+
required: ['command'],
|
|
72
|
+
additionalProperties: false
|
|
73
|
+
};
|
|
74
|
+
const label = rawToolName && rawToolName.trim().length > 0 ? rawToolName.trim() : 'shell';
|
|
75
|
+
dst.function.description = buildShellDescription(label, hasApplyPatchTool);
|
|
113
76
|
}
|
|
114
77
|
}
|
|
115
78
|
catch { /* ignore */ }
|
|
@@ -17,6 +17,393 @@ function readString(value) {
|
|
|
17
17
|
}
|
|
18
18
|
return undefined;
|
|
19
19
|
}
|
|
20
|
+
function stripCodeFences(text) {
|
|
21
|
+
const trimmed = text.trim();
|
|
22
|
+
const fenceRe = /```(?:diff|patch|apply_patch|toon|text|json)?\s*([\s\S]*?)```/gi;
|
|
23
|
+
const candidates = [];
|
|
24
|
+
let match = null;
|
|
25
|
+
while ((match = fenceRe.exec(trimmed))) {
|
|
26
|
+
if (match[1]) {
|
|
27
|
+
candidates.push(match[1].trim());
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (!candidates.length) {
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
for (const candidate of candidates) {
|
|
34
|
+
if (candidate.includes('*** Begin Patch') ||
|
|
35
|
+
candidate.includes('*** Update File:') ||
|
|
36
|
+
candidate.includes('diff --git')) {
|
|
37
|
+
return candidate;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return candidates[0] ?? text;
|
|
41
|
+
}
|
|
42
|
+
function looksLikePatch(text) {
|
|
43
|
+
if (!text)
|
|
44
|
+
return false;
|
|
45
|
+
const t = text.trim();
|
|
46
|
+
if (!t)
|
|
47
|
+
return false;
|
|
48
|
+
return (t.includes('*** Begin Patch') ||
|
|
49
|
+
t.includes('*** Update File:') ||
|
|
50
|
+
t.includes('*** Add File:') ||
|
|
51
|
+
t.includes('*** Delete File:') ||
|
|
52
|
+
t.includes('diff --git') ||
|
|
53
|
+
/^(?:@@|\+\+\+\s|---\s)/m.test(t));
|
|
54
|
+
}
|
|
55
|
+
function convertGitDiffToApplyPatch(text) {
|
|
56
|
+
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
|
57
|
+
const files = [];
|
|
58
|
+
let current = null;
|
|
59
|
+
let sawDiff = false;
|
|
60
|
+
const flush = () => {
|
|
61
|
+
if (!current)
|
|
62
|
+
return;
|
|
63
|
+
if (current.path && current.kind === 'delete') {
|
|
64
|
+
files.push(current);
|
|
65
|
+
current = null;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!current.path || (current.kind === 'update' && current.lines.length === 0)) {
|
|
69
|
+
current = null;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
files.push(current);
|
|
73
|
+
current = null;
|
|
74
|
+
};
|
|
75
|
+
for (const raw of lines) {
|
|
76
|
+
const line = raw;
|
|
77
|
+
const diffMatch = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
78
|
+
if (diffMatch) {
|
|
79
|
+
sawDiff = true;
|
|
80
|
+
flush();
|
|
81
|
+
const path = diffMatch[2] || diffMatch[1];
|
|
82
|
+
current = { path, kind: 'update', lines: [] };
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!current) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (line.startsWith('new file mode')) {
|
|
89
|
+
current.kind = 'add';
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (line.startsWith('deleted file mode')) {
|
|
93
|
+
current.kind = 'delete';
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (line.startsWith('index ') || line.startsWith('old mode') || line.startsWith('new mode')) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (line.startsWith('--- ')) {
|
|
100
|
+
if (line.includes('/dev/null')) {
|
|
101
|
+
current.kind = 'add';
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (line.startsWith('+++ ')) {
|
|
106
|
+
if (line.includes('/dev/null')) {
|
|
107
|
+
current.kind = 'delete';
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const plusMatch = line.match(/^\+\+\+\s+(?:b\/)?(.+)$/);
|
|
111
|
+
if (plusMatch && plusMatch[1]) {
|
|
112
|
+
current.path = plusMatch[1];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (current.kind === 'delete') {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (current.kind === 'add') {
|
|
121
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
122
|
+
current.lines.push(line);
|
|
123
|
+
}
|
|
124
|
+
else if (line.startsWith('\\')) {
|
|
125
|
+
current.lines.push(`+${line}`);
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (line.startsWith('@@') ||
|
|
130
|
+
line.startsWith('+') ||
|
|
131
|
+
line.startsWith('-') ||
|
|
132
|
+
line.startsWith(' ') ||
|
|
133
|
+
line.startsWith('\\')) {
|
|
134
|
+
current.lines.push(line.startsWith('\\') ? ` ${line}` : line);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
flush();
|
|
138
|
+
if (!sawDiff || files.length === 0) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const output = ['*** Begin Patch'];
|
|
142
|
+
for (const file of files) {
|
|
143
|
+
if (!file.path)
|
|
144
|
+
continue;
|
|
145
|
+
if (file.kind === 'add') {
|
|
146
|
+
output.push(`*** Add File: ${file.path}`);
|
|
147
|
+
for (const line of file.lines) {
|
|
148
|
+
if (line.startsWith('+')) {
|
|
149
|
+
output.push(line);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
output.push(`+${line}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (file.kind === 'delete') {
|
|
158
|
+
output.push(`*** Delete File: ${file.path}`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
output.push(`*** Update File: ${file.path}`);
|
|
162
|
+
for (const line of file.lines) {
|
|
163
|
+
if (line.startsWith('@@') || line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) {
|
|
164
|
+
output.push(line);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
output.push(` ${line}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
output.push('*** End Patch');
|
|
172
|
+
return output.join('\n');
|
|
173
|
+
}
|
|
174
|
+
function normalizeApplyPatchText(raw) {
|
|
175
|
+
if (!raw)
|
|
176
|
+
return raw;
|
|
177
|
+
let text = raw.replace(/\r\n/g, '\n');
|
|
178
|
+
text = stripCodeFences(text);
|
|
179
|
+
text = text.trim();
|
|
180
|
+
if (!text) {
|
|
181
|
+
return raw;
|
|
182
|
+
}
|
|
183
|
+
if (!text.includes('*** Begin Patch') && text.includes('diff --git')) {
|
|
184
|
+
const converted = convertGitDiffToApplyPatch(text);
|
|
185
|
+
if (converted) {
|
|
186
|
+
text = converted;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (text.includes('*** Create File:')) {
|
|
190
|
+
text = text.replace(/\*\*\* Create File:/g, '*** Add File:');
|
|
191
|
+
}
|
|
192
|
+
const hasBegin = text.includes('*** Begin Patch');
|
|
193
|
+
const hasEnd = text.includes('*** End Patch');
|
|
194
|
+
if (hasBegin && !hasEnd) {
|
|
195
|
+
text = `${text}\n*** End Patch`;
|
|
196
|
+
}
|
|
197
|
+
if (!hasBegin && /^\*\*\* (Add|Update|Delete) File:/m.test(text)) {
|
|
198
|
+
text = `*** Begin Patch\n${text}\n*** End Patch`;
|
|
199
|
+
}
|
|
200
|
+
if (!text.includes('*** Begin Patch')) {
|
|
201
|
+
return text;
|
|
202
|
+
}
|
|
203
|
+
const lines = text.split('\n');
|
|
204
|
+
const output = [];
|
|
205
|
+
let inUpdateSection = false;
|
|
206
|
+
let afterUpdateHeader = false;
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
if (line.startsWith('*** Begin Patch')) {
|
|
209
|
+
output.push(line);
|
|
210
|
+
inUpdateSection = false;
|
|
211
|
+
afterUpdateHeader = false;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (line.startsWith('*** End Patch')) {
|
|
215
|
+
output.push(line);
|
|
216
|
+
inUpdateSection = false;
|
|
217
|
+
afterUpdateHeader = false;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (line.startsWith('*** Update File:')) {
|
|
221
|
+
output.push(line);
|
|
222
|
+
inUpdateSection = true;
|
|
223
|
+
afterUpdateHeader = true;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (line.startsWith('*** Add File:') || line.startsWith('*** Delete File:')) {
|
|
227
|
+
output.push(line);
|
|
228
|
+
inUpdateSection = false;
|
|
229
|
+
afterUpdateHeader = false;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (inUpdateSection) {
|
|
233
|
+
if (afterUpdateHeader && line.trim() === '') {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
afterUpdateHeader = false;
|
|
237
|
+
if (line.startsWith('@@') || line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) {
|
|
238
|
+
output.push(line);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
output.push(` ${line}`);
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
output.push(line);
|
|
246
|
+
}
|
|
247
|
+
return output.join('\n');
|
|
248
|
+
}
|
|
249
|
+
function collectFunctionCallAdapters(payload) {
|
|
250
|
+
const adapters = [];
|
|
251
|
+
const anyPayload = payload;
|
|
252
|
+
const choices = Array.isArray(anyPayload.choices)
|
|
253
|
+
? (anyPayload.choices ?? [])
|
|
254
|
+
: [];
|
|
255
|
+
for (const choice of choices) {
|
|
256
|
+
const message = choice && typeof choice === 'object' ? choice.message : undefined;
|
|
257
|
+
const toolCalls = message && Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
258
|
+
for (const tc of toolCalls) {
|
|
259
|
+
const fnObj = tc && typeof tc === 'object' ? tc.function : undefined;
|
|
260
|
+
if (fnObj && typeof fnObj === 'object') {
|
|
261
|
+
adapters.push({
|
|
262
|
+
name: typeof fnObj.name === 'string' ? String(fnObj.name) : undefined,
|
|
263
|
+
getArguments: () => fnObj.arguments,
|
|
264
|
+
setArguments: (value) => {
|
|
265
|
+
fnObj.arguments = value;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const output = Array.isArray(anyPayload.output) ? anyPayload.output : [];
|
|
272
|
+
for (const item of output) {
|
|
273
|
+
if (!item || typeof item !== 'object')
|
|
274
|
+
continue;
|
|
275
|
+
const type = String(item.type || '').toLowerCase();
|
|
276
|
+
if (type === 'function_call') {
|
|
277
|
+
adapters.push({
|
|
278
|
+
name: typeof item.name === 'string' ? String(item.name) : undefined,
|
|
279
|
+
getArguments: () => item.arguments,
|
|
280
|
+
setArguments: (value) => {
|
|
281
|
+
item.arguments = value;
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (type === 'message' && Array.isArray(item.tool_calls)) {
|
|
287
|
+
for (const tc of item.tool_calls) {
|
|
288
|
+
const fnObj = tc && typeof tc === 'object' ? tc.function : undefined;
|
|
289
|
+
if (!fnObj || typeof fnObj !== 'object')
|
|
290
|
+
continue;
|
|
291
|
+
adapters.push({
|
|
292
|
+
name: typeof fnObj.name === 'string' ? String(fnObj.name) : undefined,
|
|
293
|
+
getArguments: () => fnObj.arguments,
|
|
294
|
+
setArguments: (value) => {
|
|
295
|
+
fnObj.arguments = value;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return adapters;
|
|
302
|
+
}
|
|
303
|
+
function buildSingleChangePayload(record) {
|
|
304
|
+
const kindRaw = readString(record.kind);
|
|
305
|
+
if (!kindRaw)
|
|
306
|
+
return undefined;
|
|
307
|
+
const change = {
|
|
308
|
+
kind: kindRaw.toLowerCase(),
|
|
309
|
+
lines: record.lines ?? record.text ?? record.body,
|
|
310
|
+
target: readString(record.target),
|
|
311
|
+
anchor: readString(record.anchor)
|
|
312
|
+
};
|
|
313
|
+
if (typeof record.use_anchor_indent === 'boolean') {
|
|
314
|
+
change.use_anchor_indent = record.use_anchor_indent;
|
|
315
|
+
}
|
|
316
|
+
const changeFile = readString(record.file);
|
|
317
|
+
if (changeFile) {
|
|
318
|
+
change.file = changeFile;
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
file: changeFile,
|
|
322
|
+
changes: [change]
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function coerceStructuredPayload(record) {
|
|
326
|
+
if (isStructuredApplyPatchPayload(record)) {
|
|
327
|
+
return record;
|
|
328
|
+
}
|
|
329
|
+
if (Array.isArray(record.changes) && record.changes.length === 0) {
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
const single = buildSingleChangePayload(record);
|
|
333
|
+
if (single) {
|
|
334
|
+
return single;
|
|
335
|
+
}
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
function maybeNormalizeArguments(argIn) {
|
|
339
|
+
let parsed;
|
|
340
|
+
let patchText;
|
|
341
|
+
if (typeof argIn === 'string') {
|
|
342
|
+
const trimmed = argIn.trim();
|
|
343
|
+
if (!trimmed)
|
|
344
|
+
return undefined;
|
|
345
|
+
try {
|
|
346
|
+
parsed = JSON.parse(trimmed);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
if (looksLikePatch(trimmed)) {
|
|
350
|
+
patchText = trimmed;
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
const stripped = stripCodeFences(trimmed);
|
|
354
|
+
if (looksLikePatch(stripped)) {
|
|
355
|
+
patchText = stripped;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else if (isObject(argIn)) {
|
|
361
|
+
parsed = argIn;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
const record = parsed ?? {};
|
|
367
|
+
if (!patchText) {
|
|
368
|
+
const toon = readString(record.toon);
|
|
369
|
+
if (looksLikePatch(toon)) {
|
|
370
|
+
patchText = toon;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (!patchText) {
|
|
374
|
+
const payload = coerceStructuredPayload(record);
|
|
375
|
+
if (payload) {
|
|
376
|
+
try {
|
|
377
|
+
patchText = buildStructuredPatch(payload);
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
if (!(error instanceof StructuredApplyPatchError)) {
|
|
381
|
+
/* ignore */
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (!patchText) {
|
|
387
|
+
const patchField = readString(record.patch);
|
|
388
|
+
if (looksLikePatch(patchField)) {
|
|
389
|
+
patchText = patchField;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (!patchText) {
|
|
393
|
+
const inputField = readString(record.input);
|
|
394
|
+
if (looksLikePatch(inputField)) {
|
|
395
|
+
patchText = inputField;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (!patchText) {
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
const normalizedText = normalizeApplyPatchText(patchText);
|
|
402
|
+
if (!looksLikePatch(normalizedText)) {
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
return JSON.stringify({ input: normalizedText, patch: normalizedText });
|
|
406
|
+
}
|
|
20
407
|
/**
|
|
21
408
|
* Response-side apply_patch arguments 规范化(TOON + 结构化 JSON → 统一 diff 文本)。
|
|
22
409
|
*
|
|
@@ -42,94 +429,28 @@ export class ResponseApplyPatchToonDecodeFilter {
|
|
|
42
429
|
return { ok: true, data: input };
|
|
43
430
|
try {
|
|
44
431
|
const out = JSON.parse(JSON.stringify(input || {}));
|
|
45
|
-
const
|
|
46
|
-
for (const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
432
|
+
const adapters = collectFunctionCallAdapters(out);
|
|
433
|
+
for (const adapter of adapters) {
|
|
434
|
+
try {
|
|
435
|
+
const name = adapter.name ? adapter.name.trim().toLowerCase() : '';
|
|
436
|
+
if (name !== 'apply_patch') {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const normalizedArgs = maybeNormalizeArguments(adapter.getArguments());
|
|
440
|
+
if (!normalizedArgs) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
50
443
|
try {
|
|
51
|
-
|
|
52
|
-
if (!fn || typeof fn !== 'object')
|
|
53
|
-
continue;
|
|
54
|
-
const nameRaw = fn.name;
|
|
55
|
-
if (typeof nameRaw !== 'string' || nameRaw.trim().toLowerCase() !== 'apply_patch') {
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
const argIn = fn.arguments;
|
|
59
|
-
let parsed;
|
|
60
|
-
let patchText;
|
|
61
|
-
if (typeof argIn === 'string') {
|
|
62
|
-
const trimmed = argIn.trim();
|
|
63
|
-
if (!trimmed)
|
|
64
|
-
continue;
|
|
65
|
-
try {
|
|
66
|
-
// 尝试作为 JSON 解析
|
|
67
|
-
parsed = JSON.parse(trimmed);
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
// 非 JSON 字符串:如果看起来就是统一 diff,则直接视为 patch 文本
|
|
71
|
-
if (trimmed.includes('*** Begin Patch') && trimmed.includes('*** End Patch')) {
|
|
72
|
-
patchText = trimmed;
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
// 退而求其次:当作原始补丁文本传递给客户端,由本地 apply_patch 决定是否接受
|
|
76
|
-
patchText = trimmed;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
else if (isObject(argIn)) {
|
|
81
|
-
parsed = argIn;
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
if (!patchText && !isObject(parsed))
|
|
87
|
-
continue;
|
|
88
|
-
const record = isObject(parsed) ? parsed : {};
|
|
89
|
-
// 优先处理 toon: "<patch text>" 形态(兼容旧 TOON 协议)
|
|
90
|
-
if (!patchText) {
|
|
91
|
-
const toon = readString(record.toon);
|
|
92
|
-
if (toon && toon.includes('*** Begin Patch') && toon.includes('*** End Patch')) {
|
|
93
|
-
patchText = toon;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
// 处理结构化 JSON(changes 数组 → 统一 diff)
|
|
97
|
-
if (!patchText && isStructuredApplyPatchPayload(record)) {
|
|
98
|
-
try {
|
|
99
|
-
patchText = buildStructuredPatch(record);
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
if (error instanceof StructuredApplyPatchError) {
|
|
103
|
-
// 结构化 payload 无法构建补丁时,保留原始 arguments,
|
|
104
|
-
// 由下游工具或客户端根据自身策略报错;这里不吞掉错误。
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
// 处理 { patch }, { input, patch } 形态 —— Responses 入口常见形状
|
|
111
|
-
if (!patchText) {
|
|
112
|
-
const patchField = readString(record.patch);
|
|
113
|
-
const inputField = readString(record.input);
|
|
114
|
-
patchText = patchField || inputField;
|
|
115
|
-
}
|
|
116
|
-
if (!patchText) {
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
const normalized = { input: patchText, patch: patchText };
|
|
120
|
-
try {
|
|
121
|
-
fn.arguments = JSON.stringify(normalized);
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
// stringify 失败时保留原始 arguments
|
|
125
|
-
}
|
|
444
|
+
adapter.setArguments(normalizedArgs);
|
|
126
445
|
}
|
|
127
446
|
catch {
|
|
128
|
-
//
|
|
447
|
+
// ignore single adapter failures
|
|
129
448
|
}
|
|
130
449
|
}
|
|
450
|
+
catch {
|
|
451
|
+
// 单个 tool_call best-effort
|
|
452
|
+
}
|
|
131
453
|
}
|
|
132
|
-
out.choices = choices;
|
|
133
454
|
return { ok: true, data: out };
|
|
134
455
|
}
|
|
135
456
|
catch {
|