@bxb1337/windsurf-fast-context 1.1.1 → 1.1.3
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/response-converter.js +216 -25
- package/dist/cjs/conversion/response-converter.test.js +41 -14
- package/dist/cjs/model/devstral-language-model.js +1 -3
- package/dist/cjs/model/devstral-language-model.test.js +6 -6
- package/dist/esm/conversion/response-converter.js +216 -25
- package/dist/esm/conversion/response-converter.test.js +41 -14
- package/dist/esm/model/devstral-language-model.js +1 -3
- package/dist/esm/model/devstral-language-model.test.js +6 -6
- package/package.json +1 -1
|
@@ -3,38 +3,192 @@ 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]';
|
|
6
8
|
const STOP_TOKEN = '</s>';
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
if (!trimmed.startsWith('['))
|
|
9
|
+
function parseOpenAIToolCalls(responseText) {
|
|
10
|
+
if (!responseText.startsWith('TOOL_CALLS')) {
|
|
10
11
|
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;
|
|
28
12
|
}
|
|
29
|
-
|
|
13
|
+
const jsonPart = responseText.slice('TOOL_CALLS'.length);
|
|
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) {
|
|
30
44
|
return null;
|
|
31
45
|
}
|
|
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
|
+
}
|
|
32
69
|
}
|
|
33
70
|
function pushText(parts, text) {
|
|
34
71
|
if (text.length > 0) {
|
|
35
72
|
parts.push({ type: 'text', text });
|
|
36
73
|
}
|
|
37
74
|
}
|
|
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
|
+
}
|
|
38
192
|
function hasControlChars(value) {
|
|
39
193
|
for (const char of value) {
|
|
40
194
|
const code = char.charCodeAt(0);
|
|
@@ -62,6 +216,10 @@ function isLikelyMetadata(value) {
|
|
|
62
216
|
return /^[A-Za-z0-9._:-]{1,32}$/.test(value);
|
|
63
217
|
}
|
|
64
218
|
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
|
+
}
|
|
65
223
|
const nonMetadata = values.filter((value) => !isLikelyMetadata(value));
|
|
66
224
|
const candidates = nonMetadata.length > 0 ? nonMetadata : values;
|
|
67
225
|
return candidates.reduce((best, current) => (current.length > best.length ? current : best), candidates[0] ?? '');
|
|
@@ -81,10 +239,43 @@ function decodeResponseText(buffer) {
|
|
|
81
239
|
function convertResponse(buffer) {
|
|
82
240
|
let responseText = decodeResponseText(buffer);
|
|
83
241
|
responseText = responseText.replace(STOP_TOKEN, '');
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
86
|
-
return
|
|
242
|
+
const openaiToolCalls = parseOpenAIToolCalls(responseText);
|
|
243
|
+
if (openaiToolCalls) {
|
|
244
|
+
return openaiToolCalls;
|
|
245
|
+
}
|
|
87
246
|
const parts = [];
|
|
88
|
-
|
|
247
|
+
let cursor = 0;
|
|
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
|
+
}
|
|
89
280
|
return parts;
|
|
90
281
|
}
|
|
@@ -33,38 +33,65 @@ const response_converter_js_1 = require("./response-converter.js");
|
|
|
33
33
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
34
34
|
(0, vitest_1.expect)(result).toEqual([{ type: 'text', text: 'Hello world' }]);
|
|
35
35
|
});
|
|
36
|
-
//
|
|
37
|
-
(0, vitest_1.it)('parses
|
|
38
|
-
const input = '[
|
|
36
|
+
// Native Windsurf marker format tests
|
|
37
|
+
(0, vitest_1.it)('parses native marker format [TOOL_CALLS]name[ARGS]{json}', () => {
|
|
38
|
+
const input = '[TOOL_CALLS]search[ARGS]{"q":"test"}';
|
|
39
39
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
40
40
|
(0, vitest_1.expect)(result).toEqual([
|
|
41
41
|
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'search', input: { q: 'test' } }
|
|
42
42
|
]);
|
|
43
43
|
});
|
|
44
|
-
(0, vitest_1.it)('parses
|
|
45
|
-
const input = '[
|
|
44
|
+
(0, vitest_1.it)('parses marker format with text before tool call', () => {
|
|
45
|
+
const input = 'Some text before[TOOL_CALLS]read[ARGS]{"path":"/foo"}';
|
|
46
|
+
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
47
|
+
(0, vitest_1.expect)(result).toEqual([
|
|
48
|
+
{ type: 'text', text: 'Some text before' },
|
|
49
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: { path: '/foo' } }
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
(0, vitest_1.it)('parses marker format with text after tool call', () => {
|
|
53
|
+
const input = '[TOOL_CALLS]grep[ARGS]{"pattern":"foo"}Some text after';
|
|
54
|
+
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
55
|
+
(0, vitest_1.expect)(result).toEqual([
|
|
56
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: { pattern: 'foo' } },
|
|
57
|
+
{ type: 'text', text: 'Some text after' }
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
(0, vitest_1.it)('parses multiple tool calls in marker format', () => {
|
|
61
|
+
const input = '[TOOL_CALLS]read[ARGS]{"path":"/a"}[TOOL_CALLS]grep[ARGS]{"pattern":"foo"}';
|
|
46
62
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
47
63
|
(0, vitest_1.expect)(result).toEqual([
|
|
48
64
|
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: { path: '/a' } },
|
|
49
65
|
{ type: 'tool-call', toolCallId: 'toolcall_2', toolName: 'grep', input: { pattern: 'foo' } }
|
|
50
66
|
]);
|
|
51
67
|
});
|
|
52
|
-
(0, vitest_1.it)('parses
|
|
53
|
-
const input = '
|
|
68
|
+
(0, vitest_1.it)('parses TOOL_CALLS format with numeric tool id', () => {
|
|
69
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"pattern":"test"}}}';
|
|
54
70
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
55
71
|
(0, vitest_1.expect)(result).toEqual([
|
|
56
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: '
|
|
72
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: { pattern: 'test' } }
|
|
57
73
|
]);
|
|
58
74
|
});
|
|
59
|
-
(0, vitest_1.it)('
|
|
60
|
-
const input = '
|
|
75
|
+
(0, vitest_1.it)('maps tool id 1 to read', () => {
|
|
76
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":1,"parameters":{"path":"/file"}}}';
|
|
61
77
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
62
|
-
(0, vitest_1.expect)(result).toEqual([
|
|
78
|
+
(0, vitest_1.expect)(result).toEqual([
|
|
79
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: { path: '/file' } }
|
|
80
|
+
]);
|
|
63
81
|
});
|
|
64
|
-
(0, vitest_1.it)('
|
|
65
|
-
const input = 'TOOL_CALLS{"type":"function","function":{"name":
|
|
82
|
+
(0, vitest_1.it)('maps tool id 2 to glob', () => {
|
|
83
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":2,"parameters":{"pattern":"*.ts"}}}';
|
|
66
84
|
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
67
|
-
(0, vitest_1.expect)(result).toEqual([
|
|
85
|
+
(0, vitest_1.expect)(result).toEqual([
|
|
86
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'glob', input: { pattern: '*.ts' } }
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
(0, vitest_1.it)('maps unknown tool id to tool_N', () => {
|
|
90
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":99,"parameters":{}}}';
|
|
91
|
+
const result = (0, response_converter_js_1.convertResponse)(Buffer.from(input, 'utf8'));
|
|
92
|
+
(0, vitest_1.expect)(result).toEqual([
|
|
93
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'tool_99', input: {} }
|
|
94
|
+
]);
|
|
68
95
|
});
|
|
69
96
|
(0, vitest_1.it)('returns text for non-array JSON', () => {
|
|
70
97
|
const input = '{"type":"function","function":{"name":"search","parameters":{}}}';
|
|
@@ -279,9 +279,7 @@ function buildGenerateRequest(input) {
|
|
|
279
279
|
const request = new protobuf_js_1.ProtobufEncoder();
|
|
280
280
|
request.writeMessage(1, buildMetadata(input.apiKey, input.jwt));
|
|
281
281
|
const functionTools = input.tools?.filter(isFunctionTool) ?? [];
|
|
282
|
-
const messages =
|
|
283
|
-
? [{ role: 5, content: TOOL_FORMAT_INSTRUCTION }, ...input.messages]
|
|
284
|
-
: input.messages;
|
|
282
|
+
const messages = [{ role: 5, content: TOOL_FORMAT_INSTRUCTION }, ...input.messages];
|
|
285
283
|
for (const message of messages) {
|
|
286
284
|
request.writeMessage(2, buildMessage(message));
|
|
287
285
|
}
|
|
@@ -163,7 +163,7 @@ async function collectStreamParts(stream) {
|
|
|
163
163
|
return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
|
|
164
164
|
}
|
|
165
165
|
requestBodies.push(bufferFromBody(init?.body));
|
|
166
|
-
const payload = Buffer.from('[
|
|
166
|
+
const payload = Buffer.from('[TOOL_CALLS]searchRepo[ARGS]{"query":"jwt manager"}', 'utf8');
|
|
167
167
|
return new Response(Uint8Array.from((0, connect_frame_js_1.connectFrameEncode)(payload)), { status: 200 });
|
|
168
168
|
};
|
|
169
169
|
const model = new devstral_language_model_js_1.DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
|
|
@@ -306,7 +306,7 @@ async function collectStreamParts(stream) {
|
|
|
306
306
|
const combined = strings.join('\n');
|
|
307
307
|
(0, vitest_1.expect)(combined).toContain('When you need to call tools');
|
|
308
308
|
});
|
|
309
|
-
(0, vitest_1.it)('
|
|
309
|
+
(0, vitest_1.it)('always injects tool format instruction even without tools', async () => {
|
|
310
310
|
const requestBodies = [];
|
|
311
311
|
const jwt = makeJwt(4_200_000_101, 'no-instruction');
|
|
312
312
|
const fakeFetch = async (input, init) => {
|
|
@@ -323,9 +323,9 @@ async function collectStreamParts(stream) {
|
|
|
323
323
|
});
|
|
324
324
|
const strings = (0, protobuf_js_1.extractStrings)(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
325
325
|
const combined = strings.join('\n');
|
|
326
|
-
(0, vitest_1.expect)(combined).
|
|
326
|
+
(0, vitest_1.expect)(combined).toContain('When you need to call tools');
|
|
327
327
|
});
|
|
328
|
-
(0, vitest_1.it)('
|
|
328
|
+
(0, vitest_1.it)('always injects tool format instruction even with only provider-defined tools', async () => {
|
|
329
329
|
const requestBodies = [];
|
|
330
330
|
const jwt = makeJwt(4_200_000_102, 'provider-only');
|
|
331
331
|
const fakeFetch = async (input, init) => {
|
|
@@ -350,7 +350,7 @@ async function collectStreamParts(stream) {
|
|
|
350
350
|
});
|
|
351
351
|
const strings = (0, protobuf_js_1.extractStrings)(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
352
352
|
const combined = strings.join('\n');
|
|
353
|
-
(0, vitest_1.expect)(combined).
|
|
353
|
+
(0, vitest_1.expect)(combined).toContain('When you need to call tools');
|
|
354
354
|
});
|
|
355
355
|
});
|
|
356
356
|
(0, vitest_1.describe)('DevstralLanguageModel doStream', () => {
|
|
@@ -456,7 +456,7 @@ async function collectStreamParts(stream) {
|
|
|
456
456
|
});
|
|
457
457
|
(0, vitest_1.it)('stream-tool emits tool-input deltas before final tool-call and finish', async () => {
|
|
458
458
|
const jwt = makeJwt(4_300_000_001, 'stream-tool');
|
|
459
|
-
const toolPayload = Buffer.from('[
|
|
459
|
+
const toolPayload = Buffer.from('[TOOL_CALLS]searchRepo[ARGS]{"query":"jwt manager"}', 'utf8');
|
|
460
460
|
const frames = Buffer.concat([(0, connect_frame_js_1.connectFrameEncode)(toolPayload)]);
|
|
461
461
|
const fakeFetch = async (input) => {
|
|
462
462
|
const url = String(input);
|
|
@@ -1,37 +1,191 @@
|
|
|
1
1
|
import { gunzipSync } from 'node:zlib';
|
|
2
2
|
import { extractStrings } from '../protocol/protobuf.js';
|
|
3
|
+
const TOOL_CALL_PREFIX = '[TOOL_CALLS]';
|
|
4
|
+
const ARGS_PREFIX = '[ARGS]';
|
|
3
5
|
const STOP_TOKEN = '</s>';
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
if (!trimmed.startsWith('['))
|
|
6
|
+
function parseOpenAIToolCalls(responseText) {
|
|
7
|
+
if (!responseText.startsWith('TOOL_CALLS')) {
|
|
7
8
|
return null;
|
|
8
|
-
try {
|
|
9
|
-
const parsed = JSON.parse(trimmed);
|
|
10
|
-
if (!Array.isArray(parsed) || parsed.length === 0)
|
|
11
|
-
return null;
|
|
12
|
-
const toolCalls = [];
|
|
13
|
-
for (let i = 0; i < parsed.length; i++) {
|
|
14
|
-
const item = parsed[i];
|
|
15
|
-
if (item.type !== 'function' || !item.function?.name)
|
|
16
|
-
continue;
|
|
17
|
-
toolCalls.push({
|
|
18
|
-
type: 'tool-call',
|
|
19
|
-
toolCallId: `toolcall_${i + 1}`,
|
|
20
|
-
toolName: String(item.function.name),
|
|
21
|
-
input: item.function.parameters ?? {},
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
return toolCalls.length > 0 ? toolCalls : null;
|
|
25
9
|
}
|
|
26
|
-
|
|
10
|
+
const jsonPart = responseText.slice('TOOL_CALLS'.length);
|
|
11
|
+
if (!jsonPart.startsWith('{')) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const toolCalls = [];
|
|
15
|
+
let cursor = 0;
|
|
16
|
+
while (cursor < jsonPart.length) {
|
|
17
|
+
if (jsonPart[cursor] !== '{') {
|
|
18
|
+
cursor++;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const endResult = parseBalancedEnd(jsonPart, cursor);
|
|
22
|
+
if (endResult == null) {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
const jsonStr = jsonPart.slice(cursor, endResult);
|
|
26
|
+
cursor = endResult;
|
|
27
|
+
if (jsonStr === '{}') {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(jsonStr);
|
|
32
|
+
if (parsed.type === 'function' && parsed.function) {
|
|
33
|
+
toolCalls.push(parsed);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (toolCalls.length === 0) {
|
|
27
41
|
return null;
|
|
28
42
|
}
|
|
43
|
+
return toolCalls.map((call, index) => {
|
|
44
|
+
const toolId = call.function.name;
|
|
45
|
+
const toolName = typeof toolId === 'number' ? mapToolIdToName(toolId) : String(toolId);
|
|
46
|
+
const args = call.function.parameters ?? {};
|
|
47
|
+
return {
|
|
48
|
+
type: 'tool-call',
|
|
49
|
+
toolCallId: `toolcall_${index + 1}`,
|
|
50
|
+
toolName,
|
|
51
|
+
input: args,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function mapToolIdToName(id) {
|
|
56
|
+
switch (id) {
|
|
57
|
+
case 1:
|
|
58
|
+
return 'read';
|
|
59
|
+
case 2:
|
|
60
|
+
return 'glob';
|
|
61
|
+
case 3:
|
|
62
|
+
return 'grep';
|
|
63
|
+
default:
|
|
64
|
+
return `tool_${id}`;
|
|
65
|
+
}
|
|
29
66
|
}
|
|
30
67
|
function pushText(parts, text) {
|
|
31
68
|
if (text.length > 0) {
|
|
32
69
|
parts.push({ type: 'text', text });
|
|
33
70
|
}
|
|
34
71
|
}
|
|
72
|
+
function parseStringEnd(value, startIndex) {
|
|
73
|
+
let index = startIndex + 1;
|
|
74
|
+
let escaping = false;
|
|
75
|
+
while (index < value.length) {
|
|
76
|
+
const char = value[index];
|
|
77
|
+
if (escaping) {
|
|
78
|
+
escaping = false;
|
|
79
|
+
}
|
|
80
|
+
else if (char === '\\') {
|
|
81
|
+
escaping = true;
|
|
82
|
+
}
|
|
83
|
+
else if (char === '"') {
|
|
84
|
+
return index + 1;
|
|
85
|
+
}
|
|
86
|
+
index += 1;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
function parseBalancedEnd(value, startIndex) {
|
|
91
|
+
const stack = [value[startIndex] === '{' ? '}' : ']'];
|
|
92
|
+
let index = startIndex + 1;
|
|
93
|
+
let inString = false;
|
|
94
|
+
let escaping = false;
|
|
95
|
+
while (index < value.length) {
|
|
96
|
+
const char = value[index];
|
|
97
|
+
if (inString) {
|
|
98
|
+
if (escaping) {
|
|
99
|
+
escaping = false;
|
|
100
|
+
}
|
|
101
|
+
else if (char === '\\') {
|
|
102
|
+
escaping = true;
|
|
103
|
+
}
|
|
104
|
+
else if (char === '"') {
|
|
105
|
+
inString = false;
|
|
106
|
+
}
|
|
107
|
+
index += 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (char === '"') {
|
|
111
|
+
inString = true;
|
|
112
|
+
index += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (char === '{') {
|
|
116
|
+
stack.push('}');
|
|
117
|
+
index += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (char === '[') {
|
|
121
|
+
stack.push(']');
|
|
122
|
+
index += 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (char === '}' || char === ']') {
|
|
126
|
+
const expected = stack[stack.length - 1];
|
|
127
|
+
if (expected !== char) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
stack.pop();
|
|
131
|
+
index += 1;
|
|
132
|
+
if (stack.length === 0) {
|
|
133
|
+
return index;
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
index += 1;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
function parsePrimitiveEnd(value, startIndex) {
|
|
142
|
+
let index = startIndex;
|
|
143
|
+
while (index < value.length) {
|
|
144
|
+
const char = value[index];
|
|
145
|
+
if (char === ' ' || char === '\n' || char === '\r' || char === '\t') {
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
index += 1;
|
|
149
|
+
}
|
|
150
|
+
return index;
|
|
151
|
+
}
|
|
152
|
+
function parseJsonValue(value, startIndex) {
|
|
153
|
+
let jsonStart = startIndex;
|
|
154
|
+
while (jsonStart < value.length) {
|
|
155
|
+
const char = value[jsonStart];
|
|
156
|
+
if (char !== ' ' && char !== '\n' && char !== '\r' && char !== '\t') {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
jsonStart += 1;
|
|
160
|
+
}
|
|
161
|
+
if (jsonStart >= value.length) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const firstChar = value[jsonStart];
|
|
165
|
+
let endIndex;
|
|
166
|
+
if (firstChar === '{' || firstChar === '[') {
|
|
167
|
+
endIndex = parseBalancedEnd(value, jsonStart);
|
|
168
|
+
}
|
|
169
|
+
else if (firstChar === '"') {
|
|
170
|
+
endIndex = parseStringEnd(value, jsonStart);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
endIndex = parsePrimitiveEnd(value, jsonStart);
|
|
174
|
+
}
|
|
175
|
+
if (endIndex == null || endIndex <= jsonStart) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const rawJson = value.slice(jsonStart, endIndex);
|
|
179
|
+
try {
|
|
180
|
+
return {
|
|
181
|
+
parsed: JSON.parse(rawJson),
|
|
182
|
+
endIndex,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
35
189
|
function hasControlChars(value) {
|
|
36
190
|
for (const char of value) {
|
|
37
191
|
const code = char.charCodeAt(0);
|
|
@@ -59,6 +213,10 @@ function isLikelyMetadata(value) {
|
|
|
59
213
|
return /^[A-Za-z0-9._:-]{1,32}$/.test(value);
|
|
60
214
|
}
|
|
61
215
|
function pickBestExtractedText(values) {
|
|
216
|
+
const markerValues = values.filter((value) => value.includes(TOOL_CALL_PREFIX) || value.includes(ARGS_PREFIX));
|
|
217
|
+
if (markerValues.length > 0) {
|
|
218
|
+
return markerValues.join('');
|
|
219
|
+
}
|
|
62
220
|
const nonMetadata = values.filter((value) => !isLikelyMetadata(value));
|
|
63
221
|
const candidates = nonMetadata.length > 0 ? nonMetadata : values;
|
|
64
222
|
return candidates.reduce((best, current) => (current.length > best.length ? current : best), candidates[0] ?? '');
|
|
@@ -78,10 +236,43 @@ function decodeResponseText(buffer) {
|
|
|
78
236
|
export function convertResponse(buffer) {
|
|
79
237
|
let responseText = decodeResponseText(buffer);
|
|
80
238
|
responseText = responseText.replace(STOP_TOKEN, '');
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
83
|
-
return
|
|
239
|
+
const openaiToolCalls = parseOpenAIToolCalls(responseText);
|
|
240
|
+
if (openaiToolCalls) {
|
|
241
|
+
return openaiToolCalls;
|
|
242
|
+
}
|
|
84
243
|
const parts = [];
|
|
85
|
-
|
|
244
|
+
let cursor = 0;
|
|
245
|
+
let toolCallCount = 0;
|
|
246
|
+
while (cursor < responseText.length) {
|
|
247
|
+
const markerStart = responseText.indexOf(TOOL_CALL_PREFIX, cursor);
|
|
248
|
+
if (markerStart === -1) {
|
|
249
|
+
pushText(parts, responseText.slice(cursor));
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
pushText(parts, responseText.slice(cursor, markerStart));
|
|
253
|
+
const toolNameStart = markerStart + TOOL_CALL_PREFIX.length;
|
|
254
|
+
const argsStart = responseText.indexOf(ARGS_PREFIX, toolNameStart);
|
|
255
|
+
if (argsStart === -1) {
|
|
256
|
+
pushText(parts, responseText.slice(markerStart));
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
const toolName = responseText.slice(toolNameStart, argsStart);
|
|
260
|
+
const parsedArgs = parseJsonValue(responseText, argsStart + ARGS_PREFIX.length);
|
|
261
|
+
if (parsedArgs == null) {
|
|
262
|
+
const nextMarker = responseText.indexOf(TOOL_CALL_PREFIX, markerStart + TOOL_CALL_PREFIX.length);
|
|
263
|
+
const malformedEnd = nextMarker === -1 ? responseText.length : nextMarker;
|
|
264
|
+
pushText(parts, responseText.slice(markerStart, malformedEnd));
|
|
265
|
+
cursor = malformedEnd;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
toolCallCount += 1;
|
|
269
|
+
parts.push({
|
|
270
|
+
type: 'tool-call',
|
|
271
|
+
toolCallId: `toolcall_${toolCallCount}`,
|
|
272
|
+
toolName,
|
|
273
|
+
input: parsedArgs.parsed,
|
|
274
|
+
});
|
|
275
|
+
cursor = parsedArgs.endIndex;
|
|
276
|
+
}
|
|
86
277
|
return parts;
|
|
87
278
|
}
|
|
@@ -31,38 +31,65 @@ describe('convertResponse', () => {
|
|
|
31
31
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
32
32
|
expect(result).toEqual([{ type: 'text', text: 'Hello world' }]);
|
|
33
33
|
});
|
|
34
|
-
//
|
|
35
|
-
it('parses
|
|
36
|
-
const input = '[
|
|
34
|
+
// Native Windsurf marker format tests
|
|
35
|
+
it('parses native marker format [TOOL_CALLS]name[ARGS]{json}', () => {
|
|
36
|
+
const input = '[TOOL_CALLS]search[ARGS]{"q":"test"}';
|
|
37
37
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
38
38
|
expect(result).toEqual([
|
|
39
39
|
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'search', input: { q: 'test' } }
|
|
40
40
|
]);
|
|
41
41
|
});
|
|
42
|
-
it('parses
|
|
43
|
-
const input = '[
|
|
42
|
+
it('parses marker format with text before tool call', () => {
|
|
43
|
+
const input = 'Some text before[TOOL_CALLS]read[ARGS]{"path":"/foo"}';
|
|
44
|
+
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
45
|
+
expect(result).toEqual([
|
|
46
|
+
{ type: 'text', text: 'Some text before' },
|
|
47
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: { path: '/foo' } }
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
it('parses marker format with text after tool call', () => {
|
|
51
|
+
const input = '[TOOL_CALLS]grep[ARGS]{"pattern":"foo"}Some text after';
|
|
52
|
+
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
53
|
+
expect(result).toEqual([
|
|
54
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: { pattern: 'foo' } },
|
|
55
|
+
{ type: 'text', text: 'Some text after' }
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
it('parses multiple tool calls in marker format', () => {
|
|
59
|
+
const input = '[TOOL_CALLS]read[ARGS]{"path":"/a"}[TOOL_CALLS]grep[ARGS]{"pattern":"foo"}';
|
|
44
60
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
45
61
|
expect(result).toEqual([
|
|
46
62
|
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: { path: '/a' } },
|
|
47
63
|
{ type: 'tool-call', toolCallId: 'toolcall_2', toolName: 'grep', input: { pattern: 'foo' } }
|
|
48
64
|
]);
|
|
49
65
|
});
|
|
50
|
-
it('parses
|
|
51
|
-
const input = '
|
|
66
|
+
it('parses TOOL_CALLS format with numeric tool id', () => {
|
|
67
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"pattern":"test"}}}';
|
|
52
68
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
53
69
|
expect(result).toEqual([
|
|
54
|
-
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: '
|
|
70
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'grep', input: { pattern: 'test' } }
|
|
55
71
|
]);
|
|
56
72
|
});
|
|
57
|
-
it('
|
|
58
|
-
const input = '
|
|
73
|
+
it('maps tool id 1 to read', () => {
|
|
74
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":1,"parameters":{"path":"/file"}}}';
|
|
59
75
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
60
|
-
expect(result).toEqual([
|
|
76
|
+
expect(result).toEqual([
|
|
77
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'read', input: { path: '/file' } }
|
|
78
|
+
]);
|
|
61
79
|
});
|
|
62
|
-
it('
|
|
63
|
-
const input = 'TOOL_CALLS{"type":"function","function":{"name":
|
|
80
|
+
it('maps tool id 2 to glob', () => {
|
|
81
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":2,"parameters":{"pattern":"*.ts"}}}';
|
|
64
82
|
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
65
|
-
expect(result).toEqual([
|
|
83
|
+
expect(result).toEqual([
|
|
84
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'glob', input: { pattern: '*.ts' } }
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
it('maps unknown tool id to tool_N', () => {
|
|
88
|
+
const input = 'TOOL_CALLS{"type":"function","function":{"name":99,"parameters":{}}}';
|
|
89
|
+
const result = convertResponse(Buffer.from(input, 'utf8'));
|
|
90
|
+
expect(result).toEqual([
|
|
91
|
+
{ type: 'tool-call', toolCallId: 'toolcall_1', toolName: 'tool_99', input: {} }
|
|
92
|
+
]);
|
|
66
93
|
});
|
|
67
94
|
it('returns text for non-array JSON', () => {
|
|
68
95
|
const input = '{"type":"function","function":{"name":"search","parameters":{}}}';
|
|
@@ -275,9 +275,7 @@ function buildGenerateRequest(input) {
|
|
|
275
275
|
const request = new ProtobufEncoder();
|
|
276
276
|
request.writeMessage(1, buildMetadata(input.apiKey, input.jwt));
|
|
277
277
|
const functionTools = input.tools?.filter(isFunctionTool) ?? [];
|
|
278
|
-
const messages =
|
|
279
|
-
? [{ role: 5, content: TOOL_FORMAT_INSTRUCTION }, ...input.messages]
|
|
280
|
-
: input.messages;
|
|
278
|
+
const messages = [{ role: 5, content: TOOL_FORMAT_INSTRUCTION }, ...input.messages];
|
|
281
279
|
for (const message of messages) {
|
|
282
280
|
request.writeMessage(2, buildMessage(message));
|
|
283
281
|
}
|
|
@@ -161,7 +161,7 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
161
161
|
return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
|
|
162
162
|
}
|
|
163
163
|
requestBodies.push(bufferFromBody(init?.body));
|
|
164
|
-
const payload = Buffer.from('[
|
|
164
|
+
const payload = Buffer.from('[TOOL_CALLS]searchRepo[ARGS]{"query":"jwt manager"}', 'utf8');
|
|
165
165
|
return new Response(Uint8Array.from(connectFrameEncode(payload)), { status: 200 });
|
|
166
166
|
};
|
|
167
167
|
const model = new DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
|
|
@@ -304,7 +304,7 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
304
304
|
const combined = strings.join('\n');
|
|
305
305
|
expect(combined).toContain('When you need to call tools');
|
|
306
306
|
});
|
|
307
|
-
it('
|
|
307
|
+
it('always injects tool format instruction even without tools', async () => {
|
|
308
308
|
const requestBodies = [];
|
|
309
309
|
const jwt = makeJwt(4_200_000_101, 'no-instruction');
|
|
310
310
|
const fakeFetch = async (input, init) => {
|
|
@@ -321,9 +321,9 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
321
321
|
});
|
|
322
322
|
const strings = extractStrings(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
323
323
|
const combined = strings.join('\n');
|
|
324
|
-
expect(combined).
|
|
324
|
+
expect(combined).toContain('When you need to call tools');
|
|
325
325
|
});
|
|
326
|
-
it('
|
|
326
|
+
it('always injects tool format instruction even with only provider-defined tools', async () => {
|
|
327
327
|
const requestBodies = [];
|
|
328
328
|
const jwt = makeJwt(4_200_000_102, 'provider-only');
|
|
329
329
|
const fakeFetch = async (input, init) => {
|
|
@@ -348,7 +348,7 @@ describe('DevstralLanguageModel doGenerate', () => {
|
|
|
348
348
|
});
|
|
349
349
|
const strings = extractStrings(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
|
|
350
350
|
const combined = strings.join('\n');
|
|
351
|
-
expect(combined).
|
|
351
|
+
expect(combined).toContain('When you need to call tools');
|
|
352
352
|
});
|
|
353
353
|
});
|
|
354
354
|
describe('DevstralLanguageModel doStream', () => {
|
|
@@ -454,7 +454,7 @@ describe('DevstralLanguageModel doStream', () => {
|
|
|
454
454
|
});
|
|
455
455
|
it('stream-tool emits tool-input deltas before final tool-call and finish', async () => {
|
|
456
456
|
const jwt = makeJwt(4_300_000_001, 'stream-tool');
|
|
457
|
-
const toolPayload = Buffer.from('[
|
|
457
|
+
const toolPayload = Buffer.from('[TOOL_CALLS]searchRepo[ARGS]{"query":"jwt manager"}', 'utf8');
|
|
458
458
|
const frames = Buffer.concat([connectFrameEncode(toolPayload)]);
|
|
459
459
|
const fakeFetch = async (input) => {
|
|
460
460
|
const url = String(input);
|