@bxb1337/windsurf-fast-context 1.0.9 → 1.1.1
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.test.js +7 -7
- package/dist/cjs/conversion/response-converter.js +25 -216
- package/dist/cjs/conversion/response-converter.test.js +32 -96
- package/dist/cjs/model/devstral-language-model.js +22 -29
- package/dist/cjs/model/devstral-language-model.test.js +94 -47
- package/dist/conversion/prompt-converter.d.ts +2 -2
- package/dist/conversion/response-converter.d.ts +2 -2
- package/dist/esm/conversion/prompt-converter.test.js +7 -7
- package/dist/esm/conversion/response-converter.js +25 -216
- package/dist/esm/conversion/response-converter.test.js +32 -96
- package/dist/esm/model/devstral-language-model.js +22 -29
- package/dist/esm/model/devstral-language-model.test.js +94 -47
- package/dist/model/devstral-language-model.d.ts +8 -5
- package/dist/types/index.d.ts +1 -1
- package/package.json +1 -1
|
@@ -7,38 +7,6 @@ describe('convertResponse', () => {
|
|
|
7
7
|
const result = convertResponse(Buffer.from('plain response text', 'utf8'));
|
|
8
8
|
expect(result).toEqual([{ type: 'text', text: 'plain response text' }]);
|
|
9
9
|
});
|
|
10
|
-
it('tool-call - parses tool markers and preserves surrounding text', () => {
|
|
11
|
-
const input = 'Before [TOOL_CALLS]searchDocs[ARGS]{"query":"prompt converter"} between [TOOL_CALLS]answer[ARGS]{"answer":"final answer"} after';
|
|
12
|
-
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
13
|
-
expect(result).toEqual([
|
|
14
|
-
{ type: 'text', text: 'Before ' },
|
|
15
|
-
{
|
|
16
|
-
type: 'tool-call',
|
|
17
|
-
toolCallId: 'toolcall_1',
|
|
18
|
-
toolName: 'searchDocs',
|
|
19
|
-
input: { query: 'prompt converter' },
|
|
20
|
-
},
|
|
21
|
-
{ type: 'text', text: ' between ' },
|
|
22
|
-
{
|
|
23
|
-
type: 'tool-call',
|
|
24
|
-
toolCallId: 'toolcall_2',
|
|
25
|
-
toolName: 'answer',
|
|
26
|
-
input: { answer: 'final answer' },
|
|
27
|
-
},
|
|
28
|
-
{ type: 'text', text: ' after' },
|
|
29
|
-
]);
|
|
30
|
-
});
|
|
31
|
-
it('malformed - invalid marker json remains text and does not throw', () => {
|
|
32
|
-
const input = 'prefix [TOOL_CALLS]searchDocs[ARGS]{"query": nope} suffix';
|
|
33
|
-
expect(() => convertResponse(Buffer.from(input, 'utf8'))).not.toThrow();
|
|
34
|
-
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
35
|
-
const combinedText = result
|
|
36
|
-
.filter((part) => part.type === 'text')
|
|
37
|
-
.map((part) => part.text)
|
|
38
|
-
.join('');
|
|
39
|
-
expect(result.every((part) => part.type === 'text')).toBe(true);
|
|
40
|
-
expect(combinedText).toBe(input);
|
|
41
|
-
});
|
|
42
10
|
it('protobuf payload - extracts clean utf8 text without binary mojibake prefix', () => {
|
|
43
11
|
const payload = new ProtobufEncoder();
|
|
44
12
|
payload.writeVarint(1, 150);
|
|
@@ -46,20 +14,6 @@ describe('convertResponse', () => {
|
|
|
46
14
|
const result = convertResponse(payload.toBuffer());
|
|
47
15
|
expect(result).toEqual([{ type: 'text', text: '你好,TypeScript' }]);
|
|
48
16
|
});
|
|
49
|
-
it('protobuf payload - still parses tool-call markers from extracted strings', () => {
|
|
50
|
-
const payload = new ProtobufEncoder();
|
|
51
|
-
payload.writeVarint(1, 150);
|
|
52
|
-
payload.writeString(2, '[TOOL_CALLS]answer[ARGS]{"answer":"final answer"}');
|
|
53
|
-
const result = convertResponse(payload.toBuffer());
|
|
54
|
-
expect(result).toEqual([
|
|
55
|
-
{
|
|
56
|
-
type: 'tool-call',
|
|
57
|
-
toolCallId: 'toolcall_1',
|
|
58
|
-
toolName: 'answer',
|
|
59
|
-
input: { answer: 'final answer' },
|
|
60
|
-
},
|
|
61
|
-
]);
|
|
62
|
-
});
|
|
63
17
|
it('protobuf payload - ignores metadata strings and keeps main text field', () => {
|
|
64
18
|
const payload = new ProtobufEncoder();
|
|
65
19
|
payload.writeString(1, 'meta');
|
|
@@ -77,70 +31,52 @@ describe('convertResponse', () => {
|
|
|
77
31
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
78
32
|
expect(result).toEqual([{ type: 'text', text: 'Hello world' }]);
|
|
79
33
|
});
|
|
80
|
-
|
|
81
|
-
|
|
34
|
+
// Strict OpenAI array format tests
|
|
35
|
+
it('parses strict OpenAI array with single tool call', () => {
|
|
36
|
+
const input = '[{"type":"function","function":{"name":"search","parameters":{"q":"test"}}}]';
|
|
82
37
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
83
38
|
expect(result).toEqual([
|
|
84
|
-
{
|
|
85
|
-
type: 'tool-call',
|
|
86
|
-
toolCallId: 'toolcall_1',
|
|
87
|
-
toolName: 'grep',
|
|
88
|
-
input: { file_path: '/home/test', search_pattern: 'binance' },
|
|
89
|
-
},
|
|
39
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'search', input: { q: 'test' } }
|
|
90
40
|
]);
|
|
91
41
|
});
|
|
92
|
-
it('parses
|
|
93
|
-
const input = '
|
|
42
|
+
it('parses strict OpenAI array with multiple tool calls', () => {
|
|
43
|
+
const input = '[{"type":"function","function":{"name":"read","parameters":{"path":"/a"}}},{"type":"function","function":{"name":"grep","parameters":{"pattern":"foo"}}}]';
|
|
94
44
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
95
45
|
expect(result).toEqual([
|
|
96
|
-
{
|
|
97
|
-
|
|
98
|
-
toolCallId: 'toolcall_1',
|
|
99
|
-
toolName: 'read',
|
|
100
|
-
input: { file_path: '/home/test' },
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
type: 'tool-call',
|
|
104
|
-
toolCallId: 'toolcall_2',
|
|
105
|
-
toolName: 'glob',
|
|
106
|
-
input: { pattern: '*.ts' },
|
|
107
|
-
},
|
|
46
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: { path: '/a' } },
|
|
47
|
+
{ type: 'tool-call', toolCallId: 'toolcall_2', toolName: 'grep', input: { pattern: 'foo' } }
|
|
108
48
|
]);
|
|
109
49
|
});
|
|
110
|
-
it('
|
|
111
|
-
const input = '
|
|
50
|
+
it('parses strict OpenAI array with empty parameters', () => {
|
|
51
|
+
const input = '[{"type":"function","function":{"name":"list","parameters":{}}}]';
|
|
112
52
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
113
53
|
expect(result).toEqual([
|
|
114
|
-
{
|
|
115
|
-
type: 'tool-call',
|
|
116
|
-
toolCallId: 'toolcall_1',
|
|
117
|
-
toolName: 'tool_99',
|
|
118
|
-
input: { arg: 'value' },
|
|
119
|
-
},
|
|
54
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'list', input: {} }
|
|
120
55
|
]);
|
|
121
56
|
});
|
|
122
|
-
it('
|
|
123
|
-
const input = 'TOOL_CALLS{"
|
|
57
|
+
it('returns text for old marker format (no backward compat)', () => {
|
|
58
|
+
const input = '[TOOL_CALLS]search[ARGS]{"q":"test"}';
|
|
124
59
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
125
|
-
expect(result).toEqual([
|
|
126
|
-
{
|
|
127
|
-
type: 'tool-call',
|
|
128
|
-
toolCallId: 'toolcall_1',
|
|
129
|
-
toolName: 'custom_tool',
|
|
130
|
-
input: { key: 'value' },
|
|
131
|
-
},
|
|
132
|
-
]);
|
|
60
|
+
expect(result).toEqual([{ type: 'text', text: '[TOOL_CALLS]search[ARGS]{"q":"test"}' }]);
|
|
133
61
|
});
|
|
134
|
-
it('
|
|
135
|
-
const input = 'TOOL_CALLS{"type":"function","function":{"name":
|
|
62
|
+
it('returns text for old TOOL_CALLS format (no backward compat)', () => {
|
|
63
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"q":"test"}}}{}';
|
|
136
64
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
137
|
-
expect(result).toEqual([
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
65
|
+
expect(result).toEqual([{ type: 'text', text: input }]);
|
|
66
|
+
});
|
|
67
|
+
it('returns text for non-array JSON', () => {
|
|
68
|
+
const input = '{"type":"function","function":{"name":"search","parameters":{}}}';
|
|
69
|
+
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
70
|
+
expect(result).toEqual([{ type: 'text', text: input }]);
|
|
71
|
+
});
|
|
72
|
+
it('returns text for empty array', () => {
|
|
73
|
+
const input = '[]';
|
|
74
|
+
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
75
|
+
expect(result).toEqual([{ type: 'text', text: '[]' }]);
|
|
76
|
+
});
|
|
77
|
+
it('returns text for malformed JSON array', () => {
|
|
78
|
+
const input = '[{"type":"function","function":{"name":"search","parameters":';
|
|
79
|
+
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
80
|
+
expect(result).toEqual([{ type: 'text', text: input }]);
|
|
145
81
|
});
|
|
146
82
|
});
|
|
@@ -17,8 +17,15 @@ const WS_APP_VER = process.env.WS_APP_VER ?? '1.48.2';
|
|
|
17
17
|
const WS_LS_VER = process.env.WS_LS_VER ?? '1.9544.35';
|
|
18
18
|
const SENTRY_PUBLIC_KEY = 'b813f73488da69eedec534dba1029111';
|
|
19
19
|
const CONNECT_USER_AGENT = 'connect-go/1.18.1 (go1.25.5)';
|
|
20
|
+
const TOOL_FORMAT_INSTRUCTION = `When you need to call tools, you must format your response as a JSON array of tool calls following the OpenAI function calling format. Each tool call must be an object with "type" set to "function" and a "function" object containing "name" (string matching one of the provided tool names) and "parameters" (object with the tool's arguments). Output ONLY the JSON array, no surrounding text.
|
|
21
|
+
|
|
22
|
+
Example format:
|
|
23
|
+
[{"type": "function", "function": {"name": "tool_name", "parameters": {"arg1": "value1"}}}]
|
|
24
|
+
|
|
25
|
+
For multiple tool calls:
|
|
26
|
+
[{"type": "function", "function": {"name": "tool1", "parameters": {"arg1": "value1"}}}, {"type": "function", "function": {"name": "tool2", "parameters": {"arg2": "value2"}}}]`;
|
|
20
27
|
export class DevstralLanguageModel {
|
|
21
|
-
specificationVersion = '
|
|
28
|
+
specificationVersion = 'v2';
|
|
22
29
|
provider = 'windsurf';
|
|
23
30
|
modelId;
|
|
24
31
|
supportedUrls = {};
|
|
@@ -55,16 +62,13 @@ export class DevstralLanguageModel {
|
|
|
55
62
|
const responseFrame = await this.transport.postUnary(`${this.baseURL}${API_SERVICE_PATH}${DEVSTRAL_STREAM_PATH}`, requestFrame, headers);
|
|
56
63
|
const { payloads: responsePayloads } = connectFrameDecode(responseFrame);
|
|
57
64
|
const payloads = responsePayloads.length > 0 ? responsePayloads : [responseFrame];
|
|
58
|
-
const content = payloads.flatMap((payload) =>
|
|
65
|
+
const content = payloads.flatMap((payload) => toV2Content(convertResponse(payload)));
|
|
59
66
|
const unified = content.some((part) => part.type === 'tool-call')
|
|
60
67
|
? 'tool-calls'
|
|
61
68
|
: 'stop';
|
|
62
69
|
return {
|
|
63
70
|
content,
|
|
64
|
-
finishReason:
|
|
65
|
-
unified,
|
|
66
|
-
raw: undefined,
|
|
67
|
-
},
|
|
71
|
+
finishReason: unified,
|
|
68
72
|
usage: emptyUsage(),
|
|
69
73
|
warnings: [],
|
|
70
74
|
};
|
|
@@ -122,7 +126,7 @@ export class DevstralLanguageModel {
|
|
|
122
126
|
break;
|
|
123
127
|
}
|
|
124
128
|
pending = frameResult.rest;
|
|
125
|
-
const contentParts =
|
|
129
|
+
const contentParts = toV2Content(convertResponse(frameResult.payload));
|
|
126
130
|
for (const part of contentParts) {
|
|
127
131
|
if (isAborted(options.abortSignal)) {
|
|
128
132
|
safeClose(controller);
|
|
@@ -172,10 +176,7 @@ export class DevstralLanguageModel {
|
|
|
172
176
|
const unified = hasToolCalls ? 'tool-calls' : 'stop';
|
|
173
177
|
safeEnqueue(controller, {
|
|
174
178
|
type: 'finish',
|
|
175
|
-
finishReason:
|
|
176
|
-
unified,
|
|
177
|
-
raw: undefined,
|
|
178
|
-
},
|
|
179
|
+
finishReason: unified,
|
|
179
180
|
usage: emptyUsage(),
|
|
180
181
|
});
|
|
181
182
|
safeClose(controller);
|
|
@@ -189,10 +190,7 @@ export class DevstralLanguageModel {
|
|
|
189
190
|
});
|
|
190
191
|
safeEnqueue(controller, {
|
|
191
192
|
type: 'finish',
|
|
192
|
-
finishReason:
|
|
193
|
-
unified: 'error',
|
|
194
|
-
raw: undefined,
|
|
195
|
-
},
|
|
193
|
+
finishReason: 'error',
|
|
196
194
|
usage: emptyUsage(),
|
|
197
195
|
});
|
|
198
196
|
}
|
|
@@ -251,20 +249,12 @@ function isAborted(signal) {
|
|
|
251
249
|
}
|
|
252
250
|
function emptyUsage() {
|
|
253
251
|
return {
|
|
254
|
-
inputTokens:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
cacheRead: undefined,
|
|
258
|
-
cacheWrite: undefined,
|
|
259
|
-
},
|
|
260
|
-
outputTokens: {
|
|
261
|
-
total: undefined,
|
|
262
|
-
text: undefined,
|
|
263
|
-
reasoning: undefined,
|
|
264
|
-
},
|
|
252
|
+
inputTokens: undefined,
|
|
253
|
+
outputTokens: undefined,
|
|
254
|
+
totalTokens: undefined,
|
|
265
255
|
};
|
|
266
256
|
}
|
|
267
|
-
function
|
|
257
|
+
function toV2Content(parts) {
|
|
268
258
|
return parts.map((part) => {
|
|
269
259
|
if (part.type !== 'tool-call') {
|
|
270
260
|
return part;
|
|
@@ -284,10 +274,13 @@ function isFunctionTool(tool) {
|
|
|
284
274
|
function buildGenerateRequest(input) {
|
|
285
275
|
const request = new ProtobufEncoder();
|
|
286
276
|
request.writeMessage(1, buildMetadata(input.apiKey, input.jwt));
|
|
287
|
-
|
|
277
|
+
const functionTools = input.tools?.filter(isFunctionTool) ?? [];
|
|
278
|
+
const messages = functionTools.length > 0
|
|
279
|
+
? [{ role: 5, content: TOOL_FORMAT_INSTRUCTION }, ...input.messages]
|
|
280
|
+
: input.messages;
|
|
281
|
+
for (const message of messages) {
|
|
288
282
|
request.writeMessage(2, buildMessage(message));
|
|
289
283
|
}
|
|
290
|
-
const functionTools = input.tools?.filter(isFunctionTool) ?? [];
|
|
291
284
|
if (functionTools.length > 0) {
|
|
292
285
|
const toolsArray = functionTools.map((tool) => ({
|
|
293
286
|
type: 'function',
|
|
@@ -131,25 +131,17 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
131
131
|
});
|
|
132
132
|
};
|
|
133
133
|
const model = new DevstralLanguageModel({ apiKey: 'test-api-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
|
|
134
|
-
expect(model.specificationVersion).toBe('
|
|
134
|
+
expect(model.specificationVersion).toBe('v2');
|
|
135
135
|
expect(model.supportedUrls).toEqual({});
|
|
136
136
|
const result = await model.doGenerate({
|
|
137
137
|
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Find auth logic.' }] }],
|
|
138
138
|
});
|
|
139
139
|
expect(result.content).toEqual([{ type: 'text', text: 'generated answer' }]);
|
|
140
|
-
expect(result.finishReason).
|
|
140
|
+
expect(result.finishReason).toBe('stop');
|
|
141
141
|
expect(result.usage).toEqual({
|
|
142
|
-
inputTokens:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
cacheRead: undefined,
|
|
146
|
-
cacheWrite: undefined,
|
|
147
|
-
},
|
|
148
|
-
outputTokens: {
|
|
149
|
-
total: undefined,
|
|
150
|
-
text: undefined,
|
|
151
|
-
reasoning: undefined,
|
|
152
|
-
},
|
|
142
|
+
inputTokens: undefined,
|
|
143
|
+
outputTokens: undefined,
|
|
144
|
+
totalTokens: undefined,
|
|
153
145
|
});
|
|
154
146
|
expect(calls).toHaveLength(2);
|
|
155
147
|
expect(calls[0]?.url).toBe('https://windsurf.test/exa.auth_pb.AuthService/GetUserJwt');
|
|
@@ -169,7 +161,7 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
169
161
|
return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
|
|
170
162
|
}
|
|
171
163
|
requestBodies.push(bufferFromBody(init?.body));
|
|
172
|
-
const payload = Buffer.from('[
|
|
164
|
+
const payload = Buffer.from('[{"type":"function","function":{"name":"searchRepo","parameters":{"query":"jwt manager"}}}]', 'utf8');
|
|
173
165
|
return new Response(Uint8Array.from(connectFrameEncode(payload)), { status: 200 });
|
|
174
166
|
};
|
|
175
167
|
const model = new DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
|
|
@@ -198,7 +190,7 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
198
190
|
input: '{"query":"jwt manager"}',
|
|
199
191
|
},
|
|
200
192
|
]);
|
|
201
|
-
expect(result.finishReason).
|
|
193
|
+
expect(result.finishReason).toBe('tool-calls');
|
|
202
194
|
const strings = extractStrings(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
203
195
|
const toolsPayload = extractToolsPayload(strings);
|
|
204
196
|
expect(toolsPayload).toBeDefined();
|
|
@@ -248,7 +240,7 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
248
240
|
},
|
|
249
241
|
},
|
|
250
242
|
{
|
|
251
|
-
type: 'provider',
|
|
243
|
+
type: 'provider-defined',
|
|
252
244
|
id: 'windsurf.restricted_exec',
|
|
253
245
|
name: 'restricted_exec',
|
|
254
246
|
args: { mode: 'read-only' },
|
|
@@ -281,6 +273,83 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
281
273
|
const strings = extractStrings(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
282
274
|
expect(extractToolsPayload(strings)).toBeUndefined();
|
|
283
275
|
});
|
|
276
|
+
it('injects tool format instruction into request when tools are present', async () => {
|
|
277
|
+
const requestBodies = [];
|
|
278
|
+
const jwt = makeJwt(4_200_000_100, 'instruction');
|
|
279
|
+
const fakeFetch = async (input, init) => {
|
|
280
|
+
const url = String(input);
|
|
281
|
+
if (url.endsWith('/GetUserJwt')) {
|
|
282
|
+
return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
|
|
283
|
+
}
|
|
284
|
+
requestBodies.push(bufferFromBody(init?.body));
|
|
285
|
+
return new Response(Uint8Array.from(connectFrameEncode(Buffer.from('ok', 'utf8'))), { status: 200 });
|
|
286
|
+
};
|
|
287
|
+
const model = new DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
|
|
288
|
+
await model.doGenerate({
|
|
289
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Search for auth.' }] }],
|
|
290
|
+
tools: [
|
|
291
|
+
{
|
|
292
|
+
type: 'function',
|
|
293
|
+
name: 'searchRepo',
|
|
294
|
+
description: 'Search repository files',
|
|
295
|
+
inputSchema: {
|
|
296
|
+
type: 'object',
|
|
297
|
+
properties: { query: { type: 'string' } },
|
|
298
|
+
required: ['query'],
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
});
|
|
303
|
+
const strings = extractStrings(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
304
|
+
const combined = strings.join('\n');
|
|
305
|
+
expect(combined).toContain('When you need to call tools');
|
|
306
|
+
});
|
|
307
|
+
it('does not inject tool format instruction when no tools provided', async () => {
|
|
308
|
+
const requestBodies = [];
|
|
309
|
+
const jwt = makeJwt(4_200_000_101, 'no-instruction');
|
|
310
|
+
const fakeFetch = async (input, init) => {
|
|
311
|
+
const url = String(input);
|
|
312
|
+
if (url.endsWith('/GetUserJwt')) {
|
|
313
|
+
return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
|
|
314
|
+
}
|
|
315
|
+
requestBodies.push(bufferFromBody(init?.body));
|
|
316
|
+
return new Response(Uint8Array.from(connectFrameEncode(Buffer.from('ok', 'utf8'))), { status: 200 });
|
|
317
|
+
};
|
|
318
|
+
const model = new DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
|
|
319
|
+
await model.doGenerate({
|
|
320
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Find auth logic.' }] }],
|
|
321
|
+
});
|
|
322
|
+
const strings = extractStrings(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
323
|
+
const combined = strings.join('\n');
|
|
324
|
+
expect(combined).not.toContain('When you need to call tools');
|
|
325
|
+
});
|
|
326
|
+
it('does not inject tool format instruction when only provider-defined tools', async () => {
|
|
327
|
+
const requestBodies = [];
|
|
328
|
+
const jwt = makeJwt(4_200_000_102, 'provider-only');
|
|
329
|
+
const fakeFetch = async (input, init) => {
|
|
330
|
+
const url = String(input);
|
|
331
|
+
if (url.endsWith('/GetUserJwt')) {
|
|
332
|
+
return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
|
|
333
|
+
}
|
|
334
|
+
requestBodies.push(bufferFromBody(init?.body));
|
|
335
|
+
return new Response(Uint8Array.from(connectFrameEncode(Buffer.from('ok', 'utf8'))), { status: 200 });
|
|
336
|
+
};
|
|
337
|
+
const model = new DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
|
|
338
|
+
await model.doGenerate({
|
|
339
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Use provider tools.' }] }],
|
|
340
|
+
tools: [
|
|
341
|
+
{
|
|
342
|
+
type: 'provider-defined',
|
|
343
|
+
id: 'some-provider-tool',
|
|
344
|
+
name: 'providerTool',
|
|
345
|
+
args: {},
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
});
|
|
349
|
+
const strings = extractStrings(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
350
|
+
const combined = strings.join('\n');
|
|
351
|
+
expect(combined).not.toContain('When you need to call tools');
|
|
352
|
+
});
|
|
284
353
|
});
|
|
285
354
|
describe('DevstralLanguageModel doStream', () => {
|
|
286
355
|
it('stream-text resolves before full body arrives and emits parts incrementally', async () => {
|
|
@@ -368,22 +437,11 @@ describe('DevstralLanguageModel doStream', () => {
|
|
|
368
437
|
expect(parts[4]).toMatchObject({ type: 'text-delta', delta: 'world' });
|
|
369
438
|
expect(parts[6]).toEqual({
|
|
370
439
|
type: 'finish',
|
|
371
|
-
finishReason:
|
|
372
|
-
unified: 'stop',
|
|
373
|
-
raw: undefined,
|
|
374
|
-
},
|
|
440
|
+
finishReason: 'stop',
|
|
375
441
|
usage: {
|
|
376
|
-
inputTokens:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
cacheRead: undefined,
|
|
380
|
-
cacheWrite: undefined,
|
|
381
|
-
},
|
|
382
|
-
outputTokens: {
|
|
383
|
-
total: undefined,
|
|
384
|
-
text: undefined,
|
|
385
|
-
reasoning: undefined,
|
|
386
|
-
},
|
|
442
|
+
inputTokens: undefined,
|
|
443
|
+
outputTokens: undefined,
|
|
444
|
+
totalTokens: undefined,
|
|
387
445
|
},
|
|
388
446
|
});
|
|
389
447
|
}
|
|
@@ -396,7 +454,7 @@ describe('DevstralLanguageModel doStream', () => {
|
|
|
396
454
|
});
|
|
397
455
|
it('stream-tool emits tool-input deltas before final tool-call and finish', async () => {
|
|
398
456
|
const jwt = makeJwt(4_300_000_001, 'stream-tool');
|
|
399
|
-
const toolPayload = Buffer.from('[
|
|
457
|
+
const toolPayload = Buffer.from('[{"type":"function","function":{"name":"searchRepo","parameters":{"query":"jwt manager"}}}]', 'utf8');
|
|
400
458
|
const frames = Buffer.concat([connectFrameEncode(toolPayload)]);
|
|
401
459
|
const fakeFetch = async (input) => {
|
|
402
460
|
const url = String(input);
|
|
@@ -446,22 +504,11 @@ describe('DevstralLanguageModel doStream', () => {
|
|
|
446
504
|
});
|
|
447
505
|
expect(parts[6]).toEqual({
|
|
448
506
|
type: 'finish',
|
|
449
|
-
finishReason:
|
|
450
|
-
unified: 'tool-calls',
|
|
451
|
-
raw: undefined,
|
|
452
|
-
},
|
|
507
|
+
finishReason: 'tool-calls',
|
|
453
508
|
usage: {
|
|
454
|
-
inputTokens:
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
cacheRead: undefined,
|
|
458
|
-
cacheWrite: undefined,
|
|
459
|
-
},
|
|
460
|
-
outputTokens: {
|
|
461
|
-
total: undefined,
|
|
462
|
-
text: undefined,
|
|
463
|
-
reasoning: undefined,
|
|
464
|
-
},
|
|
509
|
+
inputTokens: undefined,
|
|
510
|
+
outputTokens: undefined,
|
|
511
|
+
totalTokens: undefined,
|
|
465
512
|
},
|
|
466
513
|
});
|
|
467
514
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { LanguageModelV2, LanguageModelV2CallOptions } from '@ai-sdk/provider';
|
|
2
2
|
import { JwtManager } from '../auth/jwt-manager.js';
|
|
3
3
|
import { DevstralTransport } from '../transport/http.js';
|
|
4
4
|
import type { WindsurfProviderOptions } from '../types/index.js';
|
|
@@ -7,8 +7,10 @@ export interface DevstralLanguageModelOptions extends WindsurfProviderOptions {
|
|
|
7
7
|
transport?: DevstralTransport;
|
|
8
8
|
jwtManager?: JwtManager;
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
type LanguageModelV2GenerateResult = Awaited<ReturnType<LanguageModelV2['doGenerate']>>;
|
|
11
|
+
type LanguageModelV2StreamResult = Awaited<ReturnType<LanguageModelV2['doStream']>>;
|
|
12
|
+
export declare class DevstralLanguageModel implements LanguageModelV2 {
|
|
13
|
+
readonly specificationVersion = "v2";
|
|
12
14
|
readonly provider = "windsurf";
|
|
13
15
|
readonly modelId: string;
|
|
14
16
|
readonly supportedUrls: Record<string, RegExp[]>;
|
|
@@ -18,6 +20,7 @@ export declare class DevstralLanguageModel implements LanguageModelV3 {
|
|
|
18
20
|
private readonly transport;
|
|
19
21
|
private readonly jwtManager;
|
|
20
22
|
constructor(options?: DevstralLanguageModelOptions);
|
|
21
|
-
doGenerate(options:
|
|
22
|
-
doStream(options:
|
|
23
|
+
doGenerate(options: LanguageModelV2CallOptions): Promise<LanguageModelV2GenerateResult>;
|
|
24
|
+
doStream(options: LanguageModelV2CallOptions): Promise<LanguageModelV2StreamResult>;
|
|
23
25
|
}
|
|
26
|
+
export {};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface WindsurfProviderOptions {
|
|
|
6
6
|
generateId?: () => string;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
* Windsurf provider interface extending AI SDK
|
|
9
|
+
* Windsurf provider interface extending AI SDK V2 ProviderV2 pattern.
|
|
10
10
|
* The provider is callable as a function and has a languageModel method.
|
|
11
11
|
*/
|
|
12
12
|
export interface WindsurfProvider {
|