@bxb1337/windsurf-fast-context 1.1.2 → 1.1.3

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,192 @@ 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
+ if (!responseText.startsWith('TOOL_CALLS')) {
10
11
  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
- });
26
- }
27
- return toolCalls.length > 0 ? toolCalls : null;
28
12
  }
29
- catch {
13
+ const jsonPart = responseText.slice('TOOL_CALLS'.length);
14
+ if (!jsonPart.startsWith('{')) {
15
+ return null;
16
+ }
17
+ const toolCalls = [];
18
+ let cursor = 0;
19
+ while (cursor < jsonPart.length) {
20
+ if (jsonPart[cursor] !== '{') {
21
+ cursor++;
22
+ continue;
23
+ }
24
+ const endResult = parseBalancedEnd(jsonPart, cursor);
25
+ if (endResult == null) {
26
+ break;
27
+ }
28
+ const jsonStr = jsonPart.slice(cursor, endResult);
29
+ cursor = endResult;
30
+ if (jsonStr === '{}') {
31
+ continue;
32
+ }
33
+ try {
34
+ const parsed = JSON.parse(jsonStr);
35
+ if (parsed.type === 'function' && parsed.function) {
36
+ toolCalls.push(parsed);
37
+ }
38
+ }
39
+ catch {
40
+ continue;
41
+ }
42
+ }
43
+ if (toolCalls.length === 0) {
30
44
  return null;
31
45
  }
46
+ return toolCalls.map((call, index) => {
47
+ const toolId = call.function.name;
48
+ const toolName = typeof toolId === 'number' ? mapToolIdToName(toolId) : String(toolId);
49
+ const args = call.function.parameters ?? {};
50
+ return {
51
+ type: 'tool-call',
52
+ toolCallId: `toolcall_${index + 1}`,
53
+ toolName,
54
+ input: args,
55
+ };
56
+ });
57
+ }
58
+ function mapToolIdToName(id) {
59
+ switch (id) {
60
+ case 1:
61
+ return 'read';
62
+ case 2:
63
+ return 'glob';
64
+ case 3:
65
+ return 'grep';
66
+ default:
67
+ return `tool_${id}`;
68
+ }
32
69
  }
33
70
  function pushText(parts, text) {
34
71
  if (text.length > 0) {
35
72
  parts.push({ type: 'text', text });
36
73
  }
37
74
  }
75
+ function parseStringEnd(value, startIndex) {
76
+ let index = startIndex + 1;
77
+ let escaping = false;
78
+ while (index < value.length) {
79
+ const char = value[index];
80
+ if (escaping) {
81
+ escaping = false;
82
+ }
83
+ else if (char === '\\') {
84
+ escaping = true;
85
+ }
86
+ else if (char === '"') {
87
+ return index + 1;
88
+ }
89
+ index += 1;
90
+ }
91
+ return null;
92
+ }
93
+ function parseBalancedEnd(value, startIndex) {
94
+ const stack = [value[startIndex] === '{' ? '}' : ']'];
95
+ let index = startIndex + 1;
96
+ let inString = false;
97
+ let escaping = false;
98
+ while (index < value.length) {
99
+ const char = value[index];
100
+ if (inString) {
101
+ if (escaping) {
102
+ escaping = false;
103
+ }
104
+ else if (char === '\\') {
105
+ escaping = true;
106
+ }
107
+ else if (char === '"') {
108
+ inString = false;
109
+ }
110
+ index += 1;
111
+ continue;
112
+ }
113
+ if (char === '"') {
114
+ inString = true;
115
+ index += 1;
116
+ continue;
117
+ }
118
+ if (char === '{') {
119
+ stack.push('}');
120
+ index += 1;
121
+ continue;
122
+ }
123
+ if (char === '[') {
124
+ stack.push(']');
125
+ index += 1;
126
+ continue;
127
+ }
128
+ if (char === '}' || char === ']') {
129
+ const expected = stack[stack.length - 1];
130
+ if (expected !== char) {
131
+ return null;
132
+ }
133
+ stack.pop();
134
+ index += 1;
135
+ if (stack.length === 0) {
136
+ return index;
137
+ }
138
+ continue;
139
+ }
140
+ index += 1;
141
+ }
142
+ return null;
143
+ }
144
+ function parsePrimitiveEnd(value, startIndex) {
145
+ let index = startIndex;
146
+ while (index < value.length) {
147
+ const char = value[index];
148
+ if (char === ' ' || char === '\n' || char === '\r' || char === '\t') {
149
+ break;
150
+ }
151
+ index += 1;
152
+ }
153
+ return index;
154
+ }
155
+ function parseJsonValue(value, startIndex) {
156
+ let jsonStart = startIndex;
157
+ while (jsonStart < value.length) {
158
+ const char = value[jsonStart];
159
+ if (char !== ' ' && char !== '\n' && char !== '\r' && char !== '\t') {
160
+ break;
161
+ }
162
+ jsonStart += 1;
163
+ }
164
+ if (jsonStart >= value.length) {
165
+ return null;
166
+ }
167
+ const firstChar = value[jsonStart];
168
+ let endIndex;
169
+ if (firstChar === '{' || firstChar === '[') {
170
+ endIndex = parseBalancedEnd(value, jsonStart);
171
+ }
172
+ else if (firstChar === '"') {
173
+ endIndex = parseStringEnd(value, jsonStart);
174
+ }
175
+ else {
176
+ endIndex = parsePrimitiveEnd(value, jsonStart);
177
+ }
178
+ if (endIndex == null || endIndex <= jsonStart) {
179
+ return null;
180
+ }
181
+ const rawJson = value.slice(jsonStart, endIndex);
182
+ try {
183
+ return {
184
+ parsed: JSON.parse(rawJson),
185
+ endIndex,
186
+ };
187
+ }
188
+ catch {
189
+ return null;
190
+ }
191
+ }
38
192
  function hasControlChars(value) {
39
193
  for (const char of value) {
40
194
  const code = char.charCodeAt(0);
@@ -62,6 +216,10 @@ function isLikelyMetadata(value) {
62
216
  return /^[A-Za-z0-9._:-]{1,32}$/.test(value);
63
217
  }
64
218
  function pickBestExtractedText(values) {
219
+ const markerValues = values.filter((value) => value.includes(TOOL_CALL_PREFIX) || value.includes(ARGS_PREFIX));
220
+ if (markerValues.length > 0) {
221
+ return markerValues.join('');
222
+ }
65
223
  const nonMetadata = values.filter((value) => !isLikelyMetadata(value));
66
224
  const candidates = nonMetadata.length > 0 ? nonMetadata : values;
67
225
  return candidates.reduce((best, current) => (current.length > best.length ? current : best), candidates[0] ?? '');
@@ -81,10 +239,43 @@ function decodeResponseText(buffer) {
81
239
  function convertResponse(buffer) {
82
240
  let responseText = decodeResponseText(buffer);
83
241
  responseText = responseText.replace(STOP_TOKEN, '');
84
- const strictToolCalls = parseStrictOpenAIToolCalls(responseText);
85
- if (strictToolCalls)
86
- return strictToolCalls;
242
+ const openaiToolCalls = parseOpenAIToolCalls(responseText);
243
+ if (openaiToolCalls) {
244
+ return openaiToolCalls;
245
+ }
87
246
  const parts = [];
88
- pushText(parts, responseText);
247
+ let cursor = 0;
248
+ let toolCallCount = 0;
249
+ while (cursor < responseText.length) {
250
+ const markerStart = responseText.indexOf(TOOL_CALL_PREFIX, cursor);
251
+ if (markerStart === -1) {
252
+ pushText(parts, responseText.slice(cursor));
253
+ break;
254
+ }
255
+ 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;
261
+ }
262
+ const toolName = responseText.slice(toolNameStart, argsStart);
263
+ const parsedArgs = parseJsonValue(responseText, argsStart + ARGS_PREFIX.length);
264
+ if (parsedArgs == null) {
265
+ const nextMarker = responseText.indexOf(TOOL_CALL_PREFIX, markerStart + TOOL_CALL_PREFIX.length);
266
+ const malformedEnd = nextMarker === -1 ? responseText.length : nextMarker;
267
+ pushText(parts, responseText.slice(markerStart, malformedEnd));
268
+ cursor = malformedEnd;
269
+ continue;
270
+ }
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;
279
+ }
89
280
  return parts;
90
281
  }
@@ -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
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
+ const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
47
+ (0, vitest_1.expect)(result).toEqual([
48
+ { type: 'text', text: 'Some text before' },
49
+ { type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: { path: '/foo' } }
50
+ ]);
51
+ });
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
+ const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
55
+ (0, vitest_1.expect)(result).toEqual([
56
+ { type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: { pattern: 'foo' } },
57
+ { type: 'text', text: 'Some text after' }
58
+ ]);
59
+ });
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"}';
46
62
  const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
47
63
  (0, vitest_1.expect)(result).toEqual([
48
64
  { type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: { path: '/a' } },
49
65
  { type: 'tool-call', toolCallId: 'toolcall_2', toolName: 'grep', input: { pattern: 'foo' } }
50
66
  ]);
51
67
  });
52
- (0, vitest_1.it)('parses strict OpenAI array with empty parameters', () => {
53
- const input = '[{"type":"function","function":{"name":"list","parameters":{}}}]';
68
+ (0, vitest_1.it)('parses TOOL_CALLS format with numeric tool id', () => {
69
+ const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"pattern":"test"}}}';
54
70
  const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
55
71
  (0, vitest_1.expect)(result).toEqual([
56
- { type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'list', input: {} }
72
+ { type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: { pattern: 'test' } }
57
73
  ]);
58
74
  });
59
- (0, vitest_1.it)('returns text for old marker format (no backward compat)', () => {
60
- const input = '[TOOL_CALLS]search[ARGS]{"q":"test"}';
75
+ (0, vitest_1.it)('maps tool id 1 to read', () => {
76
+ const input = 'TOOL_CALLS{"type":"function","function":{"name":1,"parameters":{"path":"/file"}}}';
61
77
  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"}' }]);
78
+ (0, vitest_1.expect)(result).toEqual([
79
+ { type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: { path: '/file' } }
80
+ ]);
63
81
  });
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"}}}{}';
82
+ (0, vitest_1.it)('maps tool id 2 to glob', () => {
83
+ const input = 'TOOL_CALLS{"type":"function","function":{"name":2,"parameters":{"pattern":"*.ts"}}}';
66
84
  const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
67
- (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: input }]);
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":{}}}';
@@ -279,9 +279,7 @@ function buildGenerateRequest(input) {
279
279
  const request = new protobuf_js_1.ProtobufEncoder();
280
280
  request.writeMessage(1, buildMetadata(input.apiKey, input.jwt));
281
281
  const functionTools = input.tools?.filter(isFunctionTool) ?? [];
282
- const messages = functionTools.length > 0
283
- ? [{ role: 5, content: TOOL_FORMAT_INSTRUCTION }, ...input.messages]
284
- : input.messages;
282
+ const messages = [{ role: 5, content: TOOL_FORMAT_INSTRUCTION }, ...input.messages];
285
283
  for (const message of messages) {
286
284
  request.writeMessage(2, buildMessage(message));
287
285
  }
@@ -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' });
@@ -306,7 +306,7 @@ async function collectStreamParts(stream) {
306
306
  const combined = strings.join('\n');
307
307
  (0, vitest_1.expect)(combined).toContain('When you need to call tools');
308
308
  });
309
- (0, vitest_1.it)('does not inject tool format instruction when no tools provided', async () => {
309
+ (0, vitest_1.it)('always injects tool format instruction even without tools', 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).toContain('When you need to call 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)('always injects tool format instruction even with only provider-defined tools', 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).toContain('When you need to call 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);
@@ -1,37 +1,191 @@
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
+ if (!responseText.startsWith('TOOL_CALLS')) {
7
8
  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
- });
23
- }
24
- return toolCalls.length > 0 ? toolCalls : null;
25
9
  }
26
- catch {
10
+ const jsonPart = responseText.slice('TOOL_CALLS'.length);
11
+ if (!jsonPart.startsWith('{')) {
12
+ return null;
13
+ }
14
+ const toolCalls = [];
15
+ let cursor = 0;
16
+ while (cursor < jsonPart.length) {
17
+ if (jsonPart[cursor] !== '{') {
18
+ cursor++;
19
+ continue;
20
+ }
21
+ const endResult = parseBalancedEnd(jsonPart, cursor);
22
+ if (endResult == null) {
23
+ break;
24
+ }
25
+ const jsonStr = jsonPart.slice(cursor, endResult);
26
+ cursor = endResult;
27
+ if (jsonStr === '{}') {
28
+ continue;
29
+ }
30
+ try {
31
+ const parsed = JSON.parse(jsonStr);
32
+ if (parsed.type === 'function' && parsed.function) {
33
+ toolCalls.push(parsed);
34
+ }
35
+ }
36
+ catch {
37
+ continue;
38
+ }
39
+ }
40
+ if (toolCalls.length === 0) {
27
41
  return null;
28
42
  }
43
+ return toolCalls.map((call, index) => {
44
+ const toolId = call.function.name;
45
+ const toolName = typeof toolId === 'number' ? mapToolIdToName(toolId) : String(toolId);
46
+ const args = call.function.parameters ?? {};
47
+ return {
48
+ type: 'tool-call',
49
+ toolCallId: `toolcall_${index + 1}`,
50
+ toolName,
51
+ input: args,
52
+ };
53
+ });
54
+ }
55
+ function mapToolIdToName(id) {
56
+ switch (id) {
57
+ case 1:
58
+ return 'read';
59
+ case 2:
60
+ return 'glob';
61
+ case 3:
62
+ return 'grep';
63
+ default:
64
+ return `tool_${id}`;
65
+ }
29
66
  }
30
67
  function pushText(parts, text) {
31
68
  if (text.length > 0) {
32
69
  parts.push({ type: 'text', text });
33
70
  }
34
71
  }
72
+ function parseStringEnd(value, startIndex) {
73
+ let index = startIndex + 1;
74
+ let escaping = false;
75
+ while (index < value.length) {
76
+ const char = value[index];
77
+ if (escaping) {
78
+ escaping = false;
79
+ }
80
+ else if (char === '\\') {
81
+ escaping = true;
82
+ }
83
+ else if (char === '"') {
84
+ return index + 1;
85
+ }
86
+ index += 1;
87
+ }
88
+ return null;
89
+ }
90
+ function parseBalancedEnd(value, startIndex) {
91
+ const stack = [value[startIndex] === '{' ? '}' : ']'];
92
+ let index = startIndex + 1;
93
+ let inString = false;
94
+ let escaping = false;
95
+ while (index < value.length) {
96
+ const char = value[index];
97
+ if (inString) {
98
+ if (escaping) {
99
+ escaping = false;
100
+ }
101
+ else if (char === '\\') {
102
+ escaping = true;
103
+ }
104
+ else if (char === '"') {
105
+ inString = false;
106
+ }
107
+ index += 1;
108
+ continue;
109
+ }
110
+ if (char === '"') {
111
+ inString = true;
112
+ index += 1;
113
+ continue;
114
+ }
115
+ if (char === '{') {
116
+ stack.push('}');
117
+ index += 1;
118
+ continue;
119
+ }
120
+ if (char === '[') {
121
+ stack.push(']');
122
+ index += 1;
123
+ continue;
124
+ }
125
+ if (char === '}' || char === ']') {
126
+ const expected = stack[stack.length - 1];
127
+ if (expected !== char) {
128
+ return null;
129
+ }
130
+ stack.pop();
131
+ index += 1;
132
+ if (stack.length === 0) {
133
+ return index;
134
+ }
135
+ continue;
136
+ }
137
+ index += 1;
138
+ }
139
+ return null;
140
+ }
141
+ function parsePrimitiveEnd(value, startIndex) {
142
+ let index = startIndex;
143
+ while (index < value.length) {
144
+ const char = value[index];
145
+ if (char === ' ' || char === '\n' || char === '\r' || char === '\t') {
146
+ break;
147
+ }
148
+ index += 1;
149
+ }
150
+ return index;
151
+ }
152
+ function parseJsonValue(value, startIndex) {
153
+ let jsonStart = startIndex;
154
+ while (jsonStart < value.length) {
155
+ const char = value[jsonStart];
156
+ if (char !== ' ' && char !== '\n' && char !== '\r' && char !== '\t') {
157
+ break;
158
+ }
159
+ jsonStart += 1;
160
+ }
161
+ if (jsonStart >= value.length) {
162
+ return null;
163
+ }
164
+ const firstChar = value[jsonStart];
165
+ let endIndex;
166
+ if (firstChar === '{' || firstChar === '[') {
167
+ endIndex = parseBalancedEnd(value, jsonStart);
168
+ }
169
+ else if (firstChar === '"') {
170
+ endIndex = parseStringEnd(value, jsonStart);
171
+ }
172
+ else {
173
+ endIndex = parsePrimitiveEnd(value, jsonStart);
174
+ }
175
+ if (endIndex == null || endIndex <= jsonStart) {
176
+ return null;
177
+ }
178
+ const rawJson = value.slice(jsonStart, endIndex);
179
+ try {
180
+ return {
181
+ parsed: JSON.parse(rawJson),
182
+ endIndex,
183
+ };
184
+ }
185
+ catch {
186
+ return null;
187
+ }
188
+ }
35
189
  function hasControlChars(value) {
36
190
  for (const char of value) {
37
191
  const code = char.charCodeAt(0);
@@ -59,6 +213,10 @@ function isLikelyMetadata(value) {
59
213
  return /^[A-Za-z0-9._:-]{1,32}$/.test(value);
60
214
  }
61
215
  function pickBestExtractedText(values) {
216
+ const markerValues = values.filter((value) => value.includes(TOOL_CALL_PREFIX) || value.includes(ARGS_PREFIX));
217
+ if (markerValues.length > 0) {
218
+ return markerValues.join('');
219
+ }
62
220
  const nonMetadata = values.filter((value) => !isLikelyMetadata(value));
63
221
  const candidates = nonMetadata.length > 0 ? nonMetadata : values;
64
222
  return candidates.reduce((best, current) => (current.length > best.length ? current : best), candidates[0] ?? '');
@@ -78,10 +236,43 @@ function decodeResponseText(buffer) {
78
236
  export function convertResponse(buffer) {
79
237
  let responseText = decodeResponseText(buffer);
80
238
  responseText = responseText.replace(STOP_TOKEN, '');
81
- const strictToolCalls = parseStrictOpenAIToolCalls(responseText);
82
- if (strictToolCalls)
83
- return strictToolCalls;
239
+ const openaiToolCalls = parseOpenAIToolCalls(responseText);
240
+ if (openaiToolCalls) {
241
+ return openaiToolCalls;
242
+ }
84
243
  const parts = [];
85
- pushText(parts, responseText);
244
+ let cursor = 0;
245
+ let toolCallCount = 0;
246
+ while (cursor < responseText.length) {
247
+ const markerStart = responseText.indexOf(TOOL_CALL_PREFIX, cursor);
248
+ if (markerStart === -1) {
249
+ pushText(parts, responseText.slice(cursor));
250
+ break;
251
+ }
252
+ 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;
258
+ }
259
+ const toolName = responseText.slice(toolNameStart, argsStart);
260
+ const parsedArgs = parseJsonValue(responseText, argsStart + ARGS_PREFIX.length);
261
+ if (parsedArgs == null) {
262
+ const nextMarker = responseText.indexOf(TOOL_CALL_PREFIX, markerStart + TOOL_CALL_PREFIX.length);
263
+ const malformedEnd = nextMarker === -1 ? responseText.length : nextMarker;
264
+ pushText(parts, responseText.slice(markerStart, malformedEnd));
265
+ cursor = malformedEnd;
266
+ continue;
267
+ }
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;
276
+ }
86
277
  return parts;
87
278
  }
@@ -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
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
+ const result = convertResponse(Buffer.from(input, 'utf8'));
45
+ expect(result).toEqual([
46
+ { type: 'text', text: 'Some text before' },
47
+ { type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: { path: '/foo' } }
48
+ ]);
49
+ });
50
+ it('parses marker format with text after tool call', () => {
51
+ const input = '[TOOL_CALLS]grep[ARGS]{"pattern":"foo"}Some text after';
52
+ const result = convertResponse(Buffer.from(input, 'utf8'));
53
+ expect(result).toEqual([
54
+ { type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: { pattern: 'foo' } },
55
+ { type: 'text', text: 'Some text after' }
56
+ ]);
57
+ });
58
+ it('parses multiple tool calls in marker format', () => {
59
+ const input = '[TOOL_CALLS]read[ARGS]{"path":"/a"}[TOOL_CALLS]grep[ARGS]{"pattern":"foo"}';
44
60
  const result = convertResponse(Buffer.from(input, 'utf8'));
45
61
  expect(result).toEqual([
46
62
  { type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: { path: '/a' } },
47
63
  { type: 'tool-call', toolCallId: 'toolcall_2', toolName: 'grep', input: { pattern: 'foo' } }
48
64
  ]);
49
65
  });
50
- it('parses strict OpenAI array with empty parameters', () => {
51
- const input = '[{"type":"function","function":{"name":"list","parameters":{}}}]';
66
+ it('parses TOOL_CALLS format with numeric tool id', () => {
67
+ const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"pattern":"test"}}}';
52
68
  const result = convertResponse(Buffer.from(input, 'utf8'));
53
69
  expect(result).toEqual([
54
- { type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'list', input: {} }
70
+ { type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: { pattern: 'test' } }
55
71
  ]);
56
72
  });
57
- it('returns text for old marker format (no backward compat)', () => {
58
- const input = '[TOOL_CALLS]search[ARGS]{"q":"test"}';
73
+ it('maps tool id 1 to read', () => {
74
+ const input = 'TOOL_CALLS{"type":"function","function":{"name":1,"parameters":{"path":"/file"}}}';
59
75
  const result = convertResponse(Buffer.from(input, 'utf8'));
60
- expect(result).toEqual([{ type: 'text', text: '[TOOL_CALLS]search[ARGS]{"q":"test"}' }]);
76
+ expect(result).toEqual([
77
+ { type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: { path: '/file' } }
78
+ ]);
61
79
  });
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"}}}{}';
80
+ it('maps tool id 2 to glob', () => {
81
+ const input = 'TOOL_CALLS{"type":"function","function":{"name":2,"parameters":{"pattern":"*.ts"}}}';
64
82
  const result = convertResponse(Buffer.from(input, 'utf8'));
65
- expect(result).toEqual([{ type: 'text', text: input }]);
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":{}}}';
@@ -275,9 +275,7 @@ function buildGenerateRequest(input) {
275
275
  const request = new ProtobufEncoder();
276
276
  request.writeMessage(1, buildMetadata(input.apiKey, input.jwt));
277
277
  const functionTools = input.tools?.filter(isFunctionTool) ?? [];
278
- const messages = functionTools.length > 0
279
- ? [{ role: 5, content: TOOL_FORMAT_INSTRUCTION }, ...input.messages]
280
- : input.messages;
278
+ const messages = [{ role: 5, content: TOOL_FORMAT_INSTRUCTION }, ...input.messages];
281
279
  for (const message of messages) {
282
280
  request.writeMessage(2, buildMessage(message));
283
281
  }
@@ -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' });
@@ -304,7 +304,7 @@ describe('DevstralLanguageModel doGenerate', () => {
304
304
  const combined = strings.join('\n');
305
305
  expect(combined).toContain('When you need to call tools');
306
306
  });
307
- it('does not inject tool format instruction when no tools provided', async () => {
307
+ it('always injects tool format instruction even without tools', 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).toContain('When you need to call tools');
325
325
  });
326
- it('does not inject tool format instruction when only provider-defined tools', async () => {
326
+ it('always injects tool format instruction even with only provider-defined tools', 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).toContain('When you need to call 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.3",
4
4
  "description": "AI SDK V3 provider for Windsurf's Devstral code search API",
5
5
  "type": "module",
6
6
  "scripts": {