@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.
@@ -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 parseStrictOpenAIToolCalls(responseText) {
8
- const trimmed = responseText.trim();
9
- if (!trimmed.startsWith('['))
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
- try {
12
- const parsed = JSON.parse(trimmed);
13
- if (!Array.isArray(parsed) || parsed.length === 0)
14
- return null;
15
- const toolCalls = [];
16
- for (let i = 0; i < parsed.length; i++) {
17
- const item = parsed[i];
18
- if (item.type !== 'function' || !item.function?.name)
19
- continue;
20
- toolCalls.push({
21
- type: 'tool-call',
22
- toolCallId: `toolcall_${i + 1}`,
23
- toolName: String(item.function.name),
24
- input: item.function.parameters ?? {},
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
- catch {
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
- const strictToolCalls = parseStrictOpenAIToolCalls(responseText);
85
- if (strictToolCalls)
86
- return strictToolCalls;
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
- pushText(parts, responseText);
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
- // Strict OpenAI array format tests
37
- (0, vitest_1.it)('parses strict OpenAI array with single tool call', () => {
38
- const input = '[{"type":"function","function":{"name":"search","parameters":{"q":"test"}}}]';
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: { q: 'test' } }
41
+ { type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'search', input: '{"q":"test"}' }
42
42
  ]);
43
43
  });
44
- (0, vitest_1.it)('parses strict OpenAI array with multiple tool calls', () => {
45
- const input = '[{"type":"function","function":{"name":"read","parameters":{"path":"/a"}}},{"type":"function","function":{"name":"grep","parameters":{"pattern":"foo"}}}]';
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: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: { path: '/a' } },
49
- { type: 'tool-call', toolCallId: 'toolcall_2', toolName: 'grep', input: { pattern: 'foo' } }
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 strict OpenAI array with empty parameters', () => {
53
- const input = '[{"type":"function","function":{"name":"list","parameters":{}}}]';
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: 'list', input: {} }
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)('returns text for old marker format (no backward compat)', () => {
60
- const input = '[TOOL_CALLS]search[ARGS]{"q":"test"}';
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([{ type: 'text', text: '[TOOL_CALLS]search[ARGS]{"q":"test"}' }]);
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)('returns text for old TOOL_CALLS format (no backward compat)', () => {
65
- const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"q":"test"}}}{}';
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([{ type: 'text', text: input }]);
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 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';
@@ -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('[{"type":"function","function":{"name":"searchRepo","parameters":{"query":"jwt manager"}}}]', 'utf8');
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 you need to call tools');
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 provided', 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).not.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)('does not inject tool format instruction when 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).not.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', () => {
@@ -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('[{"type":"function","function":{"name":"searchRepo","parameters":{"query":"jwt manager"}}}]', 'utf8');
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: unknown;
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 parseStrictOpenAIToolCalls(responseText) {
5
- const trimmed = responseText.trim();
6
- if (!trimmed.startsWith('['))
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
- try {
9
- const parsed = JSON.parse(trimmed);
10
- if (!Array.isArray(parsed) || parsed.length === 0)
11
- return null;
12
- const toolCalls = [];
13
- for (let i = 0; i < parsed.length; i++) {
14
- const item = parsed[i];
15
- if (item.type !== 'function' || !item.function?.name)
16
- continue;
17
- toolCalls.push({
18
- type: 'tool-call',
19
- toolCallId: `toolcall_${i + 1}`,
20
- toolName: String(item.function.name),
21
- input: item.function.parameters ?? {},
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
- catch {
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
- const strictToolCalls = parseStrictOpenAIToolCalls(responseText);
82
- if (strictToolCalls)
83
- return strictToolCalls;
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
- pushText(parts, responseText);
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
- // Strict OpenAI array format tests
35
- it('parses strict OpenAI array with single tool call', () => {
36
- const input = '[{"type":"function","function":{"name":"search","parameters":{"q":"test"}}}]';
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: { q: 'test' } }
39
+ { type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'search', input: '{"q":"test"}' }
40
40
  ]);
41
41
  });
42
- it('parses strict OpenAI array with multiple tool calls', () => {
43
- const input = '[{"type":"function","function":{"name":"read","parameters":{"path":"/a"}}},{"type":"function","function":{"name":"grep","parameters":{"pattern":"foo"}}}]';
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: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: { path: '/a' } },
47
- { type: 'tool-call', toolCallId: 'toolcall_2', toolName: 'grep', input: { pattern: 'foo' } }
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 strict OpenAI array with empty parameters', () => {
51
- const input = '[{"type":"function","function":{"name":"list","parameters":{}}}]';
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: 'list', input: {} }
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('returns text for old marker format (no backward compat)', () => {
58
- const input = '[TOOL_CALLS]search[ARGS]{"q":"test"}';
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([{ type: 'text', text: '[TOOL_CALLS]search[ARGS]{"q":"test"}' }]);
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('returns text for old TOOL_CALLS format (no backward compat)', () => {
63
- const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"q":"test"}}}{}';
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([{ type: 'text', text: input }]);
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 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';
@@ -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('[{"type":"function","function":{"name":"searchRepo","parameters":{"query":"jwt manager"}}}]', 'utf8');
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 you need to call tools');
305
+ expect(combined).toContain('CRITICAL: When using tools');
306
306
  });
307
- it('does not inject tool format instruction when no tools provided', 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).not.toContain('When you need to call tools');
324
+ expect(combined).not.toContain('CRITICAL: When using tools');
325
325
  });
326
- it('does not inject tool format instruction when 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).not.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', () => {
@@ -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('[{"type":"function","function":{"name":"searchRepo","parameters":{"query":"jwt manager"}}}]', 'utf8');
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bxb1337/windsurf-fast-context",
3
- "version": "1.1.2",
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": {