@bxb1337/windsurf-fast-context 1.1.3 → 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 +84 -20
- package/dist/cjs/conversion/response-converter.test.js +9 -9
- package/dist/cjs/model/devstral-language-model.js +13 -6
- package/dist/cjs/model/devstral-language-model.test.js +5 -5
- package/dist/conversion/response-converter.d.ts +1 -1
- package/dist/esm/conversion/response-converter.js +84 -20
- package/dist/esm/conversion/response-converter.test.js +9 -9
- package/dist/esm/model/devstral-language-model.js +13 -6
- package/dist/esm/model/devstral-language-model.test.js +5 -5
- package/package.json +1 -1
|
@@ -7,11 +7,18 @@ const TOOL_CALL_PREFIX = '[TOOL_CALLS]';
|
|
|
7
7
|
const ARGS_PREFIX = '[ARGS]';
|
|
8
8
|
const STOP_TOKEN = '</s>';
|
|
9
9
|
function parseOpenAIToolCalls(responseText) {
|
|
10
|
-
|
|
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 {
|
|
11
19
|
return null;
|
|
12
20
|
}
|
|
13
|
-
|
|
14
|
-
if (!jsonPart.startsWith('{')) {
|
|
21
|
+
if (!jsonPart.startsWith('{') && !jsonPart.startsWith('[')) {
|
|
15
22
|
return null;
|
|
16
23
|
}
|
|
17
24
|
const toolCalls = [];
|
|
@@ -51,7 +58,7 @@ function parseOpenAIToolCalls(responseText) {
|
|
|
51
58
|
type: 'tool-call',
|
|
52
59
|
toolCallId: `toolcall_${index + 1}`,
|
|
53
60
|
toolName,
|
|
54
|
-
input: args,
|
|
61
|
+
input: JSON.stringify(args),
|
|
55
62
|
};
|
|
56
63
|
});
|
|
57
64
|
}
|
|
@@ -239,6 +246,7 @@ function decodeResponseText(buffer) {
|
|
|
239
246
|
function convertResponse(buffer) {
|
|
240
247
|
let responseText = decodeResponseText(buffer);
|
|
241
248
|
responseText = responseText.replace(STOP_TOKEN, '');
|
|
249
|
+
// Try parsing as pure tool calls first (for backward compat)
|
|
242
250
|
const openaiToolCalls = parseOpenAIToolCalls(responseText);
|
|
243
251
|
if (openaiToolCalls) {
|
|
244
252
|
return openaiToolCalls;
|
|
@@ -247,35 +255,91 @@ function convertResponse(buffer) {
|
|
|
247
255
|
let cursor = 0;
|
|
248
256
|
let toolCallCount = 0;
|
|
249
257
|
while (cursor < responseText.length) {
|
|
258
|
+
// Try to find [TOOL_CALLS] marker
|
|
250
259
|
const markerStart = responseText.indexOf(TOOL_CALL_PREFIX, cursor);
|
|
251
260
|
if (markerStart === -1) {
|
|
252
261
|
pushText(parts, responseText.slice(cursor));
|
|
253
262
|
break;
|
|
254
263
|
}
|
|
255
264
|
pushText(parts, responseText.slice(cursor, markerStart));
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
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;
|
|
261
298
|
}
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
if (
|
|
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
|
|
265
316
|
const nextMarker = responseText.indexOf(TOOL_CALL_PREFIX, markerStart + TOOL_CALL_PREFIX.length);
|
|
266
317
|
const malformedEnd = nextMarker === -1 ? responseText.length : nextMarker;
|
|
267
318
|
pushText(parts, responseText.slice(markerStart, malformedEnd));
|
|
268
319
|
cursor = malformedEnd;
|
|
269
320
|
continue;
|
|
270
321
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
toolName,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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;
|
|
279
343
|
}
|
|
280
344
|
return parts;
|
|
281
345
|
}
|
|
@@ -38,7 +38,7 @@ const response_converter_js_1 = require("./response-converter.js");
|
|
|
38
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
44
|
(0, vitest_1.it)('parses marker format with text before tool call', () => {
|
|
@@ -46,14 +46,14 @@ const response_converter_js_1 = require("./response-converter.js");
|
|
|
46
46
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
47
47
|
(0, vitest_1.expect)(result).toEqual([
|
|
48
48
|
{ type: 'text', text: 'Some text before' },
|
|
49
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: {
|
|
49
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: '{"path":"/foo"}' }
|
|
50
50
|
]);
|
|
51
51
|
});
|
|
52
52
|
(0, vitest_1.it)('parses marker format with text after tool call', () => {
|
|
53
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: 'grep', input: {
|
|
56
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: '{"pattern":"foo"}' },
|
|
57
57
|
{ type: 'text', text: 'Some text after' }
|
|
58
58
|
]);
|
|
59
59
|
});
|
|
@@ -61,36 +61,36 @@ const response_converter_js_1 = require("./response-converter.js");
|
|
|
61
61
|
const input = '[TOOL_CALLS]read[ARGS]{"path":"/a"}[TOOL_CALLS]grep[ARGS]{"pattern":"foo"}';
|
|
62
62
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
63
63
|
(0, vitest_1.expect)(result).toEqual([
|
|
64
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: {
|
|
65
|
-
{ type: 'tool-call', toolCallId: 'toolcall_2', toolName: 'grep', input: {
|
|
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
66
|
]);
|
|
67
67
|
});
|
|
68
68
|
(0, vitest_1.it)('parses TOOL_CALLS format with numeric tool id', () => {
|
|
69
69
|
const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"pattern":"test"}}}';
|
|
70
70
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
71
71
|
(0, vitest_1.expect)(result).toEqual([
|
|
72
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: {
|
|
72
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: '{"pattern":"test"}' }
|
|
73
73
|
]);
|
|
74
74
|
});
|
|
75
75
|
(0, vitest_1.it)('maps tool id 1 to read', () => {
|
|
76
76
|
const input = 'TOOL_CALLS{"type":"function","function":{"name":1,"parameters":{"path":"/file"}}}';
|
|
77
77
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
78
78
|
(0, vitest_1.expect)(result).toEqual([
|
|
79
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: {
|
|
79
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: '{"path":"/file"}' }
|
|
80
80
|
]);
|
|
81
81
|
});
|
|
82
82
|
(0, vitest_1.it)('maps tool id 2 to glob', () => {
|
|
83
83
|
const input = 'TOOL_CALLS{"type":"function","function":{"name":2,"parameters":{"pattern":"*.ts"}}}';
|
|
84
84
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
85
85
|
(0, vitest_1.expect)(result).toEqual([
|
|
86
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'glob', input: {
|
|
86
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'glob', input: '{"pattern":"*.ts"}' }
|
|
87
87
|
]);
|
|
88
88
|
});
|
|
89
89
|
(0, vitest_1.it)('maps unknown tool id to tool_N', () => {
|
|
90
90
|
const input = 'TOOL_CALLS{"type":"function","function":{"name":99,"parameters":{}}}';
|
|
91
91
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
92
92
|
(0, vitest_1.expect)(result).toEqual([
|
|
93
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'tool_99', input: {} }
|
|
93
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'tool_99', input: '{}' }
|
|
94
94
|
]);
|
|
95
95
|
});
|
|
96
96
|
(0, vitest_1.it)('returns text for non-array JSON', () => {
|
|
@@ -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';
|
|
@@ -279,7 +284,9 @@ function buildGenerateRequest(input) {
|
|
|
279
284
|
const request = new protobuf_js_1.ProtobufEncoder();
|
|
280
285
|
request.writeMessage(1, buildMetadata(input.apiKey, input.jwt));
|
|
281
286
|
const functionTools = input.tools?.filter(isFunctionTool) ?? [];
|
|
282
|
-
const messages =
|
|
287
|
+
const messages = functionTools.length > 0
|
|
288
|
+
? [{ role: 5, content: TOOL_FORMAT_INSTRUCTION }, ...input.messages]
|
|
289
|
+
: input.messages;
|
|
283
290
|
for (const message of messages) {
|
|
284
291
|
request.writeMessage(2, buildMessage(message));
|
|
285
292
|
}
|
|
@@ -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)('
|
|
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).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).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', () => {
|
|
@@ -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[];
|
|
@@ -4,11 +4,18 @@ const TOOL_CALL_PREFIX = '[TOOL_CALLS]';
|
|
|
4
4
|
const ARGS_PREFIX = '[ARGS]';
|
|
5
5
|
const STOP_TOKEN = '</s>';
|
|
6
6
|
function parseOpenAIToolCalls(responseText) {
|
|
7
|
-
|
|
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 {
|
|
8
16
|
return null;
|
|
9
17
|
}
|
|
10
|
-
|
|
11
|
-
if (!jsonPart.startsWith('{')) {
|
|
18
|
+
if (!jsonPart.startsWith('{') && !jsonPart.startsWith('[')) {
|
|
12
19
|
return null;
|
|
13
20
|
}
|
|
14
21
|
const toolCalls = [];
|
|
@@ -48,7 +55,7 @@ function parseOpenAIToolCalls(responseText) {
|
|
|
48
55
|
type: 'tool-call',
|
|
49
56
|
toolCallId: `toolcall_${index + 1}`,
|
|
50
57
|
toolName,
|
|
51
|
-
input: args,
|
|
58
|
+
input: JSON.stringify(args),
|
|
52
59
|
};
|
|
53
60
|
});
|
|
54
61
|
}
|
|
@@ -236,6 +243,7 @@ function decodeResponseText(buffer) {
|
|
|
236
243
|
export function convertResponse(buffer) {
|
|
237
244
|
let responseText = decodeResponseText(buffer);
|
|
238
245
|
responseText = responseText.replace(STOP_TOKEN, '');
|
|
246
|
+
// Try parsing as pure tool calls first (for backward compat)
|
|
239
247
|
const openaiToolCalls = parseOpenAIToolCalls(responseText);
|
|
240
248
|
if (openaiToolCalls) {
|
|
241
249
|
return openaiToolCalls;
|
|
@@ -244,35 +252,91 @@ export function convertResponse(buffer) {
|
|
|
244
252
|
let cursor = 0;
|
|
245
253
|
let toolCallCount = 0;
|
|
246
254
|
while (cursor < responseText.length) {
|
|
255
|
+
// Try to find [TOOL_CALLS] marker
|
|
247
256
|
const markerStart = responseText.indexOf(TOOL_CALL_PREFIX, cursor);
|
|
248
257
|
if (markerStart === -1) {
|
|
249
258
|
pushText(parts, responseText.slice(cursor));
|
|
250
259
|
break;
|
|
251
260
|
}
|
|
252
261
|
pushText(parts, responseText.slice(cursor, markerStart));
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
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;
|
|
258
295
|
}
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
if (
|
|
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
|
|
262
313
|
const nextMarker = responseText.indexOf(TOOL_CALL_PREFIX, markerStart + TOOL_CALL_PREFIX.length);
|
|
263
314
|
const malformedEnd = nextMarker === -1 ? responseText.length : nextMarker;
|
|
264
315
|
pushText(parts, responseText.slice(markerStart, malformedEnd));
|
|
265
316
|
cursor = malformedEnd;
|
|
266
317
|
continue;
|
|
267
318
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
toolName,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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;
|
|
276
340
|
}
|
|
277
341
|
return parts;
|
|
278
342
|
}
|
|
@@ -36,7 +36,7 @@ describe('convertResponse', () => {
|
|
|
36
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
42
|
it('parses marker format with text before tool call', () => {
|
|
@@ -44,14 +44,14 @@ describe('convertResponse', () => {
|
|
|
44
44
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
45
45
|
expect(result).toEqual([
|
|
46
46
|
{ type: 'text', text: 'Some text before' },
|
|
47
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: {
|
|
47
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: '{"path":"/foo"}' }
|
|
48
48
|
]);
|
|
49
49
|
});
|
|
50
50
|
it('parses marker format with text after tool call', () => {
|
|
51
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: 'grep', input: {
|
|
54
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: '{"pattern":"foo"}' },
|
|
55
55
|
{ type: 'text', text: 'Some text after' }
|
|
56
56
|
]);
|
|
57
57
|
});
|
|
@@ -59,36 +59,36 @@ describe('convertResponse', () => {
|
|
|
59
59
|
const input = '[TOOL_CALLS]read[ARGS]{"path":"/a"}[TOOL_CALLS]grep[ARGS]{"pattern":"foo"}';
|
|
60
60
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
61
61
|
expect(result).toEqual([
|
|
62
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: {
|
|
63
|
-
{ type: 'tool-call', toolCallId: 'toolcall_2', toolName: 'grep', input: {
|
|
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
64
|
]);
|
|
65
65
|
});
|
|
66
66
|
it('parses TOOL_CALLS format with numeric tool id', () => {
|
|
67
67
|
const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"pattern":"test"}}}';
|
|
68
68
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
69
69
|
expect(result).toEqual([
|
|
70
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: {
|
|
70
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: '{"pattern":"test"}' }
|
|
71
71
|
]);
|
|
72
72
|
});
|
|
73
73
|
it('maps tool id 1 to read', () => {
|
|
74
74
|
const input = 'TOOL_CALLS{"type":"function","function":{"name":1,"parameters":{"path":"/file"}}}';
|
|
75
75
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
76
76
|
expect(result).toEqual([
|
|
77
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: {
|
|
77
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: '{"path":"/file"}' }
|
|
78
78
|
]);
|
|
79
79
|
});
|
|
80
80
|
it('maps tool id 2 to glob', () => {
|
|
81
81
|
const input = 'TOOL_CALLS{"type":"function","function":{"name":2,"parameters":{"pattern":"*.ts"}}}';
|
|
82
82
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
83
83
|
expect(result).toEqual([
|
|
84
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'glob', input: {
|
|
84
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'glob', input: '{"pattern":"*.ts"}' }
|
|
85
85
|
]);
|
|
86
86
|
});
|
|
87
87
|
it('maps unknown tool id to tool_N', () => {
|
|
88
88
|
const input = 'TOOL_CALLS{"type":"function","function":{"name":99,"parameters":{}}}';
|
|
89
89
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
90
90
|
expect(result).toEqual([
|
|
91
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'tool_99', input: {} }
|
|
91
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'tool_99', input: '{}' }
|
|
92
92
|
]);
|
|
93
93
|
});
|
|
94
94
|
it('returns text for non-array JSON', () => {
|
|
@@ -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';
|
|
@@ -275,7 +280,9 @@ function buildGenerateRequest(input) {
|
|
|
275
280
|
const request = new ProtobufEncoder();
|
|
276
281
|
request.writeMessage(1, buildMetadata(input.apiKey, input.jwt));
|
|
277
282
|
const functionTools = input.tools?.filter(isFunctionTool) ?? [];
|
|
278
|
-
const messages =
|
|
283
|
+
const messages = functionTools.length > 0
|
|
284
|
+
? [{ role: 5, content: TOOL_FORMAT_INSTRUCTION }, ...input.messages]
|
|
285
|
+
: input.messages;
|
|
279
286
|
for (const message of messages) {
|
|
280
287
|
request.writeMessage(2, buildMessage(message));
|
|
281
288
|
}
|
|
@@ -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('
|
|
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).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).toContain('When
|
|
351
|
+
expect(combined).not.toContain('CRITICAL: When using tools');
|
|
352
352
|
});
|
|
353
353
|
});
|
|
354
354
|
describe('DevstralLanguageModel doStream', () => {
|