@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.
@@ -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 (V3 output shape)', () => {
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 execution-denied output includes reason', () => {
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: 'execution-denied', reason: 'User rejected tool execution' },
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: '{"type":"execution-denied","reason":"User rejected tool execution"}',
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 execution-denied output handles missing reason', () => {
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-denied' },
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: '{"type":"execution-denied"}',
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 parseOpenAIToolCalls(responseText) {
10
- if (!responseText.startsWith('TOOL_CALLS')) {
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
- 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) {
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 openaiToolCalls = parseOpenAIToolCalls(responseText);
243
- if (openaiToolCalls) {
244
- return openaiToolCalls;
245
- }
84
+ const strictToolCalls = parseStrictOpenAIToolCalls(responseText);
85
+ if (strictToolCalls)
86
+ return strictToolCalls;
246
87
  const parts = [];
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
- }
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
- (0, vitest_1.it)('parses OpenAI-style TOOL_CALLS with numeric IDs', () => {
83
- const input = 'TOOL_CALLS{"type":"function","function":{"name":3,"parameters":{"file_path":"/home/test","search_pattern":"binance"}}}{}';
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 multiple OpenAI-style tool calls', () => {
95
- const input = 'TOOL_CALLS{"type":"function","function":{"name":1,"parameters":{"file_path":"/home/test"}}}, {"type":"function","function":{"name":2,"parameters":{"pattern":"*.ts"}}}{}';
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
- type: 'tool-call',
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)('maps unknown tool IDs to tool_N format', () => {
113
- const input = 'TOOL_CALLS{"type":"function","function":{"name":99,"parameters":{"arg":"value"}}}{}';
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)('handles OpenAI-style with string tool names', () => {
125
- const input = 'TOOL_CALLS{"type":"function","function":{"name":"custom_tool","parameters":{"key":"value"}}}{}';
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)('handles OpenAI-style with string name "answer" - emits as tool-call', () => {
137
- const input = 'TOOL_CALLS{"type":"function","function":{"name":"answer","parameters":{"answer":"final answer"}}}{}';
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
- type: 'tool-call',
142
- toolCallId: 'toolcall_1',
143
- toolName: 'answer',
144
- input: { answer: 'final answer' },
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 = 'v3';
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) => toV3Content((0, response_converter_js_1.convertResponse)(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 = toV3Content((0, response_converter_js_1.convertResponse)(frameResult.payload));
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
- total: undefined,
260
- noCache: undefined,
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 toV3Content(parts) {
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
- for (const message of input.messages) {
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',