@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
|
@@ -36,7 +36,7 @@ const prompt_converter_js_1 = require("./prompt-converter.js");
|
|
|
36
36
|
},
|
|
37
37
|
});
|
|
38
38
|
});
|
|
39
|
-
(0, vitest_1.it)('multi-turn preserves ordering across mixed roles (
|
|
39
|
+
(0, vitest_1.it)('multi-turn preserves ordering across mixed roles (V2 output shape)', () => {
|
|
40
40
|
const prompt = [
|
|
41
41
|
{ role: 'system', content: 'System instruction' },
|
|
42
42
|
{
|
|
@@ -161,7 +161,7 @@ const prompt_converter_js_1 = require("./prompt-converter.js");
|
|
|
161
161
|
},
|
|
162
162
|
]);
|
|
163
163
|
});
|
|
164
|
-
(0, vitest_1.it)('tool-result with
|
|
164
|
+
(0, vitest_1.it)('tool-result with error-text output includes denial reason', () => {
|
|
165
165
|
const prompt = [
|
|
166
166
|
{
|
|
167
167
|
role: 'tool',
|
|
@@ -170,7 +170,7 @@ const prompt_converter_js_1 = require("./prompt-converter.js");
|
|
|
170
170
|
type: 'tool-result',
|
|
171
171
|
toolCallId: 'call_denied',
|
|
172
172
|
toolName: 'dangerousAction',
|
|
173
|
-
output: { type: '
|
|
173
|
+
output: { type: 'error-text', value: 'User rejected tool execution' },
|
|
174
174
|
},
|
|
175
175
|
],
|
|
176
176
|
},
|
|
@@ -179,12 +179,12 @@ const prompt_converter_js_1 = require("./prompt-converter.js");
|
|
|
179
179
|
(0, vitest_1.expect)(result).toEqual([
|
|
180
180
|
{
|
|
181
181
|
role: 4,
|
|
182
|
-
content: '
|
|
182
|
+
content: 'User rejected tool execution',
|
|
183
183
|
metadata: { refCallId: 'call_denied' },
|
|
184
184
|
},
|
|
185
185
|
]);
|
|
186
186
|
});
|
|
187
|
-
(0, vitest_1.it)('tool-result with
|
|
187
|
+
(0, vitest_1.it)('tool-result with error-text output handles fallback denial text', () => {
|
|
188
188
|
const prompt = [
|
|
189
189
|
{
|
|
190
190
|
role: 'tool',
|
|
@@ -193,7 +193,7 @@ const prompt_converter_js_1 = require("./prompt-converter.js");
|
|
|
193
193
|
type: 'tool-result',
|
|
194
194
|
toolCallId: 'call_denied_no_reason',
|
|
195
195
|
toolName: 'someTool',
|
|
196
|
-
output: { type: 'execution
|
|
196
|
+
output: { type: 'error-text', value: 'Tool execution denied' },
|
|
197
197
|
},
|
|
198
198
|
],
|
|
199
199
|
},
|
|
@@ -202,7 +202,7 @@ const prompt_converter_js_1 = require("./prompt-converter.js");
|
|
|
202
202
|
(0, vitest_1.expect)(result).toEqual([
|
|
203
203
|
{
|
|
204
204
|
role: 4,
|
|
205
|
-
content: '
|
|
205
|
+
content: 'Tool execution denied',
|
|
206
206
|
metadata: { refCallId: 'call_denied_no_reason' },
|
|
207
207
|
},
|
|
208
208
|
]);
|
|
@@ -3,192 +3,38 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.convertResponse = convertResponse;
|
|
4
4
|
const node_zlib_1 = require("node:zlib");
|
|
5
5
|
const protobuf_js_1 = require("../protocol/protobuf.js");
|
|
6
|
-
const TOOL_CALL_PREFIX = '[TOOL_CALLS]';
|
|
7
|
-
const ARGS_PREFIX = '[ARGS]';
|
|
8
6
|
const STOP_TOKEN = '</s>';
|
|
9
|
-
function
|
|
10
|
-
|
|
7
|
+
function parseStrictOpenAIToolCalls(responseText) {
|
|
8
|
+
const trimmed = responseText.trim();
|
|
9
|
+
if (!trimmed.startsWith('['))
|
|
11
10
|
return null;
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(trimmed);
|
|
13
|
+
if (!Array.isArray(parsed) || parsed.length === 0)
|
|
14
|
+
return null;
|
|
15
|
+
const toolCalls = [];
|
|
16
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
17
|
+
const item = parsed[i];
|
|
18
|
+
if (item.type !== 'function' || !item.function?.name)
|
|
19
|
+
continue;
|
|
20
|
+
toolCalls.push({
|
|
21
|
+
type: 'tool-call',
|
|
22
|
+
toolCallId: `toolcall_${i + 1}`,
|
|
23
|
+
toolName: String(item.function.name),
|
|
24
|
+
input: item.function.parameters ?? {},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return toolCalls.length > 0 ? toolCalls : null;
|
|
12
28
|
}
|
|
13
|
-
|
|
14
|
-
if (!jsonPart.startsWith('{')) {
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
const toolCalls = [];
|
|
18
|
-
let cursor = 0;
|
|
19
|
-
while (cursor < jsonPart.length) {
|
|
20
|
-
if (jsonPart[cursor] !== '{') {
|
|
21
|
-
cursor++;
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
const endResult = parseBalancedEnd(jsonPart, cursor);
|
|
25
|
-
if (endResult == null) {
|
|
26
|
-
break;
|
|
27
|
-
}
|
|
28
|
-
const jsonStr = jsonPart.slice(cursor, endResult);
|
|
29
|
-
cursor = endResult;
|
|
30
|
-
if (jsonStr === '{}') {
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
try {
|
|
34
|
-
const parsed = JSON.parse(jsonStr);
|
|
35
|
-
if (parsed.type === 'function' && parsed.function) {
|
|
36
|
-
toolCalls.push(parsed);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
if (toolCalls.length === 0) {
|
|
29
|
+
catch {
|
|
44
30
|
return null;
|
|
45
31
|
}
|
|
46
|
-
return toolCalls.map((call, index) => {
|
|
47
|
-
const toolId = call.function.name;
|
|
48
|
-
const toolName = typeof toolId === 'number' ? mapToolIdToName(toolId) : String(toolId);
|
|
49
|
-
const args = call.function.parameters ?? {};
|
|
50
|
-
return {
|
|
51
|
-
type: 'tool-call',
|
|
52
|
-
toolCallId: `toolcall_${index + 1}`,
|
|
53
|
-
toolName,
|
|
54
|
-
input: args,
|
|
55
|
-
};
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
function mapToolIdToName(id) {
|
|
59
|
-
switch (id) {
|
|
60
|
-
case 1:
|
|
61
|
-
return 'read';
|
|
62
|
-
case 2:
|
|
63
|
-
return 'glob';
|
|
64
|
-
case 3:
|
|
65
|
-
return 'grep';
|
|
66
|
-
default:
|
|
67
|
-
return `tool_${id}`;
|
|
68
|
-
}
|
|
69
32
|
}
|
|
70
33
|
function pushText(parts, text) {
|
|
71
34
|
if (text.length > 0) {
|
|
72
35
|
parts.push({ type: 'text', text });
|
|
73
36
|
}
|
|
74
37
|
}
|
|
75
|
-
function parseStringEnd(value, startIndex) {
|
|
76
|
-
let index = startIndex + 1;
|
|
77
|
-
let escaping = false;
|
|
78
|
-
while (index < value.length) {
|
|
79
|
-
const char = value[index];
|
|
80
|
-
if (escaping) {
|
|
81
|
-
escaping = false;
|
|
82
|
-
}
|
|
83
|
-
else if (char === '\\') {
|
|
84
|
-
escaping = true;
|
|
85
|
-
}
|
|
86
|
-
else if (char === '"') {
|
|
87
|
-
return index + 1;
|
|
88
|
-
}
|
|
89
|
-
index += 1;
|
|
90
|
-
}
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
function parseBalancedEnd(value, startIndex) {
|
|
94
|
-
const stack = [value[startIndex] === '{' ? '}' : ']'];
|
|
95
|
-
let index = startIndex + 1;
|
|
96
|
-
let inString = false;
|
|
97
|
-
let escaping = false;
|
|
98
|
-
while (index < value.length) {
|
|
99
|
-
const char = value[index];
|
|
100
|
-
if (inString) {
|
|
101
|
-
if (escaping) {
|
|
102
|
-
escaping = false;
|
|
103
|
-
}
|
|
104
|
-
else if (char === '\\') {
|
|
105
|
-
escaping = true;
|
|
106
|
-
}
|
|
107
|
-
else if (char === '"') {
|
|
108
|
-
inString = false;
|
|
109
|
-
}
|
|
110
|
-
index += 1;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (char === '"') {
|
|
114
|
-
inString = true;
|
|
115
|
-
index += 1;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (char === '{') {
|
|
119
|
-
stack.push('}');
|
|
120
|
-
index += 1;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (char === '[') {
|
|
124
|
-
stack.push(']');
|
|
125
|
-
index += 1;
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (char === '}' || char === ']') {
|
|
129
|
-
const expected = stack[stack.length - 1];
|
|
130
|
-
if (expected !== char) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
stack.pop();
|
|
134
|
-
index += 1;
|
|
135
|
-
if (stack.length === 0) {
|
|
136
|
-
return index;
|
|
137
|
-
}
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
index += 1;
|
|
141
|
-
}
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
function parsePrimitiveEnd(value, startIndex) {
|
|
145
|
-
let index = startIndex;
|
|
146
|
-
while (index < value.length) {
|
|
147
|
-
const char = value[index];
|
|
148
|
-
if (char === ' ' || char === '\n' || char === '\r' || char === '\t') {
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
index += 1;
|
|
152
|
-
}
|
|
153
|
-
return index;
|
|
154
|
-
}
|
|
155
|
-
function parseJsonValue(value, startIndex) {
|
|
156
|
-
let jsonStart = startIndex;
|
|
157
|
-
while (jsonStart < value.length) {
|
|
158
|
-
const char = value[jsonStart];
|
|
159
|
-
if (char !== ' ' && char !== '\n' && char !== '\r' && char !== '\t') {
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
jsonStart += 1;
|
|
163
|
-
}
|
|
164
|
-
if (jsonStart >= value.length) {
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
const firstChar = value[jsonStart];
|
|
168
|
-
let endIndex;
|
|
169
|
-
if (firstChar === '{' || firstChar === '[') {
|
|
170
|
-
endIndex = parseBalancedEnd(value, jsonStart);
|
|
171
|
-
}
|
|
172
|
-
else if (firstChar === '"') {
|
|
173
|
-
endIndex = parseStringEnd(value, jsonStart);
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
endIndex = parsePrimitiveEnd(value, jsonStart);
|
|
177
|
-
}
|
|
178
|
-
if (endIndex == null || endIndex <= jsonStart) {
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
const rawJson = value.slice(jsonStart, endIndex);
|
|
182
|
-
try {
|
|
183
|
-
return {
|
|
184
|
-
parsed: JSON.parse(rawJson),
|
|
185
|
-
endIndex,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
catch {
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
38
|
function hasControlChars(value) {
|
|
193
39
|
for (const char of value) {
|
|
194
40
|
const code = char.charCodeAt(0);
|
|
@@ -216,10 +62,6 @@ function isLikelyMetadata(value) {
|
|
|
216
62
|
return /^[A-Za-z0-9._:-]{1,32}$/.test(value);
|
|
217
63
|
}
|
|
218
64
|
function pickBestExtractedText(values) {
|
|
219
|
-
const markerValues = values.filter((value) => value.includes(TOOL_CALL_PREFIX) || value.includes(ARGS_PREFIX));
|
|
220
|
-
if (markerValues.length > 0) {
|
|
221
|
-
return markerValues.join('');
|
|
222
|
-
}
|
|
223
65
|
const nonMetadata = values.filter((value) => !isLikelyMetadata(value));
|
|
224
66
|
const candidates = nonMetadata.length > 0 ? nonMetadata : values;
|
|
225
67
|
return candidates.reduce((best, current) => (current.length > best.length ? current : best), candidates[0] ?? '');
|
|
@@ -239,43 +81,10 @@ function decodeResponseText(buffer) {
|
|
|
239
81
|
function convertResponse(buffer) {
|
|
240
82
|
let responseText = decodeResponseText(buffer);
|
|
241
83
|
responseText = responseText.replace(STOP_TOKEN, '');
|
|
242
|
-
const
|
|
243
|
-
if (
|
|
244
|
-
return
|
|
245
|
-
}
|
|
84
|
+
const strictToolCalls = parseStrictOpenAIToolCalls(responseText);
|
|
85
|
+
if (strictToolCalls)
|
|
86
|
+
return strictToolCalls;
|
|
246
87
|
const parts = [];
|
|
247
|
-
|
|
248
|
-
let toolCallCount = 0;
|
|
249
|
-
while (cursor < responseText.length) {
|
|
250
|
-
const markerStart = responseText.indexOf(TOOL_CALL_PREFIX, cursor);
|
|
251
|
-
if (markerStart === -1) {
|
|
252
|
-
pushText(parts, responseText.slice(cursor));
|
|
253
|
-
break;
|
|
254
|
-
}
|
|
255
|
-
pushText(parts, responseText.slice(cursor, markerStart));
|
|
256
|
-
const toolNameStart = markerStart + TOOL_CALL_PREFIX.length;
|
|
257
|
-
const argsStart = responseText.indexOf(ARGS_PREFIX, toolNameStart);
|
|
258
|
-
if (argsStart === -1) {
|
|
259
|
-
pushText(parts, responseText.slice(markerStart));
|
|
260
|
-
break;
|
|
261
|
-
}
|
|
262
|
-
const toolName = responseText.slice(toolNameStart, argsStart);
|
|
263
|
-
const parsedArgs = parseJsonValue(responseText, argsStart + ARGS_PREFIX.length);
|
|
264
|
-
if (parsedArgs == null) {
|
|
265
|
-
const nextMarker = responseText.indexOf(TOOL_CALL_PREFIX, markerStart + TOOL_CALL_PREFIX.length);
|
|
266
|
-
const malformedEnd = nextMarker === -1 ? responseText.length : nextMarker;
|
|
267
|
-
pushText(parts, responseText.slice(markerStart, malformedEnd));
|
|
268
|
-
cursor = malformedEnd;
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
toolCallCount += 1;
|
|
272
|
-
parts.push({
|
|
273
|
-
type: 'tool-call',
|
|
274
|
-
toolCallId: `toolcall_${toolCallCount}`,
|
|
275
|
-
toolName,
|
|
276
|
-
input: parsedArgs.parsed,
|
|
277
|
-
});
|
|
278
|
-
cursor = parsedArgs.endIndex;
|
|
279
|
-
}
|
|
88
|
+
pushText(parts, responseText);
|
|
280
89
|
return parts;
|
|
281
90
|
}
|
|
@@ -9,38 +9,6 @@ const response_converter_js_1 = require("./response-converter.js");
|
|
|
9
9
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from('plain response text', 'utf8'));
|
|
10
10
|
(0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'plain response text' }]);
|
|
11
11
|
});
|
|
12
|
-
(0, vitest_1.it)('tool-call - parses tool markers and preserves surrounding text', () => {
|
|
13
|
-
const input = 'Before [TOOL_CALLS]searchDocs[ARGS]{"query":"prompt converter"} between [TOOL_CALLS]answer[ARGS]{"answer":"final answer"} after';
|
|
14
|
-
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
15
|
-
(0, vitest_1.expect)(result).toEqual([
|
|
16
|
-
{ type: 'text', text: 'Before ' },
|
|
17
|
-
{
|
|
18
|
-
type: 'tool-call',
|
|
19
|
-
toolCallId: 'toolcall_1',
|
|
20
|
-
toolName: 'searchDocs',
|
|
21
|
-
input: { query: 'prompt converter' },
|
|
22
|
-
},
|
|
23
|
-
{ type: 'text', text: ' between ' },
|
|
24
|
-
{
|
|
25
|
-
type: 'tool-call',
|
|
26
|
-
toolCallId: 'toolcall_2',
|
|
27
|
-
toolName: 'answer',
|
|
28
|
-
input: { answer: 'final answer' },
|
|
29
|
-
},
|
|
30
|
-
{ type: 'text', text: ' after' },
|
|
31
|
-
]);
|
|
32
|
-
});
|
|
33
|
-
(0, vitest_1.it)('malformed - invalid marker json remains text and does not throw', () => {
|
|
34
|
-
const input = 'prefix [TOOL_CALLS]searchDocs[ARGS]{"query": nope} suffix';
|
|
35
|
-
(0, vitest_1.expect)(() => (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'))).not.toThrow();
|
|
36
|
-
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
37
|
-
const combinedText = result
|
|
38
|
-
.filter((part) => part.type === 'text')
|
|
39
|
-
.map((part) => part.text)
|
|
40
|
-
.join('');
|
|
41
|
-
(0, vitest_1.expect)(result.every((part) => part.type === 'text')).toBe(true);
|
|
42
|
-
(0, vitest_1.expect)(combinedText).toBe(input);
|
|
43
|
-
});
|
|
44
12
|
(0, vitest_1.it)('protobuf payload - extracts clean utf8 text without binary mojibake prefix', () => {
|
|
45
13
|
const payload = new protobuf_js_1.ProtobufEncoder();
|
|
46
14
|
payload.writeVarint(1, 150);
|
|
@@ -48,20 +16,6 @@ const response_converter_js_1 = require("./response-converter.js");
|
|
|
48
16
|
const result = (0, response_converter_js_1.convertResponse)(payload.toBuffer());
|
|
49
17
|
(0, vitest_1.expect)(result).toEqual([{ type: 'text', text: '你好,TypeScript' }]);
|
|
50
18
|
});
|
|
51
|
-
(0, vitest_1.it)('protobuf payload - still parses tool-call markers from extracted strings', () => {
|
|
52
|
-
const payload = new protobuf_js_1.ProtobufEncoder();
|
|
53
|
-
payload.writeVarint(1, 150);
|
|
54
|
-
payload.writeString(2, '[TOOL_CALLS]answer[ARGS]{"answer":"final answer"}');
|
|
55
|
-
const result = (0, response_converter_js_1.convertResponse)(payload.toBuffer());
|
|
56
|
-
(0, vitest_1.expect)(result).toEqual([
|
|
57
|
-
{
|
|
58
|
-
type: 'tool-call',
|
|
59
|
-
toolCallId: 'toolcall_1',
|
|
60
|
-
toolName: 'answer',
|
|
61
|
-
input: { answer: 'final answer' },
|
|
62
|
-
},
|
|
63
|
-
]);
|
|
64
|
-
});
|
|
65
19
|
(0, vitest_1.it)('protobuf payload - ignores metadata strings and keeps main text field', () => {
|
|
66
20
|
const payload = new protobuf_js_1.ProtobufEncoder();
|
|
67
21
|
payload.writeString(1, 'meta');
|
|
@@ -79,70 +33,52 @@ const response_converter_js_1 = require("./response-converter.js");
|
|
|
79
33
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
80
34
|
(0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Hello world' }]);
|
|
81
35
|
});
|
|
82
|
-
|
|
83
|
-
|
|
36
|
+
// Strict OpenAI array format tests
|
|
37
|
+
(0, vitest_1.it)('parses strict OpenAI array with single tool call', () => {
|
|
38
|
+
const input = '[{"type":"function","function":{"name":"search","parameters":{"q":"test"}}}]';
|
|
84
39
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
85
40
|
(0, vitest_1.expect)(result).toEqual([
|
|
86
|
-
{
|
|
87
|
-
type: 'tool-call',
|
|
88
|
-
toolCallId: 'toolcall_1',
|
|
89
|
-
toolName: 'grep',
|
|
90
|
-
input: { file_path: '/home/test', search_pattern: 'binance' },
|
|
91
|
-
},
|
|
41
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'search', input: { q: 'test' } }
|
|
92
42
|
]);
|
|
93
43
|
});
|
|
94
|
-
(0, vitest_1.it)('parses
|
|
95
|
-
const input = '
|
|
44
|
+
(0, vitest_1.it)('parses strict OpenAI array with multiple tool calls', () => {
|
|
45
|
+
const input = '[{"type":"function","function":{"name":"read","parameters":{"path":"/a"}}},{"type":"function","function":{"name":"grep","parameters":{"pattern":"foo"}}}]';
|
|
96
46
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
97
47
|
(0, vitest_1.expect)(result).toEqual([
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
toolCallId: 'toolcall_1',
|
|
101
|
-
toolName: 'read',
|
|
102
|
-
input: { file_path: '/home/test' },
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
type: 'tool-call',
|
|
106
|
-
toolCallId: 'toolcall_2',
|
|
107
|
-
toolName: 'glob',
|
|
108
|
-
input: { pattern: '*.ts' },
|
|
109
|
-
},
|
|
48
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: { path: '/a' } },
|
|
49
|
+
{ type: 'tool-call', toolCallId: 'toolcall_2', toolName: 'grep', input: { pattern: 'foo' } }
|
|
110
50
|
]);
|
|
111
51
|
});
|
|
112
|
-
(0, vitest_1.it)('
|
|
113
|
-
const input = '
|
|
52
|
+
(0, vitest_1.it)('parses strict OpenAI array with empty parameters', () => {
|
|
53
|
+
const input = '[{"type":"function","function":{"name":"list","parameters":{}}}]';
|
|
114
54
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
115
55
|
(0, vitest_1.expect)(result).toEqual([
|
|
116
|
-
{
|
|
117
|
-
type: 'tool-call',
|
|
118
|
-
toolCallId: 'toolcall_1',
|
|
119
|
-
toolName: 'tool_99',
|
|
120
|
-
input: { arg: 'value' },
|
|
121
|
-
},
|
|
56
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'list', input: {} }
|
|
122
57
|
]);
|
|
123
58
|
});
|
|
124
|
-
(0, vitest_1.it)('
|
|
125
|
-
const input = 'TOOL_CALLS{"
|
|
59
|
+
(0, vitest_1.it)('returns text for old marker format (no backward compat)', () => {
|
|
60
|
+
const input = '[TOOL_CALLS]search[ARGS]{"q":"test"}';
|
|
126
61
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
127
|
-
(0, vitest_1.expect)(result).toEqual([
|
|
128
|
-
{
|
|
129
|
-
type: 'tool-call',
|
|
130
|
-
toolCallId: 'toolcall_1',
|
|
131
|
-
toolName: 'custom_tool',
|
|
132
|
-
input: { key: 'value' },
|
|
133
|
-
},
|
|
134
|
-
]);
|
|
62
|
+
(0, vitest_1.expect)(result).toEqual([{ type: 'text', text: '[TOOL_CALLS]search[ARGS]{"q":"test"}' }]);
|
|
135
63
|
});
|
|
136
|
-
(0, vitest_1.it)('
|
|
137
|
-
const input = 'TOOL_CALLS{"type":"function","function":{"name":
|
|
64
|
+
(0, vitest_1.it)('returns text for old TOOL_CALLS format (no backward compat)', () => {
|
|
65
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"q":"test"}}}{}';
|
|
138
66
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
139
|
-
(0, vitest_1.expect)(result).toEqual([
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
67
|
+
(0, vitest_1.expect)(result).toEqual([{ type: 'text', text: input }]);
|
|
68
|
+
});
|
|
69
|
+
(0, vitest_1.it)('returns text for non-array JSON', () => {
|
|
70
|
+
const input = '{"type":"function","function":{"name":"search","parameters":{}}}';
|
|
71
|
+
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
72
|
+
(0, vitest_1.expect)(result).toEqual([{ type: 'text', text: input }]);
|
|
73
|
+
});
|
|
74
|
+
(0, vitest_1.it)('returns text for empty array', () => {
|
|
75
|
+
const input = '[]';
|
|
76
|
+
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
77
|
+
(0, vitest_1.expect)(result).toEqual([{ type: 'text', text: '[]' }]);
|
|
78
|
+
});
|
|
79
|
+
(0, vitest_1.it)('returns text for malformed JSON array', () => {
|
|
80
|
+
const input = '[{"type":"function","function":{"name":"search","parameters":';
|
|
81
|
+
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
82
|
+
(0, vitest_1.expect)(result).toEqual([{ type: 'text', text: input }]);
|
|
147
83
|
});
|
|
148
84
|
});
|
|
@@ -20,8 +20,15 @@ const WS_APP_VER = process.env.WS_APP_VER ?? '1.48.2';
|
|
|
20
20
|
const WS_LS_VER = process.env.WS_LS_VER ?? '1.9544.35';
|
|
21
21
|
const SENTRY_PUBLIC_KEY = 'b813f73488da69eedec534dba1029111';
|
|
22
22
|
const CONNECT_USER_AGENT = 'connect-go/1.18.1 (go1.25.5)';
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
Example format:
|
|
26
|
+
[{"type": "function", "function": {"name": "tool_name", "parameters": {"arg1": "value1"}}}]
|
|
27
|
+
|
|
28
|
+
For multiple tool calls:
|
|
29
|
+
[{"type": "function", "function": {"name": "tool1", "parameters": {"arg1": "value1"}}}, {"type": "function", "function": {"name": "tool2", "parameters": {"arg2": "value2"}}}]`;
|
|
23
30
|
class DevstralLanguageModel {
|
|
24
|
-
specificationVersion = '
|
|
31
|
+
specificationVersion = 'v2';
|
|
25
32
|
provider = 'windsurf';
|
|
26
33
|
modelId;
|
|
27
34
|
supportedUrls = {};
|
|
@@ -58,16 +65,13 @@ class DevstralLanguageModel {
|
|
|
58
65
|
const responseFrame = await this.transport.postUnary(`${this.baseURL}${API_SERVICE_PATH}${DEVSTRAL_STREAM_PATH}`, requestFrame, headers);
|
|
59
66
|
const { payloads: responsePayloads } = (0, connect_frame_js_1.connectFrameDecode)(responseFrame);
|
|
60
67
|
const payloads = responsePayloads.length > 0 ? responsePayloads : [responseFrame];
|
|
61
|
-
const content = payloads.flatMap((payload) =>
|
|
68
|
+
const content = payloads.flatMap((payload) => toV2Content((0, response_converter_js_1.convertResponse)(payload)));
|
|
62
69
|
const unified = content.some((part) => part.type === 'tool-call')
|
|
63
70
|
? 'tool-calls'
|
|
64
71
|
: 'stop';
|
|
65
72
|
return {
|
|
66
73
|
content,
|
|
67
|
-
finishReason:
|
|
68
|
-
unified,
|
|
69
|
-
raw: undefined,
|
|
70
|
-
},
|
|
74
|
+
finishReason: unified,
|
|
71
75
|
usage: emptyUsage(),
|
|
72
76
|
warnings: [],
|
|
73
77
|
};
|
|
@@ -125,7 +129,7 @@ class DevstralLanguageModel {
|
|
|
125
129
|
break;
|
|
126
130
|
}
|
|
127
131
|
pending = frameResult.rest;
|
|
128
|
-
const contentParts =
|
|
132
|
+
const contentParts = toV2Content((0, response_converter_js_1.convertResponse)(frameResult.payload));
|
|
129
133
|
for (const part of contentParts) {
|
|
130
134
|
if (isAborted(options.abortSignal)) {
|
|
131
135
|
safeClose(controller);
|
|
@@ -175,10 +179,7 @@ class DevstralLanguageModel {
|
|
|
175
179
|
const unified = hasToolCalls ? 'tool-calls' : 'stop';
|
|
176
180
|
safeEnqueue(controller, {
|
|
177
181
|
type: 'finish',
|
|
178
|
-
finishReason:
|
|
179
|
-
unified,
|
|
180
|
-
raw: undefined,
|
|
181
|
-
},
|
|
182
|
+
finishReason: unified,
|
|
182
183
|
usage: emptyUsage(),
|
|
183
184
|
});
|
|
184
185
|
safeClose(controller);
|
|
@@ -192,10 +193,7 @@ class DevstralLanguageModel {
|
|
|
192
193
|
});
|
|
193
194
|
safeEnqueue(controller, {
|
|
194
195
|
type: 'finish',
|
|
195
|
-
finishReason:
|
|
196
|
-
unified: 'error',
|
|
197
|
-
raw: undefined,
|
|
198
|
-
},
|
|
196
|
+
finishReason: 'error',
|
|
199
197
|
usage: emptyUsage(),
|
|
200
198
|
});
|
|
201
199
|
}
|
|
@@ -255,20 +253,12 @@ function isAborted(signal) {
|
|
|
255
253
|
}
|
|
256
254
|
function emptyUsage() {
|
|
257
255
|
return {
|
|
258
|
-
inputTokens:
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
cacheRead: undefined,
|
|
262
|
-
cacheWrite: undefined,
|
|
263
|
-
},
|
|
264
|
-
outputTokens: {
|
|
265
|
-
total: undefined,
|
|
266
|
-
text: undefined,
|
|
267
|
-
reasoning: undefined,
|
|
268
|
-
},
|
|
256
|
+
inputTokens: undefined,
|
|
257
|
+
outputTokens: undefined,
|
|
258
|
+
totalTokens: undefined,
|
|
269
259
|
};
|
|
270
260
|
}
|
|
271
|
-
function
|
|
261
|
+
function toV2Content(parts) {
|
|
272
262
|
return parts.map((part) => {
|
|
273
263
|
if (part.type !== 'tool-call') {
|
|
274
264
|
return part;
|
|
@@ -288,10 +278,13 @@ function isFunctionTool(tool) {
|
|
|
288
278
|
function buildGenerateRequest(input) {
|
|
289
279
|
const request = new protobuf_js_1.ProtobufEncoder();
|
|
290
280
|
request.writeMessage(1, buildMetadata(input.apiKey, input.jwt));
|
|
291
|
-
|
|
281
|
+
const functionTools = input.tools?.filter(isFunctionTool) ?? [];
|
|
282
|
+
const messages = functionTools.length > 0
|
|
283
|
+
? [{ role: 5, content: TOOL_FORMAT_INSTRUCTION }, ...input.messages]
|
|
284
|
+
: input.messages;
|
|
285
|
+
for (const message of messages) {
|
|
292
286
|
request.writeMessage(2, buildMessage(message));
|
|
293
287
|
}
|
|
294
|
-
const functionTools = input.tools?.filter(isFunctionTool) ?? [];
|
|
295
288
|
if (functionTools.length > 0) {
|
|
296
289
|
const toolsArray = functionTools.map((tool) => ({
|
|
297
290
|
type: 'function',
|