@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.
@@ -133,25 +133,17 @@ async function collectStreamParts(stream) {
133
133
  });
134
134
  };
135
135
  const model = new devstral_language_model_js_1.DevstralLanguageModel({ apiKey: 'test-api-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
136
- (0, vitest_1.expect)(model.specificationVersion).toBe('v3');
136
+ (0, vitest_1.expect)(model.specificationVersion).toBe('v2');
137
137
  (0, vitest_1.expect)(model.supportedUrls).toEqual({});
138
138
  const result = await model.doGenerate({
139
139
  prompt: [{ role: 'user', content: [{ type: 'text', text: 'Find auth logic.' }] }],
140
140
  });
141
141
  (0, vitest_1.expect)(result.content).toEqual([{ type: 'text', text: 'generated answer' }]);
142
- (0, vitest_1.expect)(result.finishReason).toEqual({ unified: 'stop', raw: undefined });
142
+ (0, vitest_1.expect)(result.finishReason).toBe('stop');
143
143
  (0, vitest_1.expect)(result.usage).toEqual({
144
- inputTokens: {
145
- total: undefined,
146
- noCache: undefined,
147
- cacheRead: undefined,
148
- cacheWrite: undefined,
149
- },
150
- outputTokens: {
151
- total: undefined,
152
- text: undefined,
153
- reasoning: undefined,
154
- },
144
+ inputTokens: undefined,
145
+ outputTokens: undefined,
146
+ totalTokens: undefined,
155
147
  });
156
148
  (0, vitest_1.expect)(calls).toHaveLength(2);
157
149
  (0, vitest_1.expect)(calls[0]?.url).toBe('https://windsurf.test/exa.auth_pb.AuthService/GetUserJwt');
@@ -171,7 +163,7 @@ async function collectStreamParts(stream) {
171
163
  return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
172
164
  }
173
165
  requestBodies.push(bufferFromBody(init?.body));
174
- const payload = Buffer.from('[TOOL_CALLS]searchRepo[ARGS]{"query":"jwt manager"}', 'utf8');
166
+ const payload = Buffer.from('[{"type":"function","function":{"name":"searchRepo","parameters":{"query":"jwt manager"}}}]', 'utf8');
175
167
  return new Response(Uint8Array.from((0, connect_frame_js_1.connectFrameEncode)(payload)), { status: 200 });
176
168
  };
177
169
  const model = new devstral_language_model_js_1.DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
@@ -200,7 +192,7 @@ async function collectStreamParts(stream) {
200
192
  input: '{"query":"jwt manager"}',
201
193
  },
202
194
  ]);
203
- (0, vitest_1.expect)(result.finishReason).toEqual({ unified: 'tool-calls', raw: undefined });
195
+ (0, vitest_1.expect)(result.finishReason).toBe('tool-calls');
204
196
  const strings = (0, protobuf_js_1.extractStrings)(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
205
197
  const toolsPayload = extractToolsPayload(strings);
206
198
  (0, vitest_1.expect)(toolsPayload).toBeDefined();
@@ -250,7 +242,7 @@ async function collectStreamParts(stream) {
250
242
  },
251
243
  },
252
244
  {
253
- type: 'provider',
245
+ type: 'provider-defined',
254
246
  id: 'windsurf.restricted_exec',
255
247
  name: 'restricted_exec',
256
248
  args: { mode: 'read-only' },
@@ -283,6 +275,83 @@ async function collectStreamParts(stream) {
283
275
  const strings = (0, protobuf_js_1.extractStrings)(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
284
276
  (0, vitest_1.expect)(extractToolsPayload(strings)).toBeUndefined();
285
277
  });
278
+ (0, vitest_1.it)('injects tool format instruction into request when tools are present', async () => {
279
+ const requestBodies = [];
280
+ const jwt = makeJwt(4_200_000_100, 'instruction');
281
+ const fakeFetch = async (input, init) => {
282
+ const url = String(input);
283
+ if (url.endsWith('/GetUserJwt')) {
284
+ return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
285
+ }
286
+ requestBodies.push(bufferFromBody(init?.body));
287
+ return new Response(Uint8Array.from((0, connect_frame_js_1.connectFrameEncode)(Buffer.from('ok', 'utf8'))), { status: 200 });
288
+ };
289
+ const model = new devstral_language_model_js_1.DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
290
+ await model.doGenerate({
291
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'Search for auth.' }] }],
292
+ tools: [
293
+ {
294
+ type: 'function',
295
+ name: 'searchRepo',
296
+ description: 'Search repository files',
297
+ inputSchema: {
298
+ type: 'object',
299
+ properties: { query: { type: 'string' } },
300
+ required: ['query'],
301
+ },
302
+ },
303
+ ],
304
+ });
305
+ const strings = (0, protobuf_js_1.extractStrings)(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
306
+ const combined = strings.join('\n');
307
+ (0, vitest_1.expect)(combined).toContain('When you need to call tools');
308
+ });
309
+ (0, vitest_1.it)('does not inject tool format instruction when no tools provided', async () => {
310
+ const requestBodies = [];
311
+ const jwt = makeJwt(4_200_000_101, 'no-instruction');
312
+ const fakeFetch = async (input, init) => {
313
+ const url = String(input);
314
+ if (url.endsWith('/GetUserJwt')) {
315
+ return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
316
+ }
317
+ requestBodies.push(bufferFromBody(init?.body));
318
+ return new Response(Uint8Array.from((0, connect_frame_js_1.connectFrameEncode)(Buffer.from('ok', 'utf8'))), { status: 200 });
319
+ };
320
+ const model = new devstral_language_model_js_1.DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
321
+ await model.doGenerate({
322
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'Find auth logic.' }] }],
323
+ });
324
+ const strings = (0, protobuf_js_1.extractStrings)(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
325
+ const combined = strings.join('\n');
326
+ (0, vitest_1.expect)(combined).not.toContain('When you need to call tools');
327
+ });
328
+ (0, vitest_1.it)('does not inject tool format instruction when only provider-defined tools', async () => {
329
+ const requestBodies = [];
330
+ const jwt = makeJwt(4_200_000_102, 'provider-only');
331
+ const fakeFetch = async (input, init) => {
332
+ const url = String(input);
333
+ if (url.endsWith('/GetUserJwt')) {
334
+ return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
335
+ }
336
+ requestBodies.push(bufferFromBody(init?.body));
337
+ return new Response(Uint8Array.from((0, connect_frame_js_1.connectFrameEncode)(Buffer.from('ok', 'utf8'))), { status: 200 });
338
+ };
339
+ const model = new devstral_language_model_js_1.DevstralLanguageModel({ apiKey: 'tools-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
340
+ await model.doGenerate({
341
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'Use provider tools.' }] }],
342
+ tools: [
343
+ {
344
+ type: 'provider-defined',
345
+ id: 'some-provider-tool',
346
+ name: 'providerTool',
347
+ args: {},
348
+ },
349
+ ],
350
+ });
351
+ const strings = (0, protobuf_js_1.extractStrings)(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
352
+ const combined = strings.join('\n');
353
+ (0, vitest_1.expect)(combined).not.toContain('When you need to call tools');
354
+ });
286
355
  });
287
356
  (0, vitest_1.describe)('DevstralLanguageModel doStream', () => {
288
357
  (0, vitest_1.it)('stream-text resolves before full body arrives and emits parts incrementally', async () => {
@@ -370,22 +439,11 @@ async function collectStreamParts(stream) {
370
439
  (0, vitest_1.expect)(parts[4]).toMatchObject({ type: 'text-delta', delta: 'world' });
371
440
  (0, vitest_1.expect)(parts[6]).toEqual({
372
441
  type: 'finish',
373
- finishReason: {
374
- unified: 'stop',
375
- raw: undefined,
376
- },
442
+ finishReason: 'stop',
377
443
  usage: {
378
- inputTokens: {
379
- total: undefined,
380
- noCache: undefined,
381
- cacheRead: undefined,
382
- cacheWrite: undefined,
383
- },
384
- outputTokens: {
385
- total: undefined,
386
- text: undefined,
387
- reasoning: undefined,
388
- },
444
+ inputTokens: undefined,
445
+ outputTokens: undefined,
446
+ totalTokens: undefined,
389
447
  },
390
448
  });
391
449
  }
@@ -398,7 +456,7 @@ async function collectStreamParts(stream) {
398
456
  });
399
457
  (0, vitest_1.it)('stream-tool emits tool-input deltas before final tool-call and finish', async () => {
400
458
  const jwt = makeJwt(4_300_000_001, 'stream-tool');
401
- const toolPayload = Buffer.from('[TOOL_CALLS]searchRepo[ARGS]{"query":"jwt manager"}', 'utf8');
459
+ const toolPayload = Buffer.from('[{"type":"function","function":{"name":"searchRepo","parameters":{"query":"jwt manager"}}}]', 'utf8');
402
460
  const frames = Buffer.concat([(0, connect_frame_js_1.connectFrameEncode)(toolPayload)]);
403
461
  const fakeFetch = async (input) => {
404
462
  const url = String(input);
@@ -448,22 +506,11 @@ async function collectStreamParts(stream) {
448
506
  });
449
507
  (0, vitest_1.expect)(parts[6]).toEqual({
450
508
  type: 'finish',
451
- finishReason: {
452
- unified: 'tool-calls',
453
- raw: undefined,
454
- },
509
+ finishReason: 'tool-calls',
455
510
  usage: {
456
- inputTokens: {
457
- total: undefined,
458
- noCache: undefined,
459
- cacheRead: undefined,
460
- cacheWrite: undefined,
461
- },
462
- outputTokens: {
463
- total: undefined,
464
- text: undefined,
465
- reasoning: undefined,
466
- },
511
+ inputTokens: undefined,
512
+ outputTokens: undefined,
513
+ totalTokens: undefined,
467
514
  },
468
515
  });
469
516
  });
@@ -1,3 +1,3 @@
1
- import type { LanguageModelV3Prompt } from '@ai-sdk/provider';
1
+ import type { LanguageModelV2Prompt } from '@ai-sdk/provider';
2
2
  import type { DevstralMessage } from '../types/index.js';
3
- export declare function convertPrompt(prompt: LanguageModelV3Prompt): DevstralMessage[];
3
+ export declare function convertPrompt(prompt: LanguageModelV2Prompt): DevstralMessage[];
@@ -8,5 +8,5 @@ export interface ToolCallPart {
8
8
  toolName: string;
9
9
  input: unknown;
10
10
  }
11
- export type LanguageModelV3Content = TextPart | ToolCallPart;
12
- export declare function convertResponse(buffer: Buffer): LanguageModelV3Content[];
11
+ export type LanguageModelV2Content = TextPart | ToolCallPart;
12
+ export declare function convertResponse(buffer: Buffer): LanguageModelV2Content[];
@@ -34,7 +34,7 @@ describe('convertPrompt', () => {
34
34
  },
35
35
  });
36
36
  });
37
- it('multi-turn preserves ordering across mixed roles (V3 output shape)', () => {
37
+ it('multi-turn preserves ordering across mixed roles (V2 output shape)', () => {
38
38
  const prompt = [
39
39
  { role: 'system', content: 'System instruction' },
40
40
  {
@@ -159,7 +159,7 @@ describe('convertPrompt', () => {
159
159
  },
160
160
  ]);
161
161
  });
162
- it('tool-result with execution-denied output includes reason', () => {
162
+ it('tool-result with error-text output includes denial reason', () => {
163
163
  const prompt = [
164
164
  {
165
165
  role: 'tool',
@@ -168,7 +168,7 @@ describe('convertPrompt', () => {
168
168
  type: 'tool-result',
169
169
  toolCallId: 'call_denied',
170
170
  toolName: 'dangerousAction',
171
- output: { type: 'execution-denied', reason: 'User rejected tool execution' },
171
+ output: { type: 'error-text', value: 'User rejected tool execution' },
172
172
  },
173
173
  ],
174
174
  },
@@ -177,12 +177,12 @@ describe('convertPrompt', () => {
177
177
  expect(result).toEqual([
178
178
  {
179
179
  role: 4,
180
- content: '{"type":"execution-denied","reason":"User rejected tool execution"}',
180
+ content: 'User rejected tool execution',
181
181
  metadata: { refCallId: 'call_denied' },
182
182
  },
183
183
  ]);
184
184
  });
185
- it('tool-result with execution-denied output handles missing reason', () => {
185
+ it('tool-result with error-text output handles fallback denial text', () => {
186
186
  const prompt = [
187
187
  {
188
188
  role: 'tool',
@@ -191,7 +191,7 @@ describe('convertPrompt', () => {
191
191
  type: 'tool-result',
192
192
  toolCallId: 'call_denied_no_reason',
193
193
  toolName: 'someTool',
194
- output: { type: 'execution-denied' },
194
+ output: { type: 'error-text', value: 'Tool execution denied' },
195
195
  },
196
196
  ],
197
197
  },
@@ -200,7 +200,7 @@ describe('convertPrompt', () => {
200
200
  expect(result).toEqual([
201
201
  {
202
202
  role: 4,
203
- content: '{"type":"execution-denied"}',
203
+ content: 'Tool execution denied',
204
204
  metadata: { refCallId: 'call_denied_no_reason' },
205
205
  },
206
206
  ]);
@@ -1,191 +1,37 @@
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]';
5
3
  const STOP_TOKEN = '</s>';
6
- function parseOpenAIToolCalls(responseText) {
7
- if (!responseText.startsWith('TOOL_CALLS')) {
4
+ function parseStrictOpenAIToolCalls(responseText) {
5
+ const trimmed = responseText.trim();
6
+ if (!trimmed.startsWith('['))
8
7
  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;
9
25
  }
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) {
26
+ catch {
41
27
  return null;
42
28
  }
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
- }
66
29
  }
67
30
  function pushText(parts, text) {
68
31
  if (text.length > 0) {
69
32
  parts.push({ type: 'text', text });
70
33
  }
71
34
  }
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
- }
189
35
  function hasControlChars(value) {
190
36
  for (const char of value) {
191
37
  const code = char.charCodeAt(0);
@@ -213,10 +59,6 @@ function isLikelyMetadata(value) {
213
59
  return /^[A-Za-z0-9._:-]{1,32}$/.test(value);
214
60
  }
215
61
  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
- }
220
62
  const nonMetadata = values.filter((value) => !isLikelyMetadata(value));
221
63
  const candidates = nonMetadata.length > 0 ? nonMetadata : values;
222
64
  return candidates.reduce((best, current) => (current.length > best.length ? current : best), candidates[0] ?? '');
@@ -236,43 +78,10 @@ function decodeResponseText(buffer) {
236
78
  export function convertResponse(buffer) {
237
79
  let responseText = decodeResponseText(buffer);
238
80
  responseText = responseText.replace(STOP_TOKEN, '');
239
- const openaiToolCalls = parseOpenAIToolCalls(responseText);
240
- if (openaiToolCalls) {
241
- return openaiToolCalls;
242
- }
81
+ const strictToolCalls = parseStrictOpenAIToolCalls(responseText);
82
+ if (strictToolCalls)
83
+ return strictToolCalls;
243
84
  const parts = [];
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
- }
85
+ pushText(parts, responseText);
277
86
  return parts;
278
87
  }