@bxb1337/windsurf-fast-context 1.0.4 → 1.0.6

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.
@@ -35,7 +35,7 @@ function convertPrompt(prompt) {
35
35
  metadata: {
36
36
  toolCallId: part.toolCallId,
37
37
  toolName: part.toolName,
38
- toolArgsJson: JSON.stringify(part.args),
38
+ toolArgsJson: JSON.stringify(part.input),
39
39
  },
40
40
  });
41
41
  }
@@ -18,7 +18,7 @@ const prompt_converter_js_1 = require("./prompt-converter.js");
18
18
  type: 'tool-call',
19
19
  toolCallId: 'call_1',
20
20
  toolName: 'searchDocs',
21
- args: { query: 'prompt converter', topK: 3 },
21
+ input: { query: 'prompt converter', topK: 3 },
22
22
  },
23
23
  ],
24
24
  },
@@ -50,7 +50,7 @@ const prompt_converter_js_1 = require("./prompt-converter.js");
50
50
  role: 'assistant',
51
51
  content: [
52
52
  { type: 'text', text: 'I will call a tool now.' },
53
- { type: 'tool-call', toolCallId: 'call_2', toolName: 'searchDocs', args: { q: 'usage examples' } },
53
+ { type: 'tool-call', toolCallId: 'call_2', toolName: 'searchDocs', input: { q: 'usage examples' } },
54
54
  ],
55
55
  },
56
56
  {
@@ -6,7 +6,70 @@ const protobuf_js_1 = require("../protocol/protobuf.js");
6
6
  const TOOL_CALL_PREFIX = '[TOOL_CALLS]';
7
7
  const ARGS_PREFIX = '[ARGS]';
8
8
  const STOP_TOKEN = '</s>';
9
- const EMPTY_TOOL_CALLS_PATTERN = /TOOL_CALLS\d*(?:<\/s>)?\s*\{\s*\}/g;
9
+ const EMPTY_TOOL_CALLS_PATTERN = /TOOL_CALLS\d*(?:<\/s>)?\s*(?:\{\s*\}\s*)+/g;
10
+ // OpenAI-style TOOL_CALLS format: TOOL_CALLS{"type":"function","function":{"name":3,...}}{}...
11
+ const OPENAI_TOOL_CALLS_PATTERN = /^TOOL_CALLS(\{[\s\S]*\}\s*)+\s*$/;
12
+ function parseOpenAIToolCalls(responseText) {
13
+ if (!responseText.startsWith('TOOL_CALLS')) {
14
+ return null;
15
+ }
16
+ const jsonPart = responseText.slice('TOOL_CALLS'.length);
17
+ if (!jsonPart.startsWith('{')) {
18
+ return null;
19
+ }
20
+ const toolCalls = [];
21
+ let cursor = 0;
22
+ while (cursor < jsonPart.length) {
23
+ if (jsonPart[cursor] !== '{') {
24
+ cursor++;
25
+ continue;
26
+ }
27
+ const endResult = parseBalancedEnd(jsonPart, cursor);
28
+ if (endResult == null) {
29
+ break;
30
+ }
31
+ const jsonStr = jsonPart.slice(cursor, endResult);
32
+ cursor = endResult;
33
+ if (jsonStr === '{}') {
34
+ continue;
35
+ }
36
+ try {
37
+ const parsed = JSON.parse(jsonStr);
38
+ if (parsed.type === 'function' && parsed.function) {
39
+ toolCalls.push(parsed);
40
+ }
41
+ }
42
+ catch {
43
+ continue;
44
+ }
45
+ }
46
+ if (toolCalls.length === 0) {
47
+ return null;
48
+ }
49
+ return toolCalls.map((call, index) => {
50
+ const toolId = call.function.name;
51
+ const toolName = typeof toolId === 'number' ? mapToolIdToName(toolId) : String(toolId);
52
+ const args = call.function.parameters ?? {};
53
+ return {
54
+ type: 'tool-call',
55
+ toolCallId: `toolcall_${index + 1}`,
56
+ toolName,
57
+ input: args,
58
+ };
59
+ });
60
+ }
61
+ function mapToolIdToName(id) {
62
+ switch (id) {
63
+ case 1:
64
+ return 'read';
65
+ case 2:
66
+ return 'glob';
67
+ case 3:
68
+ return 'grep';
69
+ default:
70
+ return `tool_${id}`;
71
+ }
72
+ }
10
73
  function pushText(parts, text) {
11
74
  if (text.length > 0) {
12
75
  parts.push({ type: 'text', text });
@@ -194,6 +257,10 @@ function convertResponse(buffer) {
194
257
  let responseText = decodeResponseText(buffer);
195
258
  responseText = responseText.replace(EMPTY_TOOL_CALLS_PATTERN, '');
196
259
  responseText = responseText.replace(STOP_TOKEN, '');
260
+ const openaiToolCalls = parseOpenAIToolCalls(responseText);
261
+ if (openaiToolCalls) {
262
+ return openaiToolCalls;
263
+ }
197
264
  const parts = [];
198
265
  let cursor = 0;
199
266
  let toolCallCount = 0;
@@ -228,7 +295,7 @@ function convertResponse(buffer) {
228
295
  type: 'tool-call',
229
296
  toolCallId: `toolcall_${toolCallCount}`,
230
297
  toolName,
231
- args: parsedArgs.parsed,
298
+ input: parsedArgs.parsed,
232
299
  });
233
300
  }
234
301
  cursor = parsedArgs.endIndex;
@@ -18,7 +18,7 @@ const response_converter_js_1 = require("./response-converter.js");
18
18
  type: 'tool-call',
19
19
  toolCallId: 'toolcall_1',
20
20
  toolName: 'searchDocs',
21
- args: { query: 'prompt converter' },
21
+ input: { query: 'prompt converter' },
22
22
  },
23
23
  { type: 'text', text: ' between ' },
24
24
  { type: 'text', text: 'final answer' },
@@ -75,7 +75,7 @@ const response_converter_js_1 = require("./response-converter.js");
75
75
  (0, vitest_1.it)('strips empty TOOL_CALLS markers with whitespace', () => {
76
76
  const input = 'Text TOOL_CALLS2{ } more text';
77
77
  const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
78
- (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Text more text' }]);
78
+ (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Text more text' }]);
79
79
  });
80
80
  (0, vitest_1.it)('strips standalone stop token', () => {
81
81
  const input = 'Hello world</s>';
@@ -85,6 +85,105 @@ const response_converter_js_1 = require("./response-converter.js");
85
85
  (0, vitest_1.it)('handles TOOL_CALLS with number prefix before stop token', () => {
86
86
  const input = 'Text before TOOL_CALLS1</s>{} text after';
87
87
  const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
88
- (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Text before text after' }]);
88
+ (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Text before text after' }]);
89
+ });
90
+ (0, vitest_1.it)('strips TOOL_CALLS with double empty braces', () => {
91
+ const input = 'Hello world TOOL_CALLS1{}{}';
92
+ const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
93
+ (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Hello world ' }]);
94
+ });
95
+ (0, vitest_1.it)('strips TOOL_CALLS with triple empty braces', () => {
96
+ const input = 'Response TOOL_CALLS2{}{}{} end';
97
+ const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
98
+ (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Response end' }]);
99
+ });
100
+ (0, vitest_1.it)('strips TOOL_CALLS with stop token and multiple braces', () => {
101
+ const input = 'Text TOOL_CALLS0</s>{}{} more text';
102
+ const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
103
+ (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Text more text' }]);
104
+ });
105
+ (0, vitest_1.it)('handles empty marker followed by real tool call', () => {
106
+ const input = 'TOOL_CALLS0{} [TOOL_CALLS]searchDocs[ARGS]{"query":"test"}';
107
+ const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
108
+ (0, vitest_1.expect)(result).toEqual([
109
+ {
110
+ type: 'tool-call',
111
+ toolCallId: 'toolcall_1',
112
+ toolName: 'searchDocs',
113
+ input: { query: 'test' },
114
+ },
115
+ ]);
116
+ });
117
+ (0, vitest_1.it)('handles real tool call followed by empty marker', () => {
118
+ const input = '[TOOL_CALLS]searchDocs[ARGS]{"query":"test"} TOOL_CALLS1{} done';
119
+ const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
120
+ (0, vitest_1.expect)(result).toEqual([
121
+ {
122
+ type: 'tool-call',
123
+ toolCallId: 'toolcall_1',
124
+ toolName: 'searchDocs',
125
+ input: { query: 'test' },
126
+ },
127
+ { type: 'text', text: ' done' },
128
+ ]);
129
+ });
130
+ (0, vitest_1.it)('handles empty marker adjacent to real tool call', () => {
131
+ const input = 'TOOL_CALLS0{}[TOOL_CALLS]answer[ARGS]{"answer":"result"}';
132
+ const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
133
+ (0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'result' }]);
134
+ });
135
+ (0, vitest_1.it)('parses OpenAI-style TOOL_CALLS with numeric IDs', () => {
136
+ const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"file_path":"/home/test","search_pattern":"binance"}}}{}';
137
+ const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
138
+ (0, vitest_1.expect)(result).toEqual([
139
+ {
140
+ type: 'tool-call',
141
+ toolCallId: 'toolcall_1',
142
+ toolName: 'grep',
143
+ input: { file_path: '/home/test', search_pattern: 'binance' },
144
+ },
145
+ ]);
146
+ });
147
+ (0, vitest_1.it)('parses multiple OpenAI-style tool calls', () => {
148
+ const input = 'TOOL_CALLS{"type":"function","function":{"name":1,"parameters":{"file_path":"/home/test"}}}, {"type":"function","function":{"name":2,"parameters":{"pattern":"*.ts"}}}{}';
149
+ const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
150
+ (0, vitest_1.expect)(result).toEqual([
151
+ {
152
+ type: 'tool-call',
153
+ toolCallId: 'toolcall_1',
154
+ toolName: 'read',
155
+ input: { file_path: '/home/test' },
156
+ },
157
+ {
158
+ type: 'tool-call',
159
+ toolCallId: 'toolcall_2',
160
+ toolName: 'glob',
161
+ input: { pattern: '*.ts' },
162
+ },
163
+ ]);
164
+ });
165
+ (0, vitest_1.it)('maps unknown tool IDs to tool_N format', () => {
166
+ const input = 'TOOL_CALLS{"type":"function","function":{"name":99,"parameters":{"arg":"value"}}}{}';
167
+ const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
168
+ (0, vitest_1.expect)(result).toEqual([
169
+ {
170
+ type: 'tool-call',
171
+ toolCallId: 'toolcall_1',
172
+ toolName: 'tool_99',
173
+ input: { arg: 'value' },
174
+ },
175
+ ]);
176
+ });
177
+ (0, vitest_1.it)('handles OpenAI-style with string tool names', () => {
178
+ const input = 'TOOL_CALLS{"type":"function","function":{"name":"custom_tool","parameters":{"key":"value"}}}{}';
179
+ const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
180
+ (0, vitest_1.expect)(result).toEqual([
181
+ {
182
+ type: 'tool-call',
183
+ toolCallId: 'toolcall_1',
184
+ toolName: 'custom_tool',
185
+ input: { key: 'value' },
186
+ },
187
+ ]);
89
188
  });
90
189
  });
@@ -140,17 +140,17 @@ class DevstralLanguageModel {
140
140
  closeTextSegment();
141
141
  safeEnqueue(controller, {
142
142
  type: 'tool-input-start',
143
- toolCallId: part.toolCallId,
143
+ id: part.toolCallId,
144
144
  toolName: part.toolName,
145
145
  });
146
146
  safeEnqueue(controller, {
147
147
  type: 'tool-input-delta',
148
- toolCallId: part.toolCallId,
149
- delta: part.args,
148
+ id: part.toolCallId,
149
+ delta: part.input,
150
150
  });
151
151
  safeEnqueue(controller, {
152
152
  type: 'tool-input-end',
153
- toolCallId: part.toolCallId,
153
+ id: part.toolCallId,
154
154
  });
155
155
  safeEnqueue(controller, part);
156
156
  }
@@ -256,10 +256,9 @@ function toV3Content(parts) {
256
256
  if (part.type === 'tool-call') {
257
257
  return {
258
258
  type: 'tool-call',
259
- toolCallType: 'function',
260
259
  toolCallId: part.toolCallId,
261
260
  toolName: part.toolName,
262
- args: JSON.stringify(part.args),
261
+ input: JSON.stringify(part.input),
263
262
  };
264
263
  }
265
264
  return part;
@@ -190,10 +190,9 @@ async function collectStreamParts(stream) {
190
190
  (0, vitest_1.expect)(result.content).toEqual([
191
191
  {
192
192
  type: 'tool-call',
193
- toolCallType: 'function',
194
193
  toolCallId: 'toolcall_1',
195
194
  toolName: 'searchRepo',
196
- args: '{"query":"jwt manager"}',
195
+ input: '{"query":"jwt manager"}',
197
196
  },
198
197
  ]);
199
198
  const strings = (0, protobuf_js_1.extractStrings)(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
@@ -358,10 +357,9 @@ async function collectStreamParts(stream) {
358
357
  (0, vitest_1.expect)(parts[3]).toMatchObject({ type: 'tool-input-delta', delta: '{"query":"jwt manager"}' });
359
358
  (0, vitest_1.expect)(parts[5]).toEqual({
360
359
  type: 'tool-call',
361
- toolCallType: 'function',
362
360
  toolCallId: 'toolcall_1',
363
361
  toolName: 'searchRepo',
364
- args: '{"query":"jwt manager"}',
362
+ input: '{"query":"jwt manager"}',
365
363
  });
366
364
  });
367
365
  (0, vitest_1.it)('abort stops stream mid-response', async () => {
@@ -24,7 +24,7 @@ export type LanguageModelV3Prompt = Array<{
24
24
  type: 'tool-call';
25
25
  toolCallId: string;
26
26
  toolName: string;
27
- args: unknown;
27
+ input: unknown;
28
28
  } | {
29
29
  type: 'file';
30
30
  data: string;
@@ -6,7 +6,7 @@ export interface ToolCallPart {
6
6
  type: 'tool-call';
7
7
  toolCallId: string;
8
8
  toolName: string;
9
- args: unknown;
9
+ input: unknown;
10
10
  }
11
11
  export type LanguageModelV3Content = TextPart | ToolCallPart;
12
12
  export declare function convertResponse(buffer: Buffer): LanguageModelV3Content[];
@@ -32,7 +32,7 @@ export function convertPrompt(prompt) {
32
32
  metadata: {
33
33
  toolCallId: part.toolCallId,
34
34
  toolName: part.toolName,
35
- toolArgsJson: JSON.stringify(part.args),
35
+ toolArgsJson: JSON.stringify(part.input),
36
36
  },
37
37
  });
38
38
  }
@@ -16,7 +16,7 @@ describe('convertPrompt', () => {
16
16
  type: 'tool-call',
17
17
  toolCallId: 'call_1',
18
18
  toolName: 'searchDocs',
19
- args: { query: 'prompt converter', topK: 3 },
19
+ input: { query: 'prompt converter', topK: 3 },
20
20
  },
21
21
  ],
22
22
  },
@@ -48,7 +48,7 @@ describe('convertPrompt', () => {
48
48
  role: 'assistant',
49
49
  content: [
50
50
  { type: 'text', text: 'I will call a tool now.' },
51
- { type: 'tool-call', toolCallId: 'call_2', toolName: 'searchDocs', args: { q: 'usage examples' } },
51
+ { type: 'tool-call', toolCallId: 'call_2', toolName: 'searchDocs', input: { q: 'usage examples' } },
52
52
  ],
53
53
  },
54
54
  {
@@ -3,7 +3,70 @@ import { extractStrings } from '../protocol/protobuf.js';
3
3
  const TOOL_CALL_PREFIX = '[TOOL_CALLS]';
4
4
  const ARGS_PREFIX = '[ARGS]';
5
5
  const STOP_TOKEN = '</s>';
6
- const EMPTY_TOOL_CALLS_PATTERN = /TOOL_CALLS\d*(?:<\/s>)?\s*\{\s*\}/g;
6
+ const EMPTY_TOOL_CALLS_PATTERN = /TOOL_CALLS\d*(?:<\/s>)?\s*(?:\{\s*\}\s*)+/g;
7
+ // OpenAI-style TOOL_CALLS format: TOOL_CALLS{"type":"function","function":{"name":3,...}}{}...
8
+ const OPENAI_TOOL_CALLS_PATTERN = /^TOOL_CALLS(\{[\s\S]*\}\s*)+\s*$/;
9
+ function parseOpenAIToolCalls(responseText) {
10
+ if (!responseText.startsWith('TOOL_CALLS')) {
11
+ return null;
12
+ }
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) {
44
+ return null;
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
+ }
69
+ }
7
70
  function pushText(parts, text) {
8
71
  if (text.length > 0) {
9
72
  parts.push({ type: 'text', text });
@@ -191,6 +254,10 @@ export function convertResponse(buffer) {
191
254
  let responseText = decodeResponseText(buffer);
192
255
  responseText = responseText.replace(EMPTY_TOOL_CALLS_PATTERN, '');
193
256
  responseText = responseText.replace(STOP_TOKEN, '');
257
+ const openaiToolCalls = parseOpenAIToolCalls(responseText);
258
+ if (openaiToolCalls) {
259
+ return openaiToolCalls;
260
+ }
194
261
  const parts = [];
195
262
  let cursor = 0;
196
263
  let toolCallCount = 0;
@@ -225,7 +292,7 @@ export function convertResponse(buffer) {
225
292
  type: 'tool-call',
226
293
  toolCallId: `toolcall_${toolCallCount}`,
227
294
  toolName,
228
- args: parsedArgs.parsed,
295
+ input: parsedArgs.parsed,
229
296
  });
230
297
  }
231
298
  cursor = parsedArgs.endIndex;
@@ -16,7 +16,7 @@ describe('convertResponse', () => {
16
16
  type: 'tool-call',
17
17
  toolCallId: 'toolcall_1',
18
18
  toolName: 'searchDocs',
19
- args: { query: 'prompt converter' },
19
+ input: { query: 'prompt converter' },
20
20
  },
21
21
  { type: 'text', text: ' between ' },
22
22
  { type: 'text', text: 'final answer' },
@@ -73,7 +73,7 @@ describe('convertResponse', () => {
73
73
  it('strips empty TOOL_CALLS markers with whitespace', () => {
74
74
  const input = 'Text TOOL_CALLS2{ } more text';
75
75
  const result = convertResponse(Buffer.from(input, 'utf8'));
76
- expect(result).toEqual([{ type: 'text', text: 'Text more text' }]);
76
+ expect(result).toEqual([{ type: 'text', text: 'Text more text' }]);
77
77
  });
78
78
  it('strips standalone stop token', () => {
79
79
  const input = 'Hello world</s>';
@@ -83,6 +83,105 @@ describe('convertResponse', () => {
83
83
  it('handles TOOL_CALLS with number prefix before stop token', () => {
84
84
  const input = 'Text before TOOL_CALLS1</s>{} text after';
85
85
  const result = convertResponse(Buffer.from(input, 'utf8'));
86
- expect(result).toEqual([{ type: 'text', text: 'Text before text after' }]);
86
+ expect(result).toEqual([{ type: 'text', text: 'Text before text after' }]);
87
+ });
88
+ it('strips TOOL_CALLS with double empty braces', () => {
89
+ const input = 'Hello world TOOL_CALLS1{}{}';
90
+ const result = convertResponse(Buffer.from(input, 'utf8'));
91
+ expect(result).toEqual([{ type: 'text', text: 'Hello world ' }]);
92
+ });
93
+ it('strips TOOL_CALLS with triple empty braces', () => {
94
+ const input = 'Response TOOL_CALLS2{}{}{} end';
95
+ const result = convertResponse(Buffer.from(input, 'utf8'));
96
+ expect(result).toEqual([{ type: 'text', text: 'Response end' }]);
97
+ });
98
+ it('strips TOOL_CALLS with stop token and multiple braces', () => {
99
+ const input = 'Text TOOL_CALLS0</s>{}{} more text';
100
+ const result = convertResponse(Buffer.from(input, 'utf8'));
101
+ expect(result).toEqual([{ type: 'text', text: 'Text more text' }]);
102
+ });
103
+ it('handles empty marker followed by real tool call', () => {
104
+ const input = 'TOOL_CALLS0{} [TOOL_CALLS]searchDocs[ARGS]{"query":"test"}';
105
+ const result = convertResponse(Buffer.from(input, 'utf8'));
106
+ expect(result).toEqual([
107
+ {
108
+ type: 'tool-call',
109
+ toolCallId: 'toolcall_1',
110
+ toolName: 'searchDocs',
111
+ input: { query: 'test' },
112
+ },
113
+ ]);
114
+ });
115
+ it('handles real tool call followed by empty marker', () => {
116
+ const input = '[TOOL_CALLS]searchDocs[ARGS]{"query":"test"} TOOL_CALLS1{} done';
117
+ const result = convertResponse(Buffer.from(input, 'utf8'));
118
+ expect(result).toEqual([
119
+ {
120
+ type: 'tool-call',
121
+ toolCallId: 'toolcall_1',
122
+ toolName: 'searchDocs',
123
+ input: { query: 'test' },
124
+ },
125
+ { type: 'text', text: ' done' },
126
+ ]);
127
+ });
128
+ it('handles empty marker adjacent to real tool call', () => {
129
+ const input = 'TOOL_CALLS0{}[TOOL_CALLS]answer[ARGS]{"answer":"result"}';
130
+ const result = convertResponse(Buffer.from(input, 'utf8'));
131
+ expect(result).toEqual([{ type: 'text', text: 'result' }]);
132
+ });
133
+ it('parses OpenAI-style TOOL_CALLS with numeric IDs', () => {
134
+ const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"file_path":"/home/test","search_pattern":"binance"}}}{}';
135
+ const result = convertResponse(Buffer.from(input, 'utf8'));
136
+ expect(result).toEqual([
137
+ {
138
+ type: 'tool-call',
139
+ toolCallId: 'toolcall_1',
140
+ toolName: 'grep',
141
+ input: { file_path: '/home/test', search_pattern: 'binance' },
142
+ },
143
+ ]);
144
+ });
145
+ it('parses multiple OpenAI-style tool calls', () => {
146
+ const input = 'TOOL_CALLS{"type":"function","function":{"name":1,"parameters":{"file_path":"/home/test"}}}, {"type":"function","function":{"name":2,"parameters":{"pattern":"*.ts"}}}{}';
147
+ const result = convertResponse(Buffer.from(input, 'utf8'));
148
+ expect(result).toEqual([
149
+ {
150
+ type: 'tool-call',
151
+ toolCallId: 'toolcall_1',
152
+ toolName: 'read',
153
+ input: { file_path: '/home/test' },
154
+ },
155
+ {
156
+ type: 'tool-call',
157
+ toolCallId: 'toolcall_2',
158
+ toolName: 'glob',
159
+ input: { pattern: '*.ts' },
160
+ },
161
+ ]);
162
+ });
163
+ it('maps unknown tool IDs to tool_N format', () => {
164
+ const input = 'TOOL_CALLS{"type":"function","function":{"name":99,"parameters":{"arg":"value"}}}{}';
165
+ const result = convertResponse(Buffer.from(input, 'utf8'));
166
+ expect(result).toEqual([
167
+ {
168
+ type: 'tool-call',
169
+ toolCallId: 'toolcall_1',
170
+ toolName: 'tool_99',
171
+ input: { arg: 'value' },
172
+ },
173
+ ]);
174
+ });
175
+ it('handles OpenAI-style with string tool names', () => {
176
+ const input = 'TOOL_CALLS{"type":"function","function":{"name":"custom_tool","parameters":{"key":"value"}}}{}';
177
+ const result = convertResponse(Buffer.from(input, 'utf8'));
178
+ expect(result).toEqual([
179
+ {
180
+ type: 'tool-call',
181
+ toolCallId: 'toolcall_1',
182
+ toolName: 'custom_tool',
183
+ input: { key: 'value' },
184
+ },
185
+ ]);
87
186
  });
88
187
  });
@@ -137,17 +137,17 @@ export class DevstralLanguageModel {
137
137
  closeTextSegment();
138
138
  safeEnqueue(controller, {
139
139
  type: 'tool-input-start',
140
- toolCallId: part.toolCallId,
140
+ id: part.toolCallId,
141
141
  toolName: part.toolName,
142
142
  });
143
143
  safeEnqueue(controller, {
144
144
  type: 'tool-input-delta',
145
- toolCallId: part.toolCallId,
146
- delta: part.args,
145
+ id: part.toolCallId,
146
+ delta: part.input,
147
147
  });
148
148
  safeEnqueue(controller, {
149
149
  type: 'tool-input-end',
150
- toolCallId: part.toolCallId,
150
+ id: part.toolCallId,
151
151
  });
152
152
  safeEnqueue(controller, part);
153
153
  }
@@ -252,10 +252,9 @@ function toV3Content(parts) {
252
252
  if (part.type === 'tool-call') {
253
253
  return {
254
254
  type: 'tool-call',
255
- toolCallType: 'function',
256
255
  toolCallId: part.toolCallId,
257
256
  toolName: part.toolName,
258
- args: JSON.stringify(part.args),
257
+ input: JSON.stringify(part.input),
259
258
  };
260
259
  }
261
260
  return part;
@@ -188,10 +188,9 @@ describe('DevstralLanguageModel doGenerate', () => {
188
188
  expect(result.content).toEqual([
189
189
  {
190
190
  type: 'tool-call',
191
- toolCallType: 'function',
192
191
  toolCallId: 'toolcall_1',
193
192
  toolName: 'searchRepo',
194
- args: '{"query":"jwt manager"}',
193
+ input: '{"query":"jwt manager"}',
195
194
  },
196
195
  ]);
197
196
  const strings = extractStrings(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
@@ -356,10 +355,9 @@ describe('DevstralLanguageModel doStream', () => {
356
355
  expect(parts[3]).toMatchObject({ type: 'tool-input-delta', delta: '{"query":"jwt manager"}' });
357
356
  expect(parts[5]).toEqual({
358
357
  type: 'tool-call',
359
- toolCallType: 'function',
360
358
  toolCallId: 'toolcall_1',
361
359
  toolName: 'searchRepo',
362
- args: '{"query":"jwt manager"}',
360
+ input: '{"query":"jwt manager"}',
363
361
  });
364
362
  });
365
363
  it('abort stops stream mid-response', async () => {
@@ -34,10 +34,9 @@ export interface LanguageModelV3StreamResult {
34
34
  }
35
35
  interface LanguageModelV3ToolCallContent {
36
36
  type: 'tool-call';
37
- toolCallType: 'function';
38
37
  toolCallId: string;
39
38
  toolName: string;
40
- args: string;
39
+ input: string;
41
40
  }
42
41
  type GenerateContentPart = {
43
42
  type: 'text';
@@ -61,15 +60,15 @@ type LanguageModelV3StreamPart = {
61
60
  id: string;
62
61
  } | {
63
62
  type: 'tool-input-start';
64
- toolCallId: string;
63
+ id: string;
65
64
  toolName: string;
66
65
  } | {
67
66
  type: 'tool-input-delta';
68
- toolCallId: string;
67
+ id: string;
69
68
  delta: string;
70
69
  } | {
71
70
  type: 'tool-input-end';
72
- toolCallId: string;
71
+ id: string;
73
72
  } | LanguageModelV3ToolCallContent | {
74
73
  type: 'error';
75
74
  error: unknown;
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "@bxb1337/windsurf-fast-context",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "AI SDK V3 provider for Windsurf's Devstral code search API",
5
5
  "type": "module",
6
+ "scripts": {
7
+ "test": "vitest --run",
8
+ "build:types": "tsc -p tsconfig.types.json",
9
+ "build:esm": "tsc -p tsconfig.json",
10
+ "build:cjs": "tsc -p tsconfig.cjs.json",
11
+ "build": "pnpm -s run build:types && pnpm -s run build:esm && pnpm -s run build:cjs && node ./scripts/postbuild.js",
12
+ "typecheck": "tsc -p tsconfig.json --noEmit"
13
+ },
6
14
  "keywords": [
7
15
  "windsurf",
8
16
  "devstral",
@@ -17,6 +25,9 @@
17
25
  "type": "git",
18
26
  "url": "https://github.com/bxb1337/windsurf-fast-context"
19
27
  },
28
+ "dependencies": {
29
+ "@ai-sdk/provider": "^3.0.0"
30
+ },
20
31
  "devDependencies": {
21
32
  "ai": "^6.0.116",
22
33
  "typescript": "^5.0.0",
@@ -39,13 +50,5 @@
39
50
  "dist",
40
51
  "README.md",
41
52
  "LICENSE"
42
- ],
43
- "scripts": {
44
- "test": "vitest --run",
45
- "build:types": "tsc -p tsconfig.types.json",
46
- "build:esm": "tsc -p tsconfig.json",
47
- "build:cjs": "tsc -p tsconfig.cjs.json",
48
- "build": "pnpm -s run build:types && pnpm -s run build:esm && pnpm -s run build:cjs && node ./scripts/postbuild.js",
49
- "typecheck": "tsc -p tsconfig.json --noEmit"
50
- }
51
- }
53
+ ]
54
+ }