@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.
- package/dist/cjs/conversion/prompt-converter.js +22 -7
- package/dist/cjs/conversion/prompt-converter.test.js +117 -2
- package/dist/cjs/conversion/response-converter.js +7 -30
- package/dist/cjs/conversion/response-converter.test.js +26 -67
- package/dist/cjs/model/devstral-language-model.js +35 -18
- package/dist/cjs/model/devstral-language-model.test.js +122 -17
- package/dist/conversion/prompt-converter.d.ts +1 -47
- package/dist/esm/conversion/prompt-converter.js +22 -7
- package/dist/esm/conversion/prompt-converter.test.js +117 -2
- package/dist/esm/conversion/response-converter.js +7 -30
- package/dist/esm/conversion/response-converter.test.js +26 -67
- package/dist/esm/model/devstral-language-model.js +35 -18
- package/dist/esm/model/devstral-language-model.test.js +122 -17
- package/dist/model/devstral-language-model.d.ts +2 -91
- package/package.json +1 -1
|
@@ -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).
|
|
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)('
|
|
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
|
-
|
|
180
|
+
tools: [
|
|
181
|
+
{
|
|
182
|
+
type: 'function',
|
|
183
|
+
name: 'searchRepo',
|
|
179
184
|
description: 'Search repository files',
|
|
180
|
-
|
|
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
|
|
200
|
-
(0, vitest_1.expect)(
|
|
201
|
-
|
|
202
|
-
(0, vitest_1.expect)(
|
|
203
|
-
|
|
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:
|
|
293
|
-
|
|
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
|
-
|
|
417
|
+
tools: [
|
|
418
|
+
{
|
|
419
|
+
type: 'function',
|
|
420
|
+
name: 'searchRepo',
|
|
336
421
|
description: 'Search repository files',
|
|
337
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
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:
|
|
61
|
+
content: toolOutputToString(part.output),
|
|
47
62
|
metadata: {
|
|
48
|
-
refCallId: part.toolCallId,
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
{
|
|
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([
|
|
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
|
});
|