@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.
@@ -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
- if (!responseText.startsWith('TOOL_CALLS')) {
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
- const jsonPart = responseText.slice('TOOL_CALLS'.length);
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 toolNameStart = markerStart + TOOL_CALL_PREFIX.length;
257
- const argsStart = responseText.indexOf(ARGS_PREFIX, toolNameStart);
258
- if (argsStart === -1) {
259
- pushText(parts, responseText.slice(markerStart));
260
- break;
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
- const toolName = responseText.slice(toolNameStart, argsStart);
263
- const parsedArgs = parseJsonValue(responseText, argsStart + ARGS_PREFIX.length);
264
- if (parsedArgs == null) {
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
- toolCallCount += 1;
272
- parts.push({
273
- type: 'tool-call',
274
- toolCallId: `toolcall_${toolCallCount}`,
275
- toolName,
276
- input: parsedArgs.parsed,
277
- });
278
- cursor = parsedArgs.endIndex;
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: { q: 'test' } }
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: { path: '/foo' } }
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: { pattern: 'foo' } },
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: { path: '/a' } },
65
- { type: 'tool-call', toolCallId: 'toolcall_2', toolName: 'grep', input: { pattern: 'foo' } }
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: { pattern: 'test' } }
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: { path: '/file' } }
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: { pattern: '*.ts' } }
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 you need to call tools, you must format your response as a JSON array of tool calls following the OpenAI function calling format. Each tool call must be an object with "type" set to "function" and a "function" object containing "name" (string matching one of the provided tool names) and "parameters" (object with the tool's arguments). Output ONLY the JSON array, no surrounding text.
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
- Example format:
26
- [{"type": "function", "function": {"name": "tool_name", "parameters": {"arg1": "value1"}}}]
25
+ [{"type":"function","function":{"name":"tool_name","parameters":{"arg":"value"}}}]
27
26
 
28
- For multiple tool calls:
29
- [{"type": "function", "function": {"name": "tool1", "parameters": {"arg1": "value1"}}}, {"type": "function", "function": {"name": "tool2", "parameters": {"arg2": "value2"}}}]`;
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 = [{ role: 5, content: TOOL_FORMAT_INSTRUCTION }, ...input.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 you need to call tools');
307
+ (0, vitest_1.expect)(combined).toContain('CRITICAL: When using tools');
308
308
  });
309
- (0, vitest_1.it)('always injects tool format instruction even without tools', async () => {
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 you need to call tools');
326
+ (0, vitest_1.expect)(combined).not.toContain('CRITICAL: When using tools');
327
327
  });
328
- (0, vitest_1.it)('always injects tool format instruction even with only provider-defined tools', async () => {
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 you need to call tools');
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: unknown;
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
- if (!responseText.startsWith('TOOL_CALLS')) {
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
- const jsonPart = responseText.slice('TOOL_CALLS'.length);
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 toolNameStart = markerStart + TOOL_CALL_PREFIX.length;
254
- const argsStart = responseText.indexOf(ARGS_PREFIX, toolNameStart);
255
- if (argsStart === -1) {
256
- pushText(parts, responseText.slice(markerStart));
257
- break;
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
- const toolName = responseText.slice(toolNameStart, argsStart);
260
- const parsedArgs = parseJsonValue(responseText, argsStart + ARGS_PREFIX.length);
261
- if (parsedArgs == null) {
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
- toolCallCount += 1;
269
- parts.push({
270
- type: 'tool-call',
271
- toolCallId: `toolcall_${toolCallCount}`,
272
- toolName,
273
- input: parsedArgs.parsed,
274
- });
275
- cursor = parsedArgs.endIndex;
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: { q: 'test' } }
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: { path: '/foo' } }
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: { pattern: 'foo' } },
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: { path: '/a' } },
63
- { type: 'tool-call', toolCallId: 'toolcall_2', toolName: 'grep', input: { pattern: 'foo' } }
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: { pattern: 'test' } }
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: { path: '/file' } }
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: { pattern: '*.ts' } }
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 you need to call tools, you must format your response as a JSON array of tool calls following the OpenAI function calling format. Each tool call must be an object with "type" set to "function" and a "function" object containing "name" (string matching one of the provided tool names) and "parameters" (object with the tool's arguments). Output ONLY the JSON array, no surrounding text.
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
- Example format:
23
- [{"type": "function", "function": {"name": "tool_name", "parameters": {"arg1": "value1"}}}]
22
+ [{"type":"function","function":{"name":"tool_name","parameters":{"arg":"value"}}}]
24
23
 
25
- For multiple tool calls:
26
- [{"type": "function", "function": {"name": "tool1", "parameters": {"arg1": "value1"}}}, {"type": "function", "function": {"name": "tool2", "parameters": {"arg2": "value2"}}}]`;
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 = [{ role: 5, content: TOOL_FORMAT_INSTRUCTION }, ...input.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 you need to call tools');
305
+ expect(combined).toContain('CRITICAL: When using tools');
306
306
  });
307
- it('always injects tool format instruction even without tools', async () => {
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 you need to call tools');
324
+ expect(combined).not.toContain('CRITICAL: When using tools');
325
325
  });
326
- it('always injects tool format instruction even with only provider-defined tools', async () => {
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 you need to call tools');
351
+ expect(combined).not.toContain('CRITICAL: When using tools');
352
352
  });
353
353
  });
354
354
  describe('DevstralLanguageModel doStream', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bxb1337/windsurf-fast-context",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "AI SDK V3 provider for Windsurf's Devstral code search API",
5
5
  "type": "module",
6
6
  "scripts": {