@bxb1337/windsurf-fast-context 1.1.2 → 1.1.4
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/cjs/conversion/response-converter.js +279 -24
- package/dist/cjs/conversion/response-converter.test.js +44 -17
- package/dist/cjs/model/devstral-language-model.js +10 -5
- package/dist/cjs/model/devstral-language-model.test.js +7 -7
- package/dist/conversion/response-converter.d.ts +1 -1
- package/dist/esm/conversion/response-converter.js +279 -24
- package/dist/esm/conversion/response-converter.test.js +44 -17
- package/dist/esm/model/devstral-language-model.js +10 -5
- package/dist/esm/model/devstral-language-model.test.js +7 -7
- package/package.json +1 -1
|
@@ -3,38 +3,199 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.convertResponse = convertResponse;
|
|
4
4
|
const node_zlib_1 = require("node:zlib");
|
|
5
5
|
const protobuf_js_1 = require("../protocol/protobuf.js");
|
|
6
|
+
const TOOL_CALL_PREFIX = '[TOOL_CALLS]';
|
|
7
|
+
const ARGS_PREFIX = '[ARGS]';
|
|
6
8
|
const STOP_TOKEN = '</s>';
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
function parseOpenAIToolCalls(responseText) {
|
|
10
|
+
// Support both [TOOL_CALLS]{...} and TOOL_CALLS{...} formats
|
|
11
|
+
let jsonPart = responseText;
|
|
12
|
+
if (jsonPart.startsWith('[TOOL_CALLS]')) {
|
|
13
|
+
jsonPart = jsonPart.slice('[TOOL_CALLS]'.length);
|
|
14
|
+
}
|
|
15
|
+
else if (jsonPart.startsWith('TOOL_CALLS')) {
|
|
16
|
+
jsonPart = jsonPart.slice('TOOL_CALLS'.length);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
10
19
|
return null;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
}
|
|
21
|
+
if (!jsonPart.startsWith('{') && !jsonPart.startsWith('[')) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const toolCalls = [];
|
|
25
|
+
let cursor = 0;
|
|
26
|
+
while (cursor < jsonPart.length) {
|
|
27
|
+
if (jsonPart[cursor] !== '{') {
|
|
28
|
+
cursor++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const endResult = parseBalancedEnd(jsonPart, cursor);
|
|
32
|
+
if (endResult == null) {
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
const jsonStr = jsonPart.slice(cursor, endResult);
|
|
36
|
+
cursor = endResult;
|
|
37
|
+
if (jsonStr === '{}') {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(jsonStr);
|
|
42
|
+
if (parsed.type === 'function' && parsed.function) {
|
|
43
|
+
toolCalls.push(parsed);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
26
48
|
}
|
|
27
|
-
return toolCalls.length > 0 ? toolCalls : null;
|
|
28
49
|
}
|
|
29
|
-
|
|
50
|
+
if (toolCalls.length === 0) {
|
|
30
51
|
return null;
|
|
31
52
|
}
|
|
53
|
+
return toolCalls.map((call, index) => {
|
|
54
|
+
const toolId = call.function.name;
|
|
55
|
+
const toolName = typeof toolId === 'number' ? mapToolIdToName(toolId) : String(toolId);
|
|
56
|
+
const args = call.function.parameters ?? {};
|
|
57
|
+
return {
|
|
58
|
+
type: 'tool-call',
|
|
59
|
+
toolCallId: `toolcall_${index + 1}`,
|
|
60
|
+
toolName,
|
|
61
|
+
input: JSON.stringify(args),
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function mapToolIdToName(id) {
|
|
66
|
+
switch (id) {
|
|
67
|
+
case 1:
|
|
68
|
+
return 'read';
|
|
69
|
+
case 2:
|
|
70
|
+
return 'glob';
|
|
71
|
+
case 3:
|
|
72
|
+
return 'grep';
|
|
73
|
+
default:
|
|
74
|
+
return `tool_${id}`;
|
|
75
|
+
}
|
|
32
76
|
}
|
|
33
77
|
function pushText(parts, text) {
|
|
34
78
|
if (text.length > 0) {
|
|
35
79
|
parts.push({ type: 'text', text });
|
|
36
80
|
}
|
|
37
81
|
}
|
|
82
|
+
function parseStringEnd(value, startIndex) {
|
|
83
|
+
let index = startIndex + 1;
|
|
84
|
+
let escaping = false;
|
|
85
|
+
while (index < value.length) {
|
|
86
|
+
const char = value[index];
|
|
87
|
+
if (escaping) {
|
|
88
|
+
escaping = false;
|
|
89
|
+
}
|
|
90
|
+
else if (char === '\\') {
|
|
91
|
+
escaping = true;
|
|
92
|
+
}
|
|
93
|
+
else if (char === '"') {
|
|
94
|
+
return index + 1;
|
|
95
|
+
}
|
|
96
|
+
index += 1;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
function parseBalancedEnd(value, startIndex) {
|
|
101
|
+
const stack = [value[startIndex] === '{' ? '}' : ']'];
|
|
102
|
+
let index = startIndex + 1;
|
|
103
|
+
let inString = false;
|
|
104
|
+
let escaping = false;
|
|
105
|
+
while (index < value.length) {
|
|
106
|
+
const char = value[index];
|
|
107
|
+
if (inString) {
|
|
108
|
+
if (escaping) {
|
|
109
|
+
escaping = false;
|
|
110
|
+
}
|
|
111
|
+
else if (char === '\\') {
|
|
112
|
+
escaping = true;
|
|
113
|
+
}
|
|
114
|
+
else if (char === '"') {
|
|
115
|
+
inString = false;
|
|
116
|
+
}
|
|
117
|
+
index += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (char === '"') {
|
|
121
|
+
inString = true;
|
|
122
|
+
index += 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (char === '{') {
|
|
126
|
+
stack.push('}');
|
|
127
|
+
index += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (char === '[') {
|
|
131
|
+
stack.push(']');
|
|
132
|
+
index += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (char === '}' || char === ']') {
|
|
136
|
+
const expected = stack[stack.length - 1];
|
|
137
|
+
if (expected !== char) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
stack.pop();
|
|
141
|
+
index += 1;
|
|
142
|
+
if (stack.length === 0) {
|
|
143
|
+
return index;
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
index += 1;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
function parsePrimitiveEnd(value, startIndex) {
|
|
152
|
+
let index = startIndex;
|
|
153
|
+
while (index < value.length) {
|
|
154
|
+
const char = value[index];
|
|
155
|
+
if (char === ' ' || char === '\n' || char === '\r' || char === '\t') {
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
index += 1;
|
|
159
|
+
}
|
|
160
|
+
return index;
|
|
161
|
+
}
|
|
162
|
+
function parseJsonValue(value, startIndex) {
|
|
163
|
+
let jsonStart = startIndex;
|
|
164
|
+
while (jsonStart < value.length) {
|
|
165
|
+
const char = value[jsonStart];
|
|
166
|
+
if (char !== ' ' && char !== '\n' && char !== '\r' && char !== '\t') {
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
jsonStart += 1;
|
|
170
|
+
}
|
|
171
|
+
if (jsonStart >= value.length) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const firstChar = value[jsonStart];
|
|
175
|
+
let endIndex;
|
|
176
|
+
if (firstChar === '{' || firstChar === '[') {
|
|
177
|
+
endIndex = parseBalancedEnd(value, jsonStart);
|
|
178
|
+
}
|
|
179
|
+
else if (firstChar === '"') {
|
|
180
|
+
endIndex = parseStringEnd(value, jsonStart);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
endIndex = parsePrimitiveEnd(value, jsonStart);
|
|
184
|
+
}
|
|
185
|
+
if (endIndex == null || endIndex <= jsonStart) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const rawJson = value.slice(jsonStart, endIndex);
|
|
189
|
+
try {
|
|
190
|
+
return {
|
|
191
|
+
parsed: JSON.parse(rawJson),
|
|
192
|
+
endIndex,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
38
199
|
function hasControlChars(value) {
|
|
39
200
|
for (const char of value) {
|
|
40
201
|
const code = char.charCodeAt(0);
|
|
@@ -62,6 +223,10 @@ function isLikelyMetadata(value) {
|
|
|
62
223
|
return /^[A-Za-z0-9._:-]{1,32}$/.test(value);
|
|
63
224
|
}
|
|
64
225
|
function pickBestExtractedText(values) {
|
|
226
|
+
const markerValues = values.filter((value) => value.includes(TOOL_CALL_PREFIX) || value.includes(ARGS_PREFIX));
|
|
227
|
+
if (markerValues.length > 0) {
|
|
228
|
+
return markerValues.join('');
|
|
229
|
+
}
|
|
65
230
|
const nonMetadata = values.filter((value) => !isLikelyMetadata(value));
|
|
66
231
|
const candidates = nonMetadata.length > 0 ? nonMetadata : values;
|
|
67
232
|
return candidates.reduce((best, current) => (current.length > best.length ? current : best), candidates[0] ?? '');
|
|
@@ -81,10 +246,100 @@ function decodeResponseText(buffer) {
|
|
|
81
246
|
function convertResponse(buffer) {
|
|
82
247
|
let responseText = decodeResponseText(buffer);
|
|
83
248
|
responseText = responseText.replace(STOP_TOKEN, '');
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
249
|
+
// Try parsing as pure tool calls first (for backward compat)
|
|
250
|
+
const openaiToolCalls = parseOpenAIToolCalls(responseText);
|
|
251
|
+
if (openaiToolCalls) {
|
|
252
|
+
return openaiToolCalls;
|
|
253
|
+
}
|
|
87
254
|
const parts = [];
|
|
88
|
-
|
|
255
|
+
let cursor = 0;
|
|
256
|
+
let toolCallCount = 0;
|
|
257
|
+
while (cursor < responseText.length) {
|
|
258
|
+
// Try to find [TOOL_CALLS] marker
|
|
259
|
+
const markerStart = responseText.indexOf(TOOL_CALL_PREFIX, cursor);
|
|
260
|
+
if (markerStart === -1) {
|
|
261
|
+
pushText(parts, responseText.slice(cursor));
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
pushText(parts, responseText.slice(cursor, markerStart));
|
|
265
|
+
const afterMarker = responseText.slice(markerStart + TOOL_CALL_PREFIX.length);
|
|
266
|
+
// Check if it's [TOOL_CALLS]{...} format (OpenAI-style)
|
|
267
|
+
if (afterMarker.startsWith('{') || afterMarker.startsWith('[')) {
|
|
268
|
+
const jsonResult = parseJsonValue(afterMarker, 0);
|
|
269
|
+
if (jsonResult) {
|
|
270
|
+
const parsed = jsonResult.parsed;
|
|
271
|
+
// Handle single object or array of objects
|
|
272
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
273
|
+
let foundToolCall = false;
|
|
274
|
+
for (const item of items) {
|
|
275
|
+
if (item && typeof item === 'object' && item.type === 'function' && item.function) {
|
|
276
|
+
toolCallCount += 1;
|
|
277
|
+
const toolName = typeof item.function.name === 'string'
|
|
278
|
+
? item.function.name
|
|
279
|
+
: mapToolIdToName(item.function.name);
|
|
280
|
+
parts.push({
|
|
281
|
+
type: 'tool-call',
|
|
282
|
+
toolCallId: `toolcall_${toolCallCount}`,
|
|
283
|
+
toolName,
|
|
284
|
+
input: JSON.stringify(item.function.parameters ?? {}),
|
|
285
|
+
});
|
|
286
|
+
foundToolCall = true;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (foundToolCall) {
|
|
290
|
+
cursor = markerStart + TOOL_CALL_PREFIX.length + jsonResult.endIndex;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Not a valid OpenAI format, treat as text
|
|
295
|
+
pushText(parts, responseText.slice(markerStart, markerStart + TOOL_CALL_PREFIX.length));
|
|
296
|
+
cursor = markerStart + TOOL_CALL_PREFIX.length;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
// Try [TOOL_CALLS]tool_name[ARGS]{...} format first (has ARGS marker)
|
|
300
|
+
const argsStart = responseText.indexOf(ARGS_PREFIX, markerStart + TOOL_CALL_PREFIX.length);
|
|
301
|
+
if (argsStart !== -1) {
|
|
302
|
+
const toolName = responseText.slice(markerStart + TOOL_CALL_PREFIX.length, argsStart);
|
|
303
|
+
const parsedArgs = parseJsonValue(responseText, argsStart + ARGS_PREFIX.length);
|
|
304
|
+
if (parsedArgs) {
|
|
305
|
+
toolCallCount += 1;
|
|
306
|
+
parts.push({
|
|
307
|
+
type: 'tool-call',
|
|
308
|
+
toolCallId: `toolcall_${toolCallCount}`,
|
|
309
|
+
toolName,
|
|
310
|
+
input: JSON.stringify(parsedArgs.parsed),
|
|
311
|
+
});
|
|
312
|
+
cursor = parsedArgs.endIndex;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
// Malformed ARGS, skip
|
|
316
|
+
const nextMarker = responseText.indexOf(TOOL_CALL_PREFIX, markerStart + TOOL_CALL_PREFIX.length);
|
|
317
|
+
const malformedEnd = nextMarker === -1 ? responseText.length : nextMarker;
|
|
318
|
+
pushText(parts, responseText.slice(markerStart, malformedEnd));
|
|
319
|
+
cursor = malformedEnd;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
// Try [TOOL_CALLS]tool_name{...} format (no ARGS marker)
|
|
323
|
+
const afterPrefix = responseText.slice(markerStart + TOOL_CALL_PREFIX.length);
|
|
324
|
+
const jsonStart = afterPrefix.indexOf('{');
|
|
325
|
+
if (jsonStart !== -1) {
|
|
326
|
+
const toolName = afterPrefix.slice(0, jsonStart).trim();
|
|
327
|
+
const parsedArgs = parseJsonValue(afterPrefix, jsonStart);
|
|
328
|
+
if (parsedArgs && toolName) {
|
|
329
|
+
toolCallCount += 1;
|
|
330
|
+
parts.push({
|
|
331
|
+
type: 'tool-call',
|
|
332
|
+
toolCallId: `toolcall_${toolCallCount}`,
|
|
333
|
+
toolName,
|
|
334
|
+
input: JSON.stringify(parsedArgs.parsed),
|
|
335
|
+
});
|
|
336
|
+
cursor = markerStart + TOOL_CALL_PREFIX.length + parsedArgs.endIndex;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// No valid format found
|
|
341
|
+
pushText(parts, responseText.slice(markerStart));
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
89
344
|
return parts;
|
|
90
345
|
}
|
|
@@ -33,38 +33,65 @@ const response_converter_js_1 = require("./response-converter.js");
|
|
|
33
33
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
34
34
|
(0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Hello world' }]);
|
|
35
35
|
});
|
|
36
|
-
//
|
|
37
|
-
(0, vitest_1.it)('parses
|
|
38
|
-
const input = '[
|
|
36
|
+
// Native Windsurf marker format tests
|
|
37
|
+
(0, vitest_1.it)('parses native marker format [TOOL_CALLS]name[ARGS]{json}', () => {
|
|
38
|
+
const input = '[TOOL_CALLS]search[ARGS]{"q":"test"}';
|
|
39
39
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
40
40
|
(0, vitest_1.expect)(result).toEqual([
|
|
41
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'search', input: {
|
|
41
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'search', input: '{"q":"test"}' }
|
|
42
42
|
]);
|
|
43
43
|
});
|
|
44
|
-
(0, vitest_1.it)('parses
|
|
45
|
-
const input = '[
|
|
44
|
+
(0, vitest_1.it)('parses marker format with text before tool call', () => {
|
|
45
|
+
const input = 'Some text before[TOOL_CALLS]read[ARGS]{"path":"/foo"}';
|
|
46
46
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
47
47
|
(0, vitest_1.expect)(result).toEqual([
|
|
48
|
-
{ type: '
|
|
49
|
-
{ type: 'tool-call', toolCallId: '
|
|
48
|
+
{ type: 'text', text: 'Some text before' },
|
|
49
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: '{"path":"/foo"}' }
|
|
50
50
|
]);
|
|
51
51
|
});
|
|
52
|
-
(0, vitest_1.it)('parses
|
|
53
|
-
const input = '[{"
|
|
52
|
+
(0, vitest_1.it)('parses marker format with text after tool call', () => {
|
|
53
|
+
const input = '[TOOL_CALLS]grep[ARGS]{"pattern":"foo"}Some text after';
|
|
54
54
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
55
55
|
(0, vitest_1.expect)(result).toEqual([
|
|
56
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: '
|
|
56
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: '{"pattern":"foo"}' },
|
|
57
|
+
{ type: 'text', text: 'Some text after' }
|
|
57
58
|
]);
|
|
58
59
|
});
|
|
59
|
-
(0, vitest_1.it)('
|
|
60
|
-
const input = '[TOOL_CALLS]
|
|
60
|
+
(0, vitest_1.it)('parses multiple tool calls in marker format', () => {
|
|
61
|
+
const input = '[TOOL_CALLS]read[ARGS]{"path":"/a"}[TOOL_CALLS]grep[ARGS]{"pattern":"foo"}';
|
|
61
62
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
62
|
-
(0, vitest_1.expect)(result).toEqual([
|
|
63
|
+
(0, vitest_1.expect)(result).toEqual([
|
|
64
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: '{"path":"/a"}' },
|
|
65
|
+
{ type: 'tool-call', toolCallId: 'toolcall_2', toolName: 'grep', input: '{"pattern":"foo"}' }
|
|
66
|
+
]);
|
|
63
67
|
});
|
|
64
|
-
(0, vitest_1.it)('
|
|
65
|
-
const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"
|
|
68
|
+
(0, vitest_1.it)('parses TOOL_CALLS format with numeric tool id', () => {
|
|
69
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"pattern":"test"}}}';
|
|
66
70
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
67
|
-
(0, vitest_1.expect)(result).toEqual([
|
|
71
|
+
(0, vitest_1.expect)(result).toEqual([
|
|
72
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: '{"pattern":"test"}' }
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
(0, vitest_1.it)('maps tool id 1 to read', () => {
|
|
76
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":1,"parameters":{"path":"/file"}}}';
|
|
77
|
+
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
78
|
+
(0, vitest_1.expect)(result).toEqual([
|
|
79
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: '{"path":"/file"}' }
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
(0, vitest_1.it)('maps tool id 2 to glob', () => {
|
|
83
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":2,"parameters":{"pattern":"*.ts"}}}';
|
|
84
|
+
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
85
|
+
(0, vitest_1.expect)(result).toEqual([
|
|
86
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'glob', input: '{"pattern":"*.ts"}' }
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
(0, vitest_1.it)('maps unknown tool id to tool_N', () => {
|
|
90
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":99,"parameters":{}}}';
|
|
91
|
+
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
92
|
+
(0, vitest_1.expect)(result).toEqual([
|
|
93
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'tool_99', input: '{}' }
|
|
94
|
+
]);
|
|
68
95
|
});
|
|
69
96
|
(0, vitest_1.it)('returns text for non-array JSON', () => {
|
|
70
97
|
const input = '{"type":"function","function":{"name":"search","parameters":{}}}';
|
|
@@ -20,13 +20,18 @@ const WS_APP_VER = process.env.WS_APP_VER ?? '1.48.2';
|
|
|
20
20
|
const WS_LS_VER = process.env.WS_LS_VER ?? '1.9544.35';
|
|
21
21
|
const SENTRY_PUBLIC_KEY = 'b813f73488da69eedec534dba1029111';
|
|
22
22
|
const CONNECT_USER_AGENT = 'connect-go/1.18.1 (go1.25.5)';
|
|
23
|
-
const TOOL_FORMAT_INSTRUCTION = `When
|
|
23
|
+
const TOOL_FORMAT_INSTRUCTION = `CRITICAL: When using tools, output ONLY a JSON array (no text before or after). Use this EXACT format:
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
[{"type": "function", "function": {"name": "tool_name", "parameters": {"arg1": "value1"}}}]
|
|
25
|
+
[{"type":"function","function":{"name":"tool_name","parameters":{"arg":"value"}}}]
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
RULES:
|
|
28
|
+
- Start with [ and end with ]
|
|
29
|
+
- Each tool call has: type="function", function.name (string), function.parameters (object)
|
|
30
|
+
- NO markdown, NO code blocks, NO [TOOL_CALLS] markers
|
|
31
|
+
- Use the EXACT parameter names from the tool definition
|
|
32
|
+
|
|
33
|
+
Example for calculator(a, b):
|
|
34
|
+
[{"type":"function","function":{"name":"calculator","parameters":{"a":15,"b":27}}}]`;
|
|
30
35
|
class DevstralLanguageModel {
|
|
31
36
|
specificationVersion = 'v2';
|
|
32
37
|
provider = 'windsurf';
|
|
@@ -163,7 +163,7 @@ async function collectStreamParts(stream) {
|
|
|
163
163
|
return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
|
|
164
164
|
}
|
|
165
165
|
requestBodies.push(bufferFromBody(init?.body));
|
|
166
|
-
const payload = Buffer.from('[
|
|
166
|
+
const payload = Buffer.from('[TOOL_CALLS]searchRepo[ARGS]{"query":"jwt manager"}', 'utf8');
|
|
167
167
|
return new Response(Uint8Array.from((0, connect_frame_js_1.connectFrameEncode)(payload)), { status: 200 });
|
|
168
168
|
};
|
|
169
169
|
const model = new devstral_language_model_js_1.DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
|
|
@@ -304,9 +304,9 @@ async function collectStreamParts(stream) {
|
|
|
304
304
|
});
|
|
305
305
|
const strings = (0, protobuf_js_1.extractStrings)(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
306
306
|
const combined = strings.join('\n');
|
|
307
|
-
(0, vitest_1.expect)(combined).toContain('When
|
|
307
|
+
(0, vitest_1.expect)(combined).toContain('CRITICAL: When using tools');
|
|
308
308
|
});
|
|
309
|
-
(0, vitest_1.it)('does not inject tool format instruction when no tools
|
|
309
|
+
(0, vitest_1.it)('does not inject tool format instruction when no tools present', async () => {
|
|
310
310
|
const requestBodies = [];
|
|
311
311
|
const jwt = makeJwt(4_200_000_101, 'no-instruction');
|
|
312
312
|
const fakeFetch = async (input, init) => {
|
|
@@ -323,9 +323,9 @@ async function collectStreamParts(stream) {
|
|
|
323
323
|
});
|
|
324
324
|
const strings = (0, protobuf_js_1.extractStrings)(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
325
325
|
const combined = strings.join('\n');
|
|
326
|
-
(0, vitest_1.expect)(combined).not.toContain('When
|
|
326
|
+
(0, vitest_1.expect)(combined).not.toContain('CRITICAL: When using tools');
|
|
327
327
|
});
|
|
328
|
-
(0, vitest_1.it)('
|
|
328
|
+
(0, vitest_1.it)('injects tool format instruction for function tools only, not provider-defined', async () => {
|
|
329
329
|
const requestBodies = [];
|
|
330
330
|
const jwt = makeJwt(4_200_000_102, 'provider-only');
|
|
331
331
|
const fakeFetch = async (input, init) => {
|
|
@@ -350,7 +350,7 @@ async function collectStreamParts(stream) {
|
|
|
350
350
|
});
|
|
351
351
|
const strings = (0, protobuf_js_1.extractStrings)(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
352
352
|
const combined = strings.join('\n');
|
|
353
|
-
(0, vitest_1.expect)(combined).not.toContain('When
|
|
353
|
+
(0, vitest_1.expect)(combined).not.toContain('CRITICAL: When using tools');
|
|
354
354
|
});
|
|
355
355
|
});
|
|
356
356
|
(0, vitest_1.describe)('DevstralLanguageModel doStream', () => {
|
|
@@ -456,7 +456,7 @@ async function collectStreamParts(stream) {
|
|
|
456
456
|
});
|
|
457
457
|
(0, vitest_1.it)('stream-tool emits tool-input deltas before final tool-call and finish', async () => {
|
|
458
458
|
const jwt = makeJwt(4_300_000_001, 'stream-tool');
|
|
459
|
-
const toolPayload = Buffer.from('[
|
|
459
|
+
const toolPayload = Buffer.from('[TOOL_CALLS]searchRepo[ARGS]{"query":"jwt manager"}', 'utf8');
|
|
460
460
|
const frames = Buffer.concat([(0, connect_frame_js_1.connectFrameEncode)(toolPayload)]);
|
|
461
461
|
const fakeFetch = async (input) => {
|
|
462
462
|
const url = String(input);
|
|
@@ -6,7 +6,7 @@ export interface ToolCallPart {
|
|
|
6
6
|
type: 'tool-call';
|
|
7
7
|
toolCallId: string;
|
|
8
8
|
toolName: string;
|
|
9
|
-
input:
|
|
9
|
+
input: string;
|
|
10
10
|
}
|
|
11
11
|
export type LanguageModelV2Content = TextPart | ToolCallPart;
|
|
12
12
|
export declare function convertResponse(buffer: Buffer): LanguageModelV2Content[];
|
|
@@ -1,37 +1,198 @@
|
|
|
1
1
|
import { gunzipSync } from 'node:zlib';
|
|
2
2
|
import { extractStrings } from '../protocol/protobuf.js';
|
|
3
|
+
const TOOL_CALL_PREFIX = '[TOOL_CALLS]';
|
|
4
|
+
const ARGS_PREFIX = '[ARGS]';
|
|
3
5
|
const STOP_TOKEN = '</s>';
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
function parseOpenAIToolCalls(responseText) {
|
|
7
|
+
// Support both [TOOL_CALLS]{...} and TOOL_CALLS{...} formats
|
|
8
|
+
let jsonPart = responseText;
|
|
9
|
+
if (jsonPart.startsWith('[TOOL_CALLS]')) {
|
|
10
|
+
jsonPart = jsonPart.slice('[TOOL_CALLS]'.length);
|
|
11
|
+
}
|
|
12
|
+
else if (jsonPart.startsWith('TOOL_CALLS')) {
|
|
13
|
+
jsonPart = jsonPart.slice('TOOL_CALLS'.length);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
7
16
|
return null;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
}
|
|
18
|
+
if (!jsonPart.startsWith('{') && !jsonPart.startsWith('[')) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const toolCalls = [];
|
|
22
|
+
let cursor = 0;
|
|
23
|
+
while (cursor < jsonPart.length) {
|
|
24
|
+
if (jsonPart[cursor] !== '{') {
|
|
25
|
+
cursor++;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const endResult = parseBalancedEnd(jsonPart, cursor);
|
|
29
|
+
if (endResult == null) {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
const jsonStr = jsonPart.slice(cursor, endResult);
|
|
33
|
+
cursor = endResult;
|
|
34
|
+
if (jsonStr === '{}') {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(jsonStr);
|
|
39
|
+
if (parsed.type === 'function' && parsed.function) {
|
|
40
|
+
toolCalls.push(parsed);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue;
|
|
23
45
|
}
|
|
24
|
-
return toolCalls.length > 0 ? toolCalls : null;
|
|
25
46
|
}
|
|
26
|
-
|
|
47
|
+
if (toolCalls.length === 0) {
|
|
27
48
|
return null;
|
|
28
49
|
}
|
|
50
|
+
return toolCalls.map((call, index) => {
|
|
51
|
+
const toolId = call.function.name;
|
|
52
|
+
const toolName = typeof toolId === 'number' ? mapToolIdToName(toolId) : String(toolId);
|
|
53
|
+
const args = call.function.parameters ?? {};
|
|
54
|
+
return {
|
|
55
|
+
type: 'tool-call',
|
|
56
|
+
toolCallId: `toolcall_${index + 1}`,
|
|
57
|
+
toolName,
|
|
58
|
+
input: JSON.stringify(args),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function mapToolIdToName(id) {
|
|
63
|
+
switch (id) {
|
|
64
|
+
case 1:
|
|
65
|
+
return 'read';
|
|
66
|
+
case 2:
|
|
67
|
+
return 'glob';
|
|
68
|
+
case 3:
|
|
69
|
+
return 'grep';
|
|
70
|
+
default:
|
|
71
|
+
return `tool_${id}`;
|
|
72
|
+
}
|
|
29
73
|
}
|
|
30
74
|
function pushText(parts, text) {
|
|
31
75
|
if (text.length > 0) {
|
|
32
76
|
parts.push({ type: 'text', text });
|
|
33
77
|
}
|
|
34
78
|
}
|
|
79
|
+
function parseStringEnd(value, startIndex) {
|
|
80
|
+
let index = startIndex + 1;
|
|
81
|
+
let escaping = false;
|
|
82
|
+
while (index < value.length) {
|
|
83
|
+
const char = value[index];
|
|
84
|
+
if (escaping) {
|
|
85
|
+
escaping = false;
|
|
86
|
+
}
|
|
87
|
+
else if (char === '\\') {
|
|
88
|
+
escaping = true;
|
|
89
|
+
}
|
|
90
|
+
else if (char === '"') {
|
|
91
|
+
return index + 1;
|
|
92
|
+
}
|
|
93
|
+
index += 1;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function parseBalancedEnd(value, startIndex) {
|
|
98
|
+
const stack = [value[startIndex] === '{' ? '}' : ']'];
|
|
99
|
+
let index = startIndex + 1;
|
|
100
|
+
let inString = false;
|
|
101
|
+
let escaping = false;
|
|
102
|
+
while (index < value.length) {
|
|
103
|
+
const char = value[index];
|
|
104
|
+
if (inString) {
|
|
105
|
+
if (escaping) {
|
|
106
|
+
escaping = false;
|
|
107
|
+
}
|
|
108
|
+
else if (char === '\\') {
|
|
109
|
+
escaping = true;
|
|
110
|
+
}
|
|
111
|
+
else if (char === '"') {
|
|
112
|
+
inString = false;
|
|
113
|
+
}
|
|
114
|
+
index += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (char === '"') {
|
|
118
|
+
inString = true;
|
|
119
|
+
index += 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (char === '{') {
|
|
123
|
+
stack.push('}');
|
|
124
|
+
index += 1;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (char === '[') {
|
|
128
|
+
stack.push(']');
|
|
129
|
+
index += 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (char === '}' || char === ']') {
|
|
133
|
+
const expected = stack[stack.length - 1];
|
|
134
|
+
if (expected !== char) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
stack.pop();
|
|
138
|
+
index += 1;
|
|
139
|
+
if (stack.length === 0) {
|
|
140
|
+
return index;
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
index += 1;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
function parsePrimitiveEnd(value, startIndex) {
|
|
149
|
+
let index = startIndex;
|
|
150
|
+
while (index < value.length) {
|
|
151
|
+
const char = value[index];
|
|
152
|
+
if (char === ' ' || char === '\n' || char === '\r' || char === '\t') {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
index += 1;
|
|
156
|
+
}
|
|
157
|
+
return index;
|
|
158
|
+
}
|
|
159
|
+
function parseJsonValue(value, startIndex) {
|
|
160
|
+
let jsonStart = startIndex;
|
|
161
|
+
while (jsonStart < value.length) {
|
|
162
|
+
const char = value[jsonStart];
|
|
163
|
+
if (char !== ' ' && char !== '\n' && char !== '\r' && char !== '\t') {
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
jsonStart += 1;
|
|
167
|
+
}
|
|
168
|
+
if (jsonStart >= value.length) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const firstChar = value[jsonStart];
|
|
172
|
+
let endIndex;
|
|
173
|
+
if (firstChar === '{' || firstChar === '[') {
|
|
174
|
+
endIndex = parseBalancedEnd(value, jsonStart);
|
|
175
|
+
}
|
|
176
|
+
else if (firstChar === '"') {
|
|
177
|
+
endIndex = parseStringEnd(value, jsonStart);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
endIndex = parsePrimitiveEnd(value, jsonStart);
|
|
181
|
+
}
|
|
182
|
+
if (endIndex == null || endIndex <= jsonStart) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const rawJson = value.slice(jsonStart, endIndex);
|
|
186
|
+
try {
|
|
187
|
+
return {
|
|
188
|
+
parsed: JSON.parse(rawJson),
|
|
189
|
+
endIndex,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
35
196
|
function hasControlChars(value) {
|
|
36
197
|
for (const char of value) {
|
|
37
198
|
const code = char.charCodeAt(0);
|
|
@@ -59,6 +220,10 @@ function isLikelyMetadata(value) {
|
|
|
59
220
|
return /^[A-Za-z0-9._:-]{1,32}$/.test(value);
|
|
60
221
|
}
|
|
61
222
|
function pickBestExtractedText(values) {
|
|
223
|
+
const markerValues = values.filter((value) => value.includes(TOOL_CALL_PREFIX) || value.includes(ARGS_PREFIX));
|
|
224
|
+
if (markerValues.length > 0) {
|
|
225
|
+
return markerValues.join('');
|
|
226
|
+
}
|
|
62
227
|
const nonMetadata = values.filter((value) => !isLikelyMetadata(value));
|
|
63
228
|
const candidates = nonMetadata.length > 0 ? nonMetadata : values;
|
|
64
229
|
return candidates.reduce((best, current) => (current.length > best.length ? current : best), candidates[0] ?? '');
|
|
@@ -78,10 +243,100 @@ function decodeResponseText(buffer) {
|
|
|
78
243
|
export function convertResponse(buffer) {
|
|
79
244
|
let responseText = decodeResponseText(buffer);
|
|
80
245
|
responseText = responseText.replace(STOP_TOKEN, '');
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
246
|
+
// Try parsing as pure tool calls first (for backward compat)
|
|
247
|
+
const openaiToolCalls = parseOpenAIToolCalls(responseText);
|
|
248
|
+
if (openaiToolCalls) {
|
|
249
|
+
return openaiToolCalls;
|
|
250
|
+
}
|
|
84
251
|
const parts = [];
|
|
85
|
-
|
|
252
|
+
let cursor = 0;
|
|
253
|
+
let toolCallCount = 0;
|
|
254
|
+
while (cursor < responseText.length) {
|
|
255
|
+
// Try to find [TOOL_CALLS] marker
|
|
256
|
+
const markerStart = responseText.indexOf(TOOL_CALL_PREFIX, cursor);
|
|
257
|
+
if (markerStart === -1) {
|
|
258
|
+
pushText(parts, responseText.slice(cursor));
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
pushText(parts, responseText.slice(cursor, markerStart));
|
|
262
|
+
const afterMarker = responseText.slice(markerStart + TOOL_CALL_PREFIX.length);
|
|
263
|
+
// Check if it's [TOOL_CALLS]{...} format (OpenAI-style)
|
|
264
|
+
if (afterMarker.startsWith('{') || afterMarker.startsWith('[')) {
|
|
265
|
+
const jsonResult = parseJsonValue(afterMarker, 0);
|
|
266
|
+
if (jsonResult) {
|
|
267
|
+
const parsed = jsonResult.parsed;
|
|
268
|
+
// Handle single object or array of objects
|
|
269
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
270
|
+
let foundToolCall = false;
|
|
271
|
+
for (const item of items) {
|
|
272
|
+
if (item && typeof item === 'object' && item.type === 'function' && item.function) {
|
|
273
|
+
toolCallCount += 1;
|
|
274
|
+
const toolName = typeof item.function.name === 'string'
|
|
275
|
+
? item.function.name
|
|
276
|
+
: mapToolIdToName(item.function.name);
|
|
277
|
+
parts.push({
|
|
278
|
+
type: 'tool-call',
|
|
279
|
+
toolCallId: `toolcall_${toolCallCount}`,
|
|
280
|
+
toolName,
|
|
281
|
+
input: JSON.stringify(item.function.parameters ?? {}),
|
|
282
|
+
});
|
|
283
|
+
foundToolCall = true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (foundToolCall) {
|
|
287
|
+
cursor = markerStart + TOOL_CALL_PREFIX.length + jsonResult.endIndex;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Not a valid OpenAI format, treat as text
|
|
292
|
+
pushText(parts, responseText.slice(markerStart, markerStart + TOOL_CALL_PREFIX.length));
|
|
293
|
+
cursor = markerStart + TOOL_CALL_PREFIX.length;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
// Try [TOOL_CALLS]tool_name[ARGS]{...} format first (has ARGS marker)
|
|
297
|
+
const argsStart = responseText.indexOf(ARGS_PREFIX, markerStart + TOOL_CALL_PREFIX.length);
|
|
298
|
+
if (argsStart !== -1) {
|
|
299
|
+
const toolName = responseText.slice(markerStart + TOOL_CALL_PREFIX.length, argsStart);
|
|
300
|
+
const parsedArgs = parseJsonValue(responseText, argsStart + ARGS_PREFIX.length);
|
|
301
|
+
if (parsedArgs) {
|
|
302
|
+
toolCallCount += 1;
|
|
303
|
+
parts.push({
|
|
304
|
+
type: 'tool-call',
|
|
305
|
+
toolCallId: `toolcall_${toolCallCount}`,
|
|
306
|
+
toolName,
|
|
307
|
+
input: JSON.stringify(parsedArgs.parsed),
|
|
308
|
+
});
|
|
309
|
+
cursor = parsedArgs.endIndex;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
// Malformed ARGS, skip
|
|
313
|
+
const nextMarker = responseText.indexOf(TOOL_CALL_PREFIX, markerStart + TOOL_CALL_PREFIX.length);
|
|
314
|
+
const malformedEnd = nextMarker === -1 ? responseText.length : nextMarker;
|
|
315
|
+
pushText(parts, responseText.slice(markerStart, malformedEnd));
|
|
316
|
+
cursor = malformedEnd;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
// Try [TOOL_CALLS]tool_name{...} format (no ARGS marker)
|
|
320
|
+
const afterPrefix = responseText.slice(markerStart + TOOL_CALL_PREFIX.length);
|
|
321
|
+
const jsonStart = afterPrefix.indexOf('{');
|
|
322
|
+
if (jsonStart !== -1) {
|
|
323
|
+
const toolName = afterPrefix.slice(0, jsonStart).trim();
|
|
324
|
+
const parsedArgs = parseJsonValue(afterPrefix, jsonStart);
|
|
325
|
+
if (parsedArgs && toolName) {
|
|
326
|
+
toolCallCount += 1;
|
|
327
|
+
parts.push({
|
|
328
|
+
type: 'tool-call',
|
|
329
|
+
toolCallId: `toolcall_${toolCallCount}`,
|
|
330
|
+
toolName,
|
|
331
|
+
input: JSON.stringify(parsedArgs.parsed),
|
|
332
|
+
});
|
|
333
|
+
cursor = markerStart + TOOL_CALL_PREFIX.length + parsedArgs.endIndex;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// No valid format found
|
|
338
|
+
pushText(parts, responseText.slice(markerStart));
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
86
341
|
return parts;
|
|
87
342
|
}
|
|
@@ -31,38 +31,65 @@ describe('convertResponse', () => {
|
|
|
31
31
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
32
32
|
expect(result).toEqual([{ type: 'text', text: 'Hello world' }]);
|
|
33
33
|
});
|
|
34
|
-
//
|
|
35
|
-
it('parses
|
|
36
|
-
const input = '[
|
|
34
|
+
// Native Windsurf marker format tests
|
|
35
|
+
it('parses native marker format [TOOL_CALLS]name[ARGS]{json}', () => {
|
|
36
|
+
const input = '[TOOL_CALLS]search[ARGS]{"q":"test"}';
|
|
37
37
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
38
38
|
expect(result).toEqual([
|
|
39
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'search', input: {
|
|
39
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'search', input: '{"q":"test"}' }
|
|
40
40
|
]);
|
|
41
41
|
});
|
|
42
|
-
it('parses
|
|
43
|
-
const input = '[
|
|
42
|
+
it('parses marker format with text before tool call', () => {
|
|
43
|
+
const input = 'Some text before[TOOL_CALLS]read[ARGS]{"path":"/foo"}';
|
|
44
44
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
45
45
|
expect(result).toEqual([
|
|
46
|
-
{ type: '
|
|
47
|
-
{ type: 'tool-call', toolCallId: '
|
|
46
|
+
{ type: 'text', text: 'Some text before' },
|
|
47
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: '{"path":"/foo"}' }
|
|
48
48
|
]);
|
|
49
49
|
});
|
|
50
|
-
it('parses
|
|
51
|
-
const input = '[{"
|
|
50
|
+
it('parses marker format with text after tool call', () => {
|
|
51
|
+
const input = '[TOOL_CALLS]grep[ARGS]{"pattern":"foo"}Some text after';
|
|
52
52
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
53
53
|
expect(result).toEqual([
|
|
54
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: '
|
|
54
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: '{"pattern":"foo"}' },
|
|
55
|
+
{ type: 'text', text: 'Some text after' }
|
|
55
56
|
]);
|
|
56
57
|
});
|
|
57
|
-
it('
|
|
58
|
-
const input = '[TOOL_CALLS]
|
|
58
|
+
it('parses multiple tool calls in marker format', () => {
|
|
59
|
+
const input = '[TOOL_CALLS]read[ARGS]{"path":"/a"}[TOOL_CALLS]grep[ARGS]{"pattern":"foo"}';
|
|
59
60
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
60
|
-
expect(result).toEqual([
|
|
61
|
+
expect(result).toEqual([
|
|
62
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: '{"path":"/a"}' },
|
|
63
|
+
{ type: 'tool-call', toolCallId: 'toolcall_2', toolName: 'grep', input: '{"pattern":"foo"}' }
|
|
64
|
+
]);
|
|
61
65
|
});
|
|
62
|
-
it('
|
|
63
|
-
const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"
|
|
66
|
+
it('parses TOOL_CALLS format with numeric tool id', () => {
|
|
67
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"pattern":"test"}}}';
|
|
64
68
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
65
|
-
expect(result).toEqual([
|
|
69
|
+
expect(result).toEqual([
|
|
70
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: '{"pattern":"test"}' }
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
it('maps tool id 1 to read', () => {
|
|
74
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":1,"parameters":{"path":"/file"}}}';
|
|
75
|
+
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
76
|
+
expect(result).toEqual([
|
|
77
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: '{"path":"/file"}' }
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
it('maps tool id 2 to glob', () => {
|
|
81
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":2,"parameters":{"pattern":"*.ts"}}}';
|
|
82
|
+
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
83
|
+
expect(result).toEqual([
|
|
84
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'glob', input: '{"pattern":"*.ts"}' }
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
it('maps unknown tool id to tool_N', () => {
|
|
88
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":99,"parameters":{}}}';
|
|
89
|
+
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
90
|
+
expect(result).toEqual([
|
|
91
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'tool_99', input: '{}' }
|
|
92
|
+
]);
|
|
66
93
|
});
|
|
67
94
|
it('returns text for non-array JSON', () => {
|
|
68
95
|
const input = '{"type":"function","function":{"name":"search","parameters":{}}}';
|
|
@@ -17,13 +17,18 @@ const WS_APP_VER = process.env.WS_APP_VER ?? '1.48.2';
|
|
|
17
17
|
const WS_LS_VER = process.env.WS_LS_VER ?? '1.9544.35';
|
|
18
18
|
const SENTRY_PUBLIC_KEY = 'b813f73488da69eedec534dba1029111';
|
|
19
19
|
const CONNECT_USER_AGENT = 'connect-go/1.18.1 (go1.25.5)';
|
|
20
|
-
const TOOL_FORMAT_INSTRUCTION = `When
|
|
20
|
+
const TOOL_FORMAT_INSTRUCTION = `CRITICAL: When using tools, output ONLY a JSON array (no text before or after). Use this EXACT format:
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
[{"type": "function", "function": {"name": "tool_name", "parameters": {"arg1": "value1"}}}]
|
|
22
|
+
[{"type":"function","function":{"name":"tool_name","parameters":{"arg":"value"}}}]
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
RULES:
|
|
25
|
+
- Start with [ and end with ]
|
|
26
|
+
- Each tool call has: type="function", function.name (string), function.parameters (object)
|
|
27
|
+
- NO markdown, NO code blocks, NO [TOOL_CALLS] markers
|
|
28
|
+
- Use the EXACT parameter names from the tool definition
|
|
29
|
+
|
|
30
|
+
Example for calculator(a, b):
|
|
31
|
+
[{"type":"function","function":{"name":"calculator","parameters":{"a":15,"b":27}}}]`;
|
|
27
32
|
export class DevstralLanguageModel {
|
|
28
33
|
specificationVersion = 'v2';
|
|
29
34
|
provider = 'windsurf';
|
|
@@ -161,7 +161,7 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
161
161
|
return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
|
|
162
162
|
}
|
|
163
163
|
requestBodies.push(bufferFromBody(init?.body));
|
|
164
|
-
const payload = Buffer.from('[
|
|
164
|
+
const payload = Buffer.from('[TOOL_CALLS]searchRepo[ARGS]{"query":"jwt manager"}', 'utf8');
|
|
165
165
|
return new Response(Uint8Array.from(connectFrameEncode(payload)), { status: 200 });
|
|
166
166
|
};
|
|
167
167
|
const model = new DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
|
|
@@ -302,9 +302,9 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
302
302
|
});
|
|
303
303
|
const strings = extractStrings(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
304
304
|
const combined = strings.join('\n');
|
|
305
|
-
expect(combined).toContain('When
|
|
305
|
+
expect(combined).toContain('CRITICAL: When using tools');
|
|
306
306
|
});
|
|
307
|
-
it('does not inject tool format instruction when no tools
|
|
307
|
+
it('does not inject tool format instruction when no tools present', async () => {
|
|
308
308
|
const requestBodies = [];
|
|
309
309
|
const jwt = makeJwt(4_200_000_101, 'no-instruction');
|
|
310
310
|
const fakeFetch = async (input, init) => {
|
|
@@ -321,9 +321,9 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
321
321
|
});
|
|
322
322
|
const strings = extractStrings(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
323
323
|
const combined = strings.join('\n');
|
|
324
|
-
expect(combined).not.toContain('When
|
|
324
|
+
expect(combined).not.toContain('CRITICAL: When using tools');
|
|
325
325
|
});
|
|
326
|
-
it('
|
|
326
|
+
it('injects tool format instruction for function tools only, not provider-defined', async () => {
|
|
327
327
|
const requestBodies = [];
|
|
328
328
|
const jwt = makeJwt(4_200_000_102, 'provider-only');
|
|
329
329
|
const fakeFetch = async (input, init) => {
|
|
@@ -348,7 +348,7 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
348
348
|
});
|
|
349
349
|
const strings = extractStrings(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
350
350
|
const combined = strings.join('\n');
|
|
351
|
-
expect(combined).not.toContain('When
|
|
351
|
+
expect(combined).not.toContain('CRITICAL: When using tools');
|
|
352
352
|
});
|
|
353
353
|
});
|
|
354
354
|
describe('DevstralLanguageModel doStream', () => {
|
|
@@ -454,7 +454,7 @@ describe('DevstralLanguageModel doStream', () => {
|
|
|
454
454
|
});
|
|
455
455
|
it('stream-tool emits tool-input deltas before final tool-call and finish', async () => {
|
|
456
456
|
const jwt = makeJwt(4_300_000_001, 'stream-tool');
|
|
457
|
-
const toolPayload = Buffer.from('[
|
|
457
|
+
const toolPayload = Buffer.from('[TOOL_CALLS]searchRepo[ARGS]{"query":"jwt manager"}', 'utf8');
|
|
458
458
|
const frames = Buffer.concat([connectFrameEncode(toolPayload)]);
|
|
459
459
|
const fakeFetch = async (input) => {
|
|
460
460
|
const url = String(input);
|