@bxb1337/windsurf-fast-context 1.0.7 → 1.0.9
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 +43 -22
- package/dist/cjs/model/devstral-language-model.test.js +194 -19
- package/dist/cjs/protocol/connect-frame.js +10 -4
- package/dist/cjs/protocol/connect-frame.test.js +78 -5
- 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 +43 -22
- package/dist/esm/model/devstral-language-model.test.js +194 -19
- package/dist/esm/protocol/connect-frame.js +8 -3
- package/dist/esm/protocol/connect-frame.test.js +79 -6
- package/dist/model/devstral-language-model.d.ts +2 -91
- package/dist/protocol/connect-frame.d.ts +6 -1
- package/package.json +1 -1
|
@@ -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
|
});
|
|
@@ -53,12 +53,18 @@ export class DevstralLanguageModel {
|
|
|
53
53
|
const requestFrame = connectFrameEncode(requestPayload);
|
|
54
54
|
const headers = createConnectHeaders(this.headers);
|
|
55
55
|
const responseFrame = await this.transport.postUnary(`${this.baseURL}${API_SERVICE_PATH}${DEVSTRAL_STREAM_PATH}`, requestFrame, headers);
|
|
56
|
-
const responsePayloads = connectFrameDecode(responseFrame);
|
|
56
|
+
const { payloads: responsePayloads } = connectFrameDecode(responseFrame);
|
|
57
57
|
const payloads = responsePayloads.length > 0 ? responsePayloads : [responseFrame];
|
|
58
58
|
const content = payloads.flatMap((payload) => toV3Content(convertResponse(payload)));
|
|
59
|
+
const unified = content.some((part) => part.type === 'tool-call')
|
|
60
|
+
? 'tool-calls'
|
|
61
|
+
: 'stop';
|
|
59
62
|
return {
|
|
60
63
|
content,
|
|
61
|
-
finishReason:
|
|
64
|
+
finishReason: {
|
|
65
|
+
unified,
|
|
66
|
+
raw: undefined,
|
|
67
|
+
},
|
|
62
68
|
usage: emptyUsage(),
|
|
63
69
|
warnings: [],
|
|
64
70
|
};
|
|
@@ -86,6 +92,7 @@ export class DevstralLanguageModel {
|
|
|
86
92
|
options.abortSignal?.addEventListener('abort', abortHandler, { once: true });
|
|
87
93
|
let textSegmentId = null;
|
|
88
94
|
let textSegmentCounter = 0;
|
|
95
|
+
let hasToolCalls = false;
|
|
89
96
|
let pending = Buffer.alloc(0);
|
|
90
97
|
const closeTextSegment = () => {
|
|
91
98
|
if (textSegmentId == null) {
|
|
@@ -100,7 +107,7 @@ export class DevstralLanguageModel {
|
|
|
100
107
|
try {
|
|
101
108
|
safeEnqueue(controller, { type: 'stream-start', warnings: [] });
|
|
102
109
|
safeEnqueue(controller, { type: 'response-metadata', modelId: this.modelId });
|
|
103
|
-
while (!isAborted(options.abortSignal)) {
|
|
110
|
+
outerLoop: while (!isAborted(options.abortSignal)) {
|
|
104
111
|
const next = await reader.read();
|
|
105
112
|
if (next.done) {
|
|
106
113
|
break;
|
|
@@ -135,6 +142,7 @@ export class DevstralLanguageModel {
|
|
|
135
142
|
continue;
|
|
136
143
|
}
|
|
137
144
|
closeTextSegment();
|
|
145
|
+
hasToolCalls = true;
|
|
138
146
|
safeEnqueue(controller, {
|
|
139
147
|
type: 'tool-input-start',
|
|
140
148
|
id: part.toolCallId,
|
|
@@ -151,6 +159,9 @@ export class DevstralLanguageModel {
|
|
|
151
159
|
});
|
|
152
160
|
safeEnqueue(controller, part);
|
|
153
161
|
}
|
|
162
|
+
if (frameResult.isEndStream) {
|
|
163
|
+
break outerLoop;
|
|
164
|
+
}
|
|
154
165
|
}
|
|
155
166
|
}
|
|
156
167
|
if (isAborted(options.abortSignal)) {
|
|
@@ -158,10 +169,13 @@ export class DevstralLanguageModel {
|
|
|
158
169
|
return;
|
|
159
170
|
}
|
|
160
171
|
closeTextSegment();
|
|
172
|
+
const unified = hasToolCalls ? 'tool-calls' : 'stop';
|
|
161
173
|
safeEnqueue(controller, {
|
|
162
174
|
type: 'finish',
|
|
163
|
-
finishReason:
|
|
164
|
-
|
|
175
|
+
finishReason: {
|
|
176
|
+
unified,
|
|
177
|
+
raw: undefined,
|
|
178
|
+
},
|
|
165
179
|
usage: emptyUsage(),
|
|
166
180
|
});
|
|
167
181
|
safeClose(controller);
|
|
@@ -175,8 +189,10 @@ export class DevstralLanguageModel {
|
|
|
175
189
|
});
|
|
176
190
|
safeEnqueue(controller, {
|
|
177
191
|
type: 'finish',
|
|
178
|
-
finishReason:
|
|
179
|
-
|
|
192
|
+
finishReason: {
|
|
193
|
+
unified: 'error',
|
|
194
|
+
raw: undefined,
|
|
195
|
+
},
|
|
180
196
|
usage: emptyUsage(),
|
|
181
197
|
});
|
|
182
198
|
}
|
|
@@ -202,10 +218,11 @@ function readNextConnectFrame(buffer) {
|
|
|
202
218
|
return null;
|
|
203
219
|
}
|
|
204
220
|
const frame = buffer.subarray(0, frameLength);
|
|
205
|
-
const
|
|
221
|
+
const { payloads, isEndStream } = connectFrameDecode(frame);
|
|
206
222
|
return {
|
|
207
|
-
payload:
|
|
223
|
+
payload: payloads[0] ?? Buffer.alloc(0),
|
|
208
224
|
rest: buffer.subarray(frameLength),
|
|
225
|
+
isEndStream,
|
|
209
226
|
};
|
|
210
227
|
}
|
|
211
228
|
function safeEnqueue(controller, part) {
|
|
@@ -249,31 +266,35 @@ function emptyUsage() {
|
|
|
249
266
|
}
|
|
250
267
|
function toV3Content(parts) {
|
|
251
268
|
return parts.map((part) => {
|
|
252
|
-
if (part.type
|
|
253
|
-
return
|
|
254
|
-
type: 'tool-call',
|
|
255
|
-
toolCallId: part.toolCallId,
|
|
256
|
-
toolName: part.toolName,
|
|
257
|
-
input: JSON.stringify(part.input),
|
|
258
|
-
};
|
|
269
|
+
if (part.type !== 'tool-call') {
|
|
270
|
+
return part;
|
|
259
271
|
}
|
|
260
|
-
|
|
272
|
+
const input = typeof part.input === 'string' ? part.input : JSON.stringify(part.input);
|
|
273
|
+
return {
|
|
274
|
+
type: 'tool-call',
|
|
275
|
+
toolCallId: part.toolCallId,
|
|
276
|
+
toolName: part.toolName,
|
|
277
|
+
input,
|
|
278
|
+
};
|
|
261
279
|
});
|
|
262
280
|
}
|
|
281
|
+
function isFunctionTool(tool) {
|
|
282
|
+
return tool.type === 'function';
|
|
283
|
+
}
|
|
263
284
|
function buildGenerateRequest(input) {
|
|
264
285
|
const request = new ProtobufEncoder();
|
|
265
286
|
request.writeMessage(1, buildMetadata(input.apiKey, input.jwt));
|
|
266
287
|
for (const message of input.messages) {
|
|
267
288
|
request.writeMessage(2, buildMessage(message));
|
|
268
289
|
}
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
-
const toolsArray =
|
|
290
|
+
const functionTools = input.tools?.filter(isFunctionTool) ?? [];
|
|
291
|
+
if (functionTools.length > 0) {
|
|
292
|
+
const toolsArray = functionTools.map((tool) => ({
|
|
272
293
|
type: 'function',
|
|
273
294
|
function: {
|
|
274
|
-
name,
|
|
295
|
+
name: tool.name,
|
|
275
296
|
description: tool.description ?? '',
|
|
276
|
-
parameters: tool.
|
|
297
|
+
parameters: tool.inputSchema,
|
|
277
298
|
},
|
|
278
299
|
}));
|
|
279
300
|
request.writeString(3, JSON.stringify(toolsArray));
|
|
@@ -64,8 +64,11 @@ function waitFor(ms) {
|
|
|
64
64
|
}
|
|
65
65
|
function decodeRequestPayload(body) {
|
|
66
66
|
// Body is now a connect frame directly (gzip is inside the frame, not outside)
|
|
67
|
-
const
|
|
68
|
-
return
|
|
67
|
+
const { payloads } = connectFrameDecode(body);
|
|
68
|
+
return payloads[0] ?? Buffer.alloc(0);
|
|
69
|
+
}
|
|
70
|
+
function extractToolsPayload(strings) {
|
|
71
|
+
return strings.find((value) => value.startsWith('[{"type":"function"'));
|
|
69
72
|
}
|
|
70
73
|
async function collectStreamParts(stream) {
|
|
71
74
|
const reader = stream.getReader();
|
|
@@ -134,7 +137,7 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
134
137
|
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Find auth logic.' }] }],
|
|
135
138
|
});
|
|
136
139
|
expect(result.content).toEqual([{ type: 'text', text: 'generated answer' }]);
|
|
137
|
-
expect(result.finishReason).
|
|
140
|
+
expect(result.finishReason).toEqual({ unified: 'stop', raw: undefined });
|
|
138
141
|
expect(result.usage).toEqual({
|
|
139
142
|
inputTokens: {
|
|
140
143
|
total: undefined,
|
|
@@ -157,7 +160,7 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
157
160
|
expect(combined).toContain(jwt);
|
|
158
161
|
expect(combined).toContain('Find auth logic.');
|
|
159
162
|
});
|
|
160
|
-
it('
|
|
163
|
+
it('doGenerate accepts function tools array with inputSchema', async () => {
|
|
161
164
|
const requestBodies = [];
|
|
162
165
|
const jwt = makeJwt(4_200_000_000, 'tools');
|
|
163
166
|
const fakeFetch = async (input, init) => {
|
|
@@ -172,10 +175,12 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
172
175
|
const model = new DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
|
|
173
176
|
const result = await model.doGenerate({
|
|
174
177
|
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Inspect jwt manager.' }] }],
|
|
175
|
-
tools:
|
|
176
|
-
|
|
178
|
+
tools: [
|
|
179
|
+
{
|
|
180
|
+
type: 'function',
|
|
181
|
+
name: 'searchRepo',
|
|
177
182
|
description: 'Search repository files',
|
|
178
|
-
|
|
183
|
+
inputSchema: {
|
|
179
184
|
type: 'object',
|
|
180
185
|
properties: {
|
|
181
186
|
query: { type: 'string' },
|
|
@@ -183,7 +188,7 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
183
188
|
required: ['query'],
|
|
184
189
|
},
|
|
185
190
|
},
|
|
186
|
-
|
|
191
|
+
],
|
|
187
192
|
});
|
|
188
193
|
expect(result.content).toEqual([
|
|
189
194
|
{
|
|
@@ -193,12 +198,88 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
193
198
|
input: '{"query":"jwt manager"}',
|
|
194
199
|
},
|
|
195
200
|
]);
|
|
201
|
+
expect(result.finishReason).toEqual({ unified: 'tool-calls', raw: undefined });
|
|
196
202
|
const strings = extractStrings(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
197
|
-
const
|
|
198
|
-
expect(
|
|
199
|
-
|
|
200
|
-
expect(
|
|
201
|
-
|
|
203
|
+
const toolsPayload = extractToolsPayload(strings);
|
|
204
|
+
expect(toolsPayload).toBeDefined();
|
|
205
|
+
const parsedTools = JSON.parse(toolsPayload ?? '[]');
|
|
206
|
+
expect(parsedTools).toEqual([
|
|
207
|
+
{
|
|
208
|
+
type: 'function',
|
|
209
|
+
function: {
|
|
210
|
+
name: 'searchRepo',
|
|
211
|
+
description: 'Search repository files',
|
|
212
|
+
parameters: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
properties: {
|
|
215
|
+
query: { type: 'string' },
|
|
216
|
+
},
|
|
217
|
+
required: ['query'],
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
]);
|
|
222
|
+
});
|
|
223
|
+
it('filters provider tools from tools array serialization', async () => {
|
|
224
|
+
const requestBodies = [];
|
|
225
|
+
const jwt = makeJwt(4_200_000_001, 'provider-tools');
|
|
226
|
+
const fakeFetch = async (input, init) => {
|
|
227
|
+
const url = String(input);
|
|
228
|
+
if (url.endsWith('/GetUserJwt')) {
|
|
229
|
+
return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
|
|
230
|
+
}
|
|
231
|
+
requestBodies.push(bufferFromBody(init?.body));
|
|
232
|
+
return new Response(Uint8Array.from(connectFrameEncode(Buffer.from('ok', 'utf8'))), { status: 200 });
|
|
233
|
+
};
|
|
234
|
+
const model = new DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
|
|
235
|
+
await model.doGenerate({
|
|
236
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Filter provider tools.' }] }],
|
|
237
|
+
tools: [
|
|
238
|
+
{
|
|
239
|
+
type: 'function',
|
|
240
|
+
name: 'searchRepo',
|
|
241
|
+
description: 'Search repository files',
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: 'object',
|
|
244
|
+
properties: {
|
|
245
|
+
query: { type: 'string' },
|
|
246
|
+
},
|
|
247
|
+
required: ['query'],
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
type: 'provider',
|
|
252
|
+
id: 'windsurf.restricted_exec',
|
|
253
|
+
name: 'restricted_exec',
|
|
254
|
+
args: { mode: 'read-only' },
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
});
|
|
258
|
+
const strings = extractStrings(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
259
|
+
const toolsPayload = extractToolsPayload(strings);
|
|
260
|
+
expect(toolsPayload).toBeDefined();
|
|
261
|
+
const parsedTools = JSON.parse(toolsPayload ?? '[]');
|
|
262
|
+
expect(parsedTools).toHaveLength(1);
|
|
263
|
+
expect(parsedTools[0]).toMatchObject({ function: { name: 'searchRepo' } });
|
|
264
|
+
});
|
|
265
|
+
it('handles empty tools array without serializing tool payload', async () => {
|
|
266
|
+
const requestBodies = [];
|
|
267
|
+
const jwt = makeJwt(4_200_000_002, 'empty-tools');
|
|
268
|
+
const fakeFetch = async (input, init) => {
|
|
269
|
+
const url = String(input);
|
|
270
|
+
if (url.endsWith('/GetUserJwt')) {
|
|
271
|
+
return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
|
|
272
|
+
}
|
|
273
|
+
requestBodies.push(bufferFromBody(init?.body));
|
|
274
|
+
return new Response(Uint8Array.from(connectFrameEncode(Buffer.from('ok', 'utf8'))), { status: 200 });
|
|
275
|
+
};
|
|
276
|
+
const model = new DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
|
|
277
|
+
await model.doGenerate({
|
|
278
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'No tools this turn.' }] }],
|
|
279
|
+
tools: [],
|
|
280
|
+
});
|
|
281
|
+
const strings = extractStrings(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
282
|
+
expect(extractToolsPayload(strings)).toBeUndefined();
|
|
202
283
|
});
|
|
203
284
|
});
|
|
204
285
|
describe('DevstralLanguageModel doStream', () => {
|
|
@@ -287,8 +368,10 @@ describe('DevstralLanguageModel doStream', () => {
|
|
|
287
368
|
expect(parts[4]).toMatchObject({ type: 'text-delta', delta: 'world' });
|
|
288
369
|
expect(parts[6]).toEqual({
|
|
289
370
|
type: 'finish',
|
|
290
|
-
finishReason:
|
|
291
|
-
|
|
371
|
+
finishReason: {
|
|
372
|
+
unified: 'stop',
|
|
373
|
+
raw: undefined,
|
|
374
|
+
},
|
|
292
375
|
usage: {
|
|
293
376
|
inputTokens: {
|
|
294
377
|
total: undefined,
|
|
@@ -329,10 +412,12 @@ describe('DevstralLanguageModel doStream', () => {
|
|
|
329
412
|
});
|
|
330
413
|
const result = await model.doStream({
|
|
331
414
|
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Call tools.' }] }],
|
|
332
|
-
tools:
|
|
333
|
-
|
|
415
|
+
tools: [
|
|
416
|
+
{
|
|
417
|
+
type: 'function',
|
|
418
|
+
name: 'searchRepo',
|
|
334
419
|
description: 'Search repository files',
|
|
335
|
-
|
|
420
|
+
inputSchema: {
|
|
336
421
|
type: 'object',
|
|
337
422
|
properties: {
|
|
338
423
|
query: { type: 'string' },
|
|
@@ -340,7 +425,7 @@ describe('DevstralLanguageModel doStream', () => {
|
|
|
340
425
|
required: ['query'],
|
|
341
426
|
},
|
|
342
427
|
},
|
|
343
|
-
|
|
428
|
+
],
|
|
344
429
|
});
|
|
345
430
|
const parts = await collectStreamParts(result.stream);
|
|
346
431
|
expect(parts.map((part) => part.type)).toEqual([
|
|
@@ -359,6 +444,96 @@ describe('DevstralLanguageModel doStream', () => {
|
|
|
359
444
|
toolName: 'searchRepo',
|
|
360
445
|
input: '{"query":"jwt manager"}',
|
|
361
446
|
});
|
|
447
|
+
expect(parts[6]).toEqual({
|
|
448
|
+
type: 'finish',
|
|
449
|
+
finishReason: {
|
|
450
|
+
unified: 'tool-calls',
|
|
451
|
+
raw: undefined,
|
|
452
|
+
},
|
|
453
|
+
usage: {
|
|
454
|
+
inputTokens: {
|
|
455
|
+
total: undefined,
|
|
456
|
+
noCache: undefined,
|
|
457
|
+
cacheRead: undefined,
|
|
458
|
+
cacheWrite: undefined,
|
|
459
|
+
},
|
|
460
|
+
outputTokens: {
|
|
461
|
+
total: undefined,
|
|
462
|
+
text: undefined,
|
|
463
|
+
reasoning: undefined,
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
it('stream terminates immediately on EndStreamResponse frame (flags=2)', async () => {
|
|
469
|
+
const jwt = makeJwt(4_300_000_003, 'end-stream-response');
|
|
470
|
+
const textFrame = connectFrameEncode(Buffer.from('hello world', 'utf8'));
|
|
471
|
+
const endStreamPayload = Buffer.alloc(0);
|
|
472
|
+
const endStreamFrame = Buffer.allocUnsafe(5 + endStreamPayload.length);
|
|
473
|
+
endStreamFrame.writeUInt8(2, 0);
|
|
474
|
+
endStreamFrame.writeUInt32BE(endStreamPayload.length, 1);
|
|
475
|
+
endStreamPayload.copy(endStreamFrame, 5);
|
|
476
|
+
let releaseHttpClose;
|
|
477
|
+
const waitForHttpClose = new Promise((resolve) => {
|
|
478
|
+
releaseHttpClose = resolve;
|
|
479
|
+
});
|
|
480
|
+
const fakeFetch = async (input) => {
|
|
481
|
+
const url = String(input);
|
|
482
|
+
if (url.endsWith('/GetUserJwt')) {
|
|
483
|
+
return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
|
|
484
|
+
}
|
|
485
|
+
let sentTextFrame = false;
|
|
486
|
+
let sentEndStreamFrame = false;
|
|
487
|
+
return new Response(new ReadableStream({
|
|
488
|
+
async pull(controller) {
|
|
489
|
+
if (!sentTextFrame) {
|
|
490
|
+
controller.enqueue(Uint8Array.from(textFrame));
|
|
491
|
+
sentTextFrame = true;
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (!sentEndStreamFrame) {
|
|
495
|
+
controller.enqueue(Uint8Array.from(endStreamFrame));
|
|
496
|
+
sentEndStreamFrame = true;
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
await waitForHttpClose;
|
|
500
|
+
controller.close();
|
|
501
|
+
},
|
|
502
|
+
}), { status: 200 });
|
|
503
|
+
};
|
|
504
|
+
const model = new DevstralLanguageModel({
|
|
505
|
+
apiKey: 'end-stream-key',
|
|
506
|
+
fetch: fakeFetch,
|
|
507
|
+
baseURL: 'https://windsurf.test',
|
|
508
|
+
});
|
|
509
|
+
const result = await model.doStream({
|
|
510
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Stream with end.' }] }],
|
|
511
|
+
});
|
|
512
|
+
const reader = result.stream.getReader();
|
|
513
|
+
const parts = [];
|
|
514
|
+
const resultType = await Promise.race([
|
|
515
|
+
(async () => {
|
|
516
|
+
while (true) {
|
|
517
|
+
const next = await reader.read();
|
|
518
|
+
if (next.done) {
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
parts.push(next.value);
|
|
522
|
+
}
|
|
523
|
+
return 'completed';
|
|
524
|
+
})(),
|
|
525
|
+
waitFor(100).then(() => 'timed-out'),
|
|
526
|
+
]);
|
|
527
|
+
expect(resultType).toBe('completed');
|
|
528
|
+
expect(parts.map((p) => p.type)).toEqual([
|
|
529
|
+
'stream-start',
|
|
530
|
+
'response-metadata',
|
|
531
|
+
'text-start',
|
|
532
|
+
'text-delta',
|
|
533
|
+
'text-end',
|
|
534
|
+
'finish',
|
|
535
|
+
]);
|
|
536
|
+
releaseHttpClose?.();
|
|
362
537
|
});
|
|
363
538
|
it('abort stops stream mid-response', async () => {
|
|
364
539
|
const controller = new AbortController();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { gunzipSync, gzipSync } from 'node:zlib';
|
|
2
2
|
const FRAME_HEADER_SIZE = 5;
|
|
3
|
-
const FLAG_COMPRESSED = 1;
|
|
3
|
+
export const FLAG_COMPRESSED = 1;
|
|
4
|
+
export const FLAG_END_STREAM = 2;
|
|
4
5
|
/**
|
|
5
6
|
* Encode a protobuf payload into a Connect-RPC frame.
|
|
6
7
|
*
|
|
@@ -20,6 +21,7 @@ export function connectFrameEncode(payload, compress = true) {
|
|
|
20
21
|
export function connectFrameDecode(buffer) {
|
|
21
22
|
const decoded = [];
|
|
22
23
|
let offset = 0;
|
|
24
|
+
let isEndStream = false;
|
|
23
25
|
while (offset + FRAME_HEADER_SIZE <= buffer.length) {
|
|
24
26
|
const flags = buffer.readUInt8(offset);
|
|
25
27
|
const payloadLength = buffer.readUInt32BE(offset + 1);
|
|
@@ -29,8 +31,11 @@ export function connectFrameDecode(buffer) {
|
|
|
29
31
|
break;
|
|
30
32
|
}
|
|
31
33
|
const payload = buffer.subarray(payloadStart, payloadEnd);
|
|
32
|
-
decoded.push(flags
|
|
34
|
+
decoded.push((flags & FLAG_COMPRESSED) !== 0 ? gunzipSync(payload) : Buffer.from(payload));
|
|
35
|
+
if ((flags & FLAG_END_STREAM) !== 0) {
|
|
36
|
+
isEndStream = true;
|
|
37
|
+
}
|
|
33
38
|
offset = payloadEnd;
|
|
34
39
|
}
|
|
35
|
-
return decoded;
|
|
40
|
+
return { payloads: decoded, isEndStream };
|
|
36
41
|
}
|