@bxb1337/windsurf-fast-context 1.0.6 → 1.0.8

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.
@@ -69,6 +69,9 @@ function decodeRequestPayload(body) {
69
69
  const decodedFrames = (0, connect_frame_js_1.connectFrameDecode)(body);
70
70
  return decodedFrames[0] ?? Buffer.alloc(0);
71
71
  }
72
+ function extractToolsPayload(strings) {
73
+ return strings.find((value) => value.startsWith('[{"type":"function"'));
74
+ }
72
75
  async function collectStreamParts(stream) {
73
76
  const reader = stream.getReader();
74
77
  const parts = [];
@@ -136,7 +139,7 @@ async function collectStreamParts(stream) {
136
139
  prompt: [{ role: 'user', content: [{ type: 'text', text: 'Find auth logic.' }] }],
137
140
  });
138
141
  (0, vitest_1.expect)(result.content).toEqual([{ type: 'text', text: 'generated answer' }]);
139
- (0, vitest_1.expect)(result.finishReason).toBe('stop');
142
+ (0, vitest_1.expect)(result.finishReason).toEqual({ unified: 'stop', raw: undefined });
140
143
  (0, vitest_1.expect)(result.usage).toEqual({
141
144
  inputTokens: {
142
145
  total: undefined,
@@ -159,7 +162,7 @@ async function collectStreamParts(stream) {
159
162
  (0, vitest_1.expect)(combined).toContain(jwt);
160
163
  (0, vitest_1.expect)(combined).toContain('Find auth logic.');
161
164
  });
162
- (0, vitest_1.it)('generate request with tool markers returns tool-call content parts', async () => {
165
+ (0, vitest_1.it)('doGenerate accepts function tools array with inputSchema', async () => {
163
166
  const requestBodies = [];
164
167
  const jwt = makeJwt(4_200_000_000, 'tools');
165
168
  const fakeFetch = async (input, init) => {
@@ -174,10 +177,12 @@ async function collectStreamParts(stream) {
174
177
  const model = new devstral_language_model_js_1.DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
175
178
  const result = await model.doGenerate({
176
179
  prompt: [{ role: 'user', content: [{ type: 'text', text: 'Inspect jwt manager.' }] }],
177
- tools: {
178
- searchRepo: {
180
+ tools: [
181
+ {
182
+ type: 'function',
183
+ name: 'searchRepo',
179
184
  description: 'Search repository files',
180
- parameters: {
185
+ inputSchema: {
181
186
  type: 'object',
182
187
  properties: {
183
188
  query: { type: 'string' },
@@ -185,7 +190,7 @@ async function collectStreamParts(stream) {
185
190
  required: ['query'],
186
191
  },
187
192
  },
188
- },
193
+ ],
189
194
  });
190
195
  (0, vitest_1.expect)(result.content).toEqual([
191
196
  {
@@ -195,12 +200,88 @@ async function collectStreamParts(stream) {
195
200
  input: '{"query":"jwt manager"}',
196
201
  },
197
202
  ]);
203
+ (0, vitest_1.expect)(result.finishReason).toEqual({ unified: 'tool-calls', raw: undefined });
198
204
  const strings = (0, protobuf_js_1.extractStrings)(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
199
- const combined = strings.join('\n');
200
- (0, vitest_1.expect)(combined).toContain('searchRepo');
201
- (0, vitest_1.expect)(combined).toContain('Search repository files');
202
- (0, vitest_1.expect)(combined).toContain('Inspect jwt manager.');
203
- (0, vitest_1.expect)(combined).toContain('query');
205
+ const toolsPayload = extractToolsPayload(strings);
206
+ (0, vitest_1.expect)(toolsPayload).toBeDefined();
207
+ const parsedTools = JSON.parse(toolsPayload ?? '[]');
208
+ (0, vitest_1.expect)(parsedTools).toEqual([
209
+ {
210
+ type: 'function',
211
+ function: {
212
+ name: 'searchRepo',
213
+ description: 'Search repository files',
214
+ parameters: {
215
+ type: 'object',
216
+ properties: {
217
+ query: { type: 'string' },
218
+ },
219
+ required: ['query'],
220
+ },
221
+ },
222
+ },
223
+ ]);
224
+ });
225
+ (0, vitest_1.it)('filters provider tools from tools array serialization', async () => {
226
+ const requestBodies = [];
227
+ const jwt = makeJwt(4_200_000_001, 'provider-tools');
228
+ const fakeFetch = async (input, init) => {
229
+ const url = String(input);
230
+ if (url.endsWith('/GetUserJwt')) {
231
+ return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
232
+ }
233
+ requestBodies.push(bufferFromBody(init?.body));
234
+ return new Response(Uint8Array.from((0, connect_frame_js_1.connectFrameEncode)(Buffer.from('ok', 'utf8'))), { status: 200 });
235
+ };
236
+ const model = new devstral_language_model_js_1.DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
237
+ await model.doGenerate({
238
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'Filter provider tools.' }] }],
239
+ tools: [
240
+ {
241
+ type: 'function',
242
+ name: 'searchRepo',
243
+ description: 'Search repository files',
244
+ inputSchema: {
245
+ type: 'object',
246
+ properties: {
247
+ query: { type: 'string' },
248
+ },
249
+ required: ['query'],
250
+ },
251
+ },
252
+ {
253
+ type: 'provider',
254
+ id: 'windsurf.restricted_exec',
255
+ name: 'restricted_exec',
256
+ args: { mode: 'read-only' },
257
+ },
258
+ ],
259
+ });
260
+ const strings = (0, protobuf_js_1.extractStrings)(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
261
+ const toolsPayload = extractToolsPayload(strings);
262
+ (0, vitest_1.expect)(toolsPayload).toBeDefined();
263
+ const parsedTools = JSON.parse(toolsPayload ?? '[]');
264
+ (0, vitest_1.expect)(parsedTools).toHaveLength(1);
265
+ (0, vitest_1.expect)(parsedTools[0]).toMatchObject({ function: { name: 'searchRepo' } });
266
+ });
267
+ (0, vitest_1.it)('handles empty tools array without serializing tool payload', async () => {
268
+ const requestBodies = [];
269
+ const jwt = makeJwt(4_200_000_002, 'empty-tools');
270
+ const fakeFetch = async (input, init) => {
271
+ const url = String(input);
272
+ if (url.endsWith('/GetUserJwt')) {
273
+ return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
274
+ }
275
+ requestBodies.push(bufferFromBody(init?.body));
276
+ return new Response(Uint8Array.from((0, connect_frame_js_1.connectFrameEncode)(Buffer.from('ok', 'utf8'))), { status: 200 });
277
+ };
278
+ const model = new devstral_language_model_js_1.DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
279
+ await model.doGenerate({
280
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'No tools this turn.' }] }],
281
+ tools: [],
282
+ });
283
+ const strings = (0, protobuf_js_1.extractStrings)(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
284
+ (0, vitest_1.expect)(extractToolsPayload(strings)).toBeUndefined();
204
285
  });
205
286
  });
206
287
  (0, vitest_1.describe)('DevstralLanguageModel doStream', () => {
@@ -289,8 +370,10 @@ async function collectStreamParts(stream) {
289
370
  (0, vitest_1.expect)(parts[4]).toMatchObject({ type: 'text-delta', delta: 'world' });
290
371
  (0, vitest_1.expect)(parts[6]).toEqual({
291
372
  type: 'finish',
292
- finishReason: 'stop',
293
- rawFinishReason: 'stop',
373
+ finishReason: {
374
+ unified: 'stop',
375
+ raw: undefined,
376
+ },
294
377
  usage: {
295
378
  inputTokens: {
296
379
  total: undefined,
@@ -331,10 +414,12 @@ async function collectStreamParts(stream) {
331
414
  });
332
415
  const result = await model.doStream({
333
416
  prompt: [{ role: 'user', content: [{ type: 'text', text: 'Call tools.' }] }],
334
- tools: {
335
- searchRepo: {
417
+ tools: [
418
+ {
419
+ type: 'function',
420
+ name: 'searchRepo',
336
421
  description: 'Search repository files',
337
- parameters: {
422
+ inputSchema: {
338
423
  type: 'object',
339
424
  properties: {
340
425
  query: { type: 'string' },
@@ -342,7 +427,7 @@ async function collectStreamParts(stream) {
342
427
  required: ['query'],
343
428
  },
344
429
  },
345
- },
430
+ ],
346
431
  });
347
432
  const parts = await collectStreamParts(result.stream);
348
433
  (0, vitest_1.expect)(parts.map((part) => part.type)).toEqual([
@@ -361,6 +446,26 @@ async function collectStreamParts(stream) {
361
446
  toolName: 'searchRepo',
362
447
  input: '{"query":"jwt manager"}',
363
448
  });
449
+ (0, vitest_1.expect)(parts[6]).toEqual({
450
+ type: 'finish',
451
+ finishReason: {
452
+ unified: 'tool-calls',
453
+ raw: undefined,
454
+ },
455
+ usage: {
456
+ inputTokens: {
457
+ total: undefined,
458
+ noCache: undefined,
459
+ cacheRead: undefined,
460
+ cacheWrite: undefined,
461
+ },
462
+ outputTokens: {
463
+ total: undefined,
464
+ text: undefined,
465
+ reasoning: undefined,
466
+ },
467
+ },
468
+ });
364
469
  });
365
470
  (0, vitest_1.it)('abort stops stream mid-response', async () => {
366
471
  const controller = new AbortController();
@@ -1,49 +1,3 @@
1
+ import type { LanguageModelV3Prompt } from '@ai-sdk/provider';
1
2
  import type { DevstralMessage } from '../types/index.js';
2
- export type LanguageModelV3Prompt = Array<{
3
- role: 'system';
4
- content: string;
5
- } | {
6
- role: 'user';
7
- content: Array<{
8
- type: 'text';
9
- text: string;
10
- } | {
11
- type: 'file';
12
- data: string;
13
- mediaType: string;
14
- } | {
15
- type: 'image';
16
- image: string;
17
- }>;
18
- } | {
19
- role: 'assistant';
20
- content: Array<{
21
- type: 'text';
22
- text: string;
23
- } | {
24
- type: 'tool-call';
25
- toolCallId: string;
26
- toolName: string;
27
- input: unknown;
28
- } | {
29
- type: 'file';
30
- data: string;
31
- mediaType: string;
32
- } | {
33
- type: 'image';
34
- image: string;
35
- } | {
36
- type: 'reasoning';
37
- text: string;
38
- }>;
39
- } | {
40
- role: 'tool';
41
- content: Array<{
42
- type: 'tool-result';
43
- toolCallId: string;
44
- toolName: string;
45
- result: unknown;
46
- isError?: boolean;
47
- }>;
48
- }>;
49
3
  export declare function convertPrompt(prompt: LanguageModelV3Prompt): DevstralMessage[];
@@ -1,8 +1,22 @@
1
- function toContentString(value) {
2
- if (typeof value === 'string') {
3
- return value;
1
+ function toolOutputToString(output) {
2
+ switch (output.type) {
3
+ case 'text':
4
+ case 'error-text':
5
+ return output.value;
6
+ case 'json':
7
+ case 'error-json':
8
+ return JSON.stringify(output.value);
9
+ case 'execution-denied': {
10
+ const obj = { type: 'execution-denied' };
11
+ if (output.reason !== undefined)
12
+ obj.reason = output.reason;
13
+ return JSON.stringify(obj);
14
+ }
15
+ case 'content':
16
+ return JSON.stringify(output.value);
17
+ default:
18
+ return JSON.stringify(output);
4
19
  }
5
- return JSON.stringify(value);
6
20
  }
7
21
  export function convertPrompt(prompt) {
8
22
  const messages = [];
@@ -39,13 +53,14 @@ export function convertPrompt(prompt) {
39
53
  }
40
54
  continue;
41
55
  }
42
- // Tool result messages - use refCallId to reference the original tool call
43
56
  for (const part of message.content) {
57
+ if (part.type !== 'tool-result')
58
+ continue;
44
59
  messages.push({
45
60
  role: 4,
46
- content: toContentString(part.result),
61
+ content: toolOutputToString(part.output),
47
62
  metadata: {
48
- refCallId: part.toolCallId, // This links back to the tool call
63
+ refCallId: part.toolCallId,
49
64
  },
50
65
  });
51
66
  }
@@ -34,7 +34,7 @@ describe('convertPrompt', () => {
34
34
  },
35
35
  });
36
36
  });
37
- it('multi-turn preserves ordering across mixed roles', () => {
37
+ it('multi-turn preserves ordering across mixed roles (V3 output shape)', () => {
38
38
  const prompt = [
39
39
  { role: 'system', content: 'System instruction' },
40
40
  {
@@ -58,7 +58,7 @@ describe('convertPrompt', () => {
58
58
  type: 'tool-result',
59
59
  toolCallId: 'call_2',
60
60
  toolName: 'searchDocs',
61
- result: { hits: ['a.ts', 'b.ts'] },
61
+ output: { type: 'json', value: { hits: ['a.ts', 'b.ts'] } },
62
62
  },
63
63
  ],
64
64
  },
@@ -90,4 +90,119 @@ describe('convertPrompt', () => {
90
90
  ]);
91
91
  expect(prompt).toEqual(snapshot);
92
92
  });
93
+ it('tool-result with json output serializes value to JSON', () => {
94
+ const prompt = [
95
+ {
96
+ role: 'tool',
97
+ content: [
98
+ {
99
+ type: 'tool-result',
100
+ toolCallId: 'call_json',
101
+ toolName: 'searchTool',
102
+ output: { type: 'json', value: { files: ['x.ts', 'y.ts'], count: 2 } },
103
+ },
104
+ ],
105
+ },
106
+ ];
107
+ const result = convertPrompt(prompt);
108
+ expect(result).toEqual([
109
+ {
110
+ role: 4,
111
+ content: '{"files":["x.ts","y.ts"],"count":2}',
112
+ metadata: { refCallId: 'call_json' },
113
+ },
114
+ ]);
115
+ });
116
+ it('tool-result with text output uses string directly', () => {
117
+ const prompt = [
118
+ {
119
+ role: 'tool',
120
+ content: [
121
+ {
122
+ type: 'tool-result',
123
+ toolCallId: 'call_text',
124
+ toolName: 'readFile',
125
+ output: { type: 'text', value: 'Operation completed successfully' },
126
+ },
127
+ ],
128
+ },
129
+ ];
130
+ const result = convertPrompt(prompt);
131
+ expect(result).toEqual([
132
+ {
133
+ role: 4,
134
+ content: 'Operation completed successfully',
135
+ metadata: { refCallId: 'call_text' },
136
+ },
137
+ ]);
138
+ });
139
+ it('tool-result with error-text output serializes error message', () => {
140
+ const prompt = [
141
+ {
142
+ role: 'tool',
143
+ content: [
144
+ {
145
+ type: 'tool-result',
146
+ toolCallId: 'call_error',
147
+ toolName: 'executeCommand',
148
+ output: { type: 'error-text', value: 'Tool execution failed: timeout' },
149
+ },
150
+ ],
151
+ },
152
+ ];
153
+ const result = convertPrompt(prompt);
154
+ expect(result).toEqual([
155
+ {
156
+ role: 4,
157
+ content: 'Tool execution failed: timeout',
158
+ metadata: { refCallId: 'call_error' },
159
+ },
160
+ ]);
161
+ });
162
+ it('tool-result with execution-denied output includes reason', () => {
163
+ const prompt = [
164
+ {
165
+ role: 'tool',
166
+ content: [
167
+ {
168
+ type: 'tool-result',
169
+ toolCallId: 'call_denied',
170
+ toolName: 'dangerousAction',
171
+ output: { type: 'execution-denied', reason: 'User rejected tool execution' },
172
+ },
173
+ ],
174
+ },
175
+ ];
176
+ const result = convertPrompt(prompt);
177
+ expect(result).toEqual([
178
+ {
179
+ role: 4,
180
+ content: '{"type":"execution-denied","reason":"User rejected tool execution"}',
181
+ metadata: { refCallId: 'call_denied' },
182
+ },
183
+ ]);
184
+ });
185
+ it('tool-result with execution-denied output handles missing reason', () => {
186
+ const prompt = [
187
+ {
188
+ role: 'tool',
189
+ content: [
190
+ {
191
+ type: 'tool-result',
192
+ toolCallId: 'call_denied_no_reason',
193
+ toolName: 'someTool',
194
+ output: { type: 'execution-denied' },
195
+ },
196
+ ],
197
+ },
198
+ ];
199
+ const result = convertPrompt(prompt);
200
+ expect(result).toEqual([
201
+ {
202
+ role: 4,
203
+ content: '{"type":"execution-denied"}',
204
+ metadata: { refCallId: 'call_denied_no_reason' },
205
+ },
206
+ ]);
207
+ });
93
208
  });
@@ -3,9 +3,6 @@ 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*\}\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
6
  function parseOpenAIToolCalls(responseText) {
10
7
  if (!responseText.startsWith('TOOL_CALLS')) {
11
8
  return null;
@@ -72,20 +69,6 @@ function pushText(parts, text) {
72
69
  parts.push({ type: 'text', text });
73
70
  }
74
71
  }
75
- function extractAnswerText(args) {
76
- if (typeof args === 'string') {
77
- return args;
78
- }
79
- if (args != null && typeof args === 'object') {
80
- if ('answer' in args && typeof args.answer === 'string') {
81
- return args.answer;
82
- }
83
- if ('text' in args && typeof args.text === 'string') {
84
- return args.text;
85
- }
86
- }
87
- return JSON.stringify(args);
88
- }
89
72
  function parseStringEnd(value, startIndex) {
90
73
  let index = startIndex + 1;
91
74
  let escaping = false;
@@ -252,7 +235,6 @@ function decodeResponseText(buffer) {
252
235
  }
253
236
  export function convertResponse(buffer) {
254
237
  let responseText = decodeResponseText(buffer);
255
- responseText = responseText.replace(EMPTY_TOOL_CALLS_PATTERN, '');
256
238
  responseText = responseText.replace(STOP_TOKEN, '');
257
239
  const openaiToolCalls = parseOpenAIToolCalls(responseText);
258
240
  if (openaiToolCalls) {
@@ -283,18 +265,13 @@ export function convertResponse(buffer) {
283
265
  cursor = malformedEnd;
284
266
  continue;
285
267
  }
286
- if (toolName === 'answer') {
287
- pushText(parts, extractAnswerText(parsedArgs.parsed));
288
- }
289
- else {
290
- toolCallCount += 1;
291
- parts.push({
292
- type: 'tool-call',
293
- toolCallId: `toolcall_${toolCallCount}`,
294
- toolName,
295
- input: parsedArgs.parsed,
296
- });
297
- }
268
+ toolCallCount += 1;
269
+ parts.push({
270
+ type: 'tool-call',
271
+ toolCallId: `toolcall_${toolCallCount}`,
272
+ toolName,
273
+ input: parsedArgs.parsed,
274
+ });
298
275
  cursor = parsedArgs.endIndex;
299
276
  }
300
277
  return parts;
@@ -19,7 +19,12 @@ describe('convertResponse', () => {
19
19
  input: { query: 'prompt converter' },
20
20
  },
21
21
  { type: 'text', text: ' between ' },
22
- { type: 'text', text: 'final answer' },
22
+ {
23
+ type: 'tool-call',
24
+ toolCallId: 'toolcall_2',
25
+ toolName: 'answer',
26
+ input: { answer: 'final answer' },
27
+ },
23
28
  { type: 'text', text: ' after' },
24
29
  ]);
25
30
  });
@@ -46,7 +51,14 @@ describe('convertResponse', () => {
46
51
  payload.writeVarint(1, 150);
47
52
  payload.writeString(2, '[TOOL_CALLS]answer[ARGS]{"answer":"final answer"}');
48
53
  const result = convertResponse(payload.toBuffer());
49
- expect(result).toEqual([{ type: 'text', text: 'final answer' }]);
54
+ expect(result).toEqual([
55
+ {
56
+ type: 'tool-call',
57
+ toolCallId: 'toolcall_1',
58
+ toolName: 'answer',
59
+ input: { answer: 'final answer' },
60
+ },
61
+ ]);
50
62
  });
51
63
  it('protobuf payload - ignores metadata strings and keeps main text field', () => {
52
64
  const payload = new ProtobufEncoder();
@@ -60,76 +72,11 @@ describe('convertResponse', () => {
60
72
  const result = convertResponse(compressed);
61
73
  expect(result).toEqual([{ type: 'text', text: 'hello from gzip' }]);
62
74
  });
63
- it('strips empty TOOL_CALLS markers with stop token', () => {
64
- const input = 'Hello world TOOL_CALLS0</s>{}';
65
- const result = convertResponse(Buffer.from(input, 'utf8'));
66
- expect(result).toEqual([{ type: 'text', text: 'Hello world ' }]);
67
- });
68
- it('strips empty TOOL_CALLS markers without stop token', () => {
69
- const input = 'Hello world TOOL_CALLS1{}';
70
- const result = convertResponse(Buffer.from(input, 'utf8'));
71
- expect(result).toEqual([{ type: 'text', text: 'Hello world ' }]);
72
- });
73
- it('strips empty TOOL_CALLS markers with whitespace', () => {
74
- const input = 'Text TOOL_CALLS2{ } more text';
75
- const result = convertResponse(Buffer.from(input, 'utf8'));
76
- expect(result).toEqual([{ type: 'text', text: 'Text more text' }]);
77
- });
78
75
  it('strips standalone stop token', () => {
79
76
  const input = 'Hello world</s>';
80
77
  const result = convertResponse(Buffer.from(input, 'utf8'));
81
78
  expect(result).toEqual([{ type: 'text', text: 'Hello world' }]);
82
79
  });
83
- it('handles TOOL_CALLS with number prefix before stop token', () => {
84
- const input = 'Text before TOOL_CALLS1</s>{} text after';
85
- const result = convertResponse(Buffer.from(input, 'utf8'));
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
80
  it('parses OpenAI-style TOOL_CALLS with numeric IDs', () => {
134
81
  const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"file_path":"/home/test","search_pattern":"binance"}}}{}';
135
82
  const result = convertResponse(Buffer.from(input, 'utf8'));
@@ -184,4 +131,16 @@ describe('convertResponse', () => {
184
131
  },
185
132
  ]);
186
133
  });
134
+ it('handles OpenAI-style with string name "answer" - emits as tool-call', () => {
135
+ const input = 'TOOL_CALLS{"type":"function","function":{"name":"answer","parameters":{"answer":"final answer"}}}{}';
136
+ const result = convertResponse(Buffer.from(input, 'utf8'));
137
+ expect(result).toEqual([
138
+ {
139
+ type: 'tool-call',
140
+ toolCallId: 'toolcall_1',
141
+ toolName: 'answer',
142
+ input: { answer: 'final answer' },
143
+ },
144
+ ]);
145
+ });
187
146
  });