@bxb1337/windsurf-fast-context 1.0.8 → 1.1.0

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
  ]);
@@ -21,7 +21,7 @@ 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
23
  class DevstralLanguageModel {
24
- specificationVersion = 'v3';
24
+ specificationVersion = 'v2';
25
25
  provider = 'windsurf';
26
26
  modelId;
27
27
  supportedUrls = {};
@@ -56,18 +56,15 @@ class DevstralLanguageModel {
56
56
  const requestFrame = (0, connect_frame_js_1.connectFrameEncode)(requestPayload);
57
57
  const headers = createConnectHeaders(this.headers);
58
58
  const responseFrame = await this.transport.postUnary(`${this.baseURL}${API_SERVICE_PATH}${DEVSTRAL_STREAM_PATH}`, requestFrame, headers);
59
- const responsePayloads = (0, connect_frame_js_1.connectFrameDecode)(responseFrame);
59
+ const { payloads: responsePayloads } = (0, connect_frame_js_1.connectFrameDecode)(responseFrame);
60
60
  const payloads = responsePayloads.length > 0 ? responsePayloads : [responseFrame];
61
- const content = payloads.flatMap((payload) => toV3Content((0, response_converter_js_1.convertResponse)(payload)));
61
+ const content = payloads.flatMap((payload) => toV2Content((0, response_converter_js_1.convertResponse)(payload)));
62
62
  const unified = content.some((part) => part.type === 'tool-call')
63
63
  ? 'tool-calls'
64
64
  : 'stop';
65
65
  return {
66
66
  content,
67
- finishReason: {
68
- unified,
69
- raw: undefined,
70
- },
67
+ finishReason: unified,
71
68
  usage: emptyUsage(),
72
69
  warnings: [],
73
70
  };
@@ -110,7 +107,7 @@ class DevstralLanguageModel {
110
107
  try {
111
108
  safeEnqueue(controller, { type: 'stream-start', warnings: [] });
112
109
  safeEnqueue(controller, { type: 'response-metadata', modelId: this.modelId });
113
- while (!isAborted(options.abortSignal)) {
110
+ outerLoop: while (!isAborted(options.abortSignal)) {
114
111
  const next = await reader.read();
115
112
  if (next.done) {
116
113
  break;
@@ -125,7 +122,7 @@ class DevstralLanguageModel {
125
122
  break;
126
123
  }
127
124
  pending = frameResult.rest;
128
- const contentParts = toV3Content((0, response_converter_js_1.convertResponse)(frameResult.payload));
125
+ const contentParts = toV2Content((0, response_converter_js_1.convertResponse)(frameResult.payload));
129
126
  for (const part of contentParts) {
130
127
  if (isAborted(options.abortSignal)) {
131
128
  safeClose(controller);
@@ -162,6 +159,9 @@ class DevstralLanguageModel {
162
159
  });
163
160
  safeEnqueue(controller, part);
164
161
  }
162
+ if (frameResult.isEndStream) {
163
+ break outerLoop;
164
+ }
165
165
  }
166
166
  }
167
167
  if (isAborted(options.abortSignal)) {
@@ -172,10 +172,7 @@ class DevstralLanguageModel {
172
172
  const unified = hasToolCalls ? 'tool-calls' : 'stop';
173
173
  safeEnqueue(controller, {
174
174
  type: 'finish',
175
- finishReason: {
176
- unified,
177
- raw: undefined,
178
- },
175
+ finishReason: unified,
179
176
  usage: emptyUsage(),
180
177
  });
181
178
  safeClose(controller);
@@ -189,10 +186,7 @@ class DevstralLanguageModel {
189
186
  });
190
187
  safeEnqueue(controller, {
191
188
  type: 'finish',
192
- finishReason: {
193
- unified: 'error',
194
- raw: undefined,
195
- },
189
+ finishReason: 'error',
196
190
  usage: emptyUsage(),
197
191
  });
198
192
  }
@@ -219,10 +213,11 @@ function readNextConnectFrame(buffer) {
219
213
  return null;
220
214
  }
221
215
  const frame = buffer.subarray(0, frameLength);
222
- const decoded = (0, connect_frame_js_1.connectFrameDecode)(frame);
216
+ const { payloads, isEndStream } = (0, connect_frame_js_1.connectFrameDecode)(frame);
223
217
  return {
224
- payload: decoded[0] ?? Buffer.alloc(0),
218
+ payload: payloads[0] ?? Buffer.alloc(0),
225
219
  rest: buffer.subarray(frameLength),
220
+ isEndStream,
226
221
  };
227
222
  }
228
223
  function safeEnqueue(controller, part) {
@@ -251,20 +246,12 @@ function isAborted(signal) {
251
246
  }
252
247
  function emptyUsage() {
253
248
  return {
254
- inputTokens: {
255
- total: undefined,
256
- noCache: undefined,
257
- cacheRead: undefined,
258
- cacheWrite: undefined,
259
- },
260
- outputTokens: {
261
- total: undefined,
262
- text: undefined,
263
- reasoning: undefined,
264
- },
249
+ inputTokens: undefined,
250
+ outputTokens: undefined,
251
+ totalTokens: undefined,
265
252
  };
266
253
  }
267
- function toV3Content(parts) {
254
+ function toV2Content(parts) {
268
255
  return parts.map((part) => {
269
256
  if (part.type !== 'tool-call') {
270
257
  return part;
@@ -66,8 +66,8 @@ function waitFor(ms) {
66
66
  }
67
67
  function decodeRequestPayload(body) {
68
68
  // Body is now a connect frame directly (gzip is inside the frame, not outside)
69
- const decodedFrames = (0, connect_frame_js_1.connectFrameDecode)(body);
70
- return decodedFrames[0] ?? Buffer.alloc(0);
69
+ const { payloads } = (0, connect_frame_js_1.connectFrameDecode)(body);
70
+ return payloads[0] ?? Buffer.alloc(0);
71
71
  }
72
72
  function extractToolsPayload(strings) {
73
73
  return strings.find((value) => value.startsWith('[{"type":"function"'));
@@ -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');
@@ -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' },
@@ -370,22 +362,11 @@ async function collectStreamParts(stream) {
370
362
  (0, vitest_1.expect)(parts[4]).toMatchObject({ type: 'text-delta', delta: 'world' });
371
363
  (0, vitest_1.expect)(parts[6]).toEqual({
372
364
  type: 'finish',
373
- finishReason: {
374
- unified: 'stop',
375
- raw: undefined,
376
- },
365
+ finishReason: 'stop',
377
366
  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
- },
367
+ inputTokens: undefined,
368
+ outputTokens: undefined,
369
+ totalTokens: undefined,
389
370
  },
390
371
  });
391
372
  }
@@ -448,25 +429,84 @@ async function collectStreamParts(stream) {
448
429
  });
449
430
  (0, vitest_1.expect)(parts[6]).toEqual({
450
431
  type: 'finish',
451
- finishReason: {
452
- unified: 'tool-calls',
453
- raw: undefined,
454
- },
432
+ finishReason: 'tool-calls',
455
433
  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
- },
434
+ inputTokens: undefined,
435
+ outputTokens: undefined,
436
+ totalTokens: undefined,
467
437
  },
468
438
  });
469
439
  });
440
+ (0, vitest_1.it)('stream terminates immediately on EndStreamResponse frame (flags=2)', async () => {
441
+ const jwt = makeJwt(4_300_000_003, 'end-stream-response');
442
+ const textFrame = (0, connect_frame_js_1.connectFrameEncode)(Buffer.from('hello world', 'utf8'));
443
+ const endStreamPayload = Buffer.alloc(0);
444
+ const endStreamFrame = Buffer.allocUnsafe(5 + endStreamPayload.length);
445
+ endStreamFrame.writeUInt8(2, 0);
446
+ endStreamFrame.writeUInt32BE(endStreamPayload.length, 1);
447
+ endStreamPayload.copy(endStreamFrame, 5);
448
+ let releaseHttpClose;
449
+ const waitForHttpClose = new Promise((resolve) => {
450
+ releaseHttpClose = resolve;
451
+ });
452
+ const fakeFetch = async (input) => {
453
+ const url = String(input);
454
+ if (url.endsWith('/GetUserJwt')) {
455
+ return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
456
+ }
457
+ let sentTextFrame = false;
458
+ let sentEndStreamFrame = false;
459
+ return new Response(new ReadableStream({
460
+ async pull(controller) {
461
+ if (!sentTextFrame) {
462
+ controller.enqueue(Uint8Array.from(textFrame));
463
+ sentTextFrame = true;
464
+ return;
465
+ }
466
+ if (!sentEndStreamFrame) {
467
+ controller.enqueue(Uint8Array.from(endStreamFrame));
468
+ sentEndStreamFrame = true;
469
+ return;
470
+ }
471
+ await waitForHttpClose;
472
+ controller.close();
473
+ },
474
+ }), { status: 200 });
475
+ };
476
+ const model = new devstral_language_model_js_1.DevstralLanguageModel({
477
+ apiKey: 'end-stream-key',
478
+ fetch: fakeFetch,
479
+ baseURL: 'https://windsurf.test',
480
+ });
481
+ const result = await model.doStream({
482
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'Stream with end.' }] }],
483
+ });
484
+ const reader = result.stream.getReader();
485
+ const parts = [];
486
+ const resultType = await Promise.race([
487
+ (async () => {
488
+ while (true) {
489
+ const next = await reader.read();
490
+ if (next.done) {
491
+ break;
492
+ }
493
+ parts.push(next.value);
494
+ }
495
+ return 'completed';
496
+ })(),
497
+ waitFor(100).then(() => 'timed-out'),
498
+ ]);
499
+ (0, vitest_1.expect)(resultType).toBe('completed');
500
+ (0, vitest_1.expect)(parts.map((p) => p.type)).toEqual([
501
+ 'stream-start',
502
+ 'response-metadata',
503
+ 'text-start',
504
+ 'text-delta',
505
+ 'text-end',
506
+ 'finish',
507
+ ]);
508
+ releaseHttpClose?.();
509
+ });
470
510
  (0, vitest_1.it)('abort stops stream mid-response', async () => {
471
511
  const controller = new AbortController();
472
512
  const jwt = makeJwt(4_300_000_002, 'abort');
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FLAG_END_STREAM = exports.FLAG_COMPRESSED = void 0;
3
4
  exports.connectFrameEncode = connectFrameEncode;
4
5
  exports.connectFrameDecode = connectFrameDecode;
5
6
  const node_zlib_1 = require("node:zlib");
6
7
  const FRAME_HEADER_SIZE = 5;
7
- const FLAG_COMPRESSED = 1;
8
+ exports.FLAG_COMPRESSED = 1;
9
+ exports.FLAG_END_STREAM = 2;
8
10
  /**
9
11
  * Encode a protobuf payload into a Connect-RPC frame.
10
12
  *
@@ -16,7 +18,7 @@ const FLAG_COMPRESSED = 1;
16
18
  function connectFrameEncode(payload, compress = true) {
17
19
  const payloadBytes = compress ? (0, node_zlib_1.gzipSync)(payload) : payload;
18
20
  const frame = Buffer.allocUnsafe(FRAME_HEADER_SIZE + payloadBytes.length);
19
- frame.writeUInt8(compress ? FLAG_COMPRESSED : 0, 0);
21
+ frame.writeUInt8(compress ? exports.FLAG_COMPRESSED : 0, 0);
20
22
  frame.writeUInt32BE(payloadBytes.length, 1);
21
23
  payloadBytes.copy(frame, FRAME_HEADER_SIZE);
22
24
  return frame;
@@ -24,6 +26,7 @@ function connectFrameEncode(payload, compress = true) {
24
26
  function connectFrameDecode(buffer) {
25
27
  const decoded = [];
26
28
  let offset = 0;
29
+ let isEndStream = false;
27
30
  while (offset + FRAME_HEADER_SIZE <= buffer.length) {
28
31
  const flags = buffer.readUInt8(offset);
29
32
  const payloadLength = buffer.readUInt32BE(offset + 1);
@@ -33,8 +36,11 @@ function connectFrameDecode(buffer) {
33
36
  break;
34
37
  }
35
38
  const payload = buffer.subarray(payloadStart, payloadEnd);
36
- decoded.push(flags === FLAG_COMPRESSED ? (0, node_zlib_1.gunzipSync)(payload) : Buffer.from(payload));
39
+ decoded.push((flags & exports.FLAG_COMPRESSED) !== 0 ? (0, node_zlib_1.gunzipSync)(payload) : Buffer.from(payload));
40
+ if ((flags & exports.FLAG_END_STREAM) !== 0) {
41
+ isEndStream = true;
42
+ }
37
43
  offset = payloadEnd;
38
44
  }
39
- return decoded;
45
+ return { payloads: decoded, isEndStream };
40
46
  }
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const node_zlib_1 = require("node:zlib");
3
4
  const node_fs_1 = require("node:fs");
4
5
  const node_path_1 = require("node:path");
5
6
  const vitest_1 = require("vitest");
@@ -15,22 +16,94 @@ const payloadWorld = Buffer.from('CONNECT\x02world', 'utf8');
15
16
  (0, vitest_1.expect)(encoded.readUInt8(0)).toBe(0);
16
17
  (0, vitest_1.expect)(encoded.readUInt32BE(1)).toBe(payloadHello.length);
17
18
  (0, vitest_1.expect)(encoded.subarray(5)).toEqual(payloadHello);
18
- (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(encoded)).toEqual([payloadHello]);
19
+ (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(encoded)).toEqual({ payloads: [payloadHello], isEndStream: false });
19
20
  });
20
21
  (0, vitest_1.it)('gzip roundtrip uses compressed frame and decodes back to source payload', () => {
21
22
  const encoded = (0, connect_frame_js_1.connectFrameEncode)(payloadWorld, true);
22
23
  (0, vitest_1.expect)(encoded.readUInt8(0)).toBe(1);
23
- (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(encoded)).toEqual([payloadWorld]);
24
+ (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(encoded)).toEqual({ payloads: [payloadWorld], isEndStream: false });
24
25
  });
25
26
  });
26
27
  (0, vitest_1.describe)('connectFrameDecode fixtures', () => {
27
28
  (0, vitest_1.it)('decodes frame-simple fixture payload', () => {
28
- (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(fixtureSimpleFrame)).toEqual([payloadHello]);
29
+ (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(fixtureSimpleFrame)).toEqual({ payloads: [payloadHello], isEndStream: false });
29
30
  });
30
31
  (0, vitest_1.it)('decodes gzip fixture payload', () => {
31
- (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(fixtureGzipFrame)).toEqual([payloadWorld]);
32
+ (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(fixtureGzipFrame)).toEqual({ payloads: [payloadWorld], isEndStream: false });
32
33
  });
33
34
  (0, vitest_1.it)('decodes multiple concatenated frames in order', () => {
34
- (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(fixtureMultiFrame)).toEqual([payloadHello, payloadHello]);
35
+ (0, vitest_1.expect)((0, connect_frame_js_1.connectFrameDecode)(fixtureMultiFrame)).toEqual({ payloads: [payloadHello, payloadHello], isEndStream: false });
36
+ });
37
+ });
38
+ (0, vitest_1.describe)('connect frame flags', () => {
39
+ (0, vitest_1.it)('exports FLAG_COMPRESSED constant with value 1', () => {
40
+ (0, vitest_1.expect)(connect_frame_js_1.FLAG_COMPRESSED).toBe(1);
41
+ });
42
+ (0, vitest_1.it)('exports FLAG_END_STREAM constant with value 2', () => {
43
+ (0, vitest_1.expect)(connect_frame_js_1.FLAG_END_STREAM).toBe(2);
44
+ });
45
+ (0, vitest_1.it)('supports bit combination: FLAG_COMPRESSED | FLAG_END_STREAM === 3', () => {
46
+ (0, vitest_1.expect)(connect_frame_js_1.FLAG_COMPRESSED | connect_frame_js_1.FLAG_END_STREAM).toBe(3);
47
+ });
48
+ });
49
+ (0, vitest_1.describe)('connectFrameDecode isEndStream', () => {
50
+ /**
51
+ * TDD RED PHASE: Tests for isEndStream flag detection.
52
+ * These tests MUST FAIL because connectFrameDecode currently returns Buffer[],
53
+ * not { payloads: Buffer[], isEndStream: boolean }.
54
+ *
55
+ * Connect-RPC flag bits:
56
+ * - Bit 0 (value 1): FLAG_COMPRESSED
57
+ * - Bit 1 (value 2): FLAG_END_STREAM
58
+ */
59
+ (0, vitest_1.it)('returns isEndStream: false for flags=0 (no flags)', () => {
60
+ // flags=0: neither compressed nor end stream
61
+ const frame = Buffer.allocUnsafe(5 + payloadHello.length);
62
+ frame.writeUInt8(0, 0); // flags = 0
63
+ frame.writeUInt32BE(payloadHello.length, 1);
64
+ payloadHello.copy(frame, 5);
65
+ const result = (0, connect_frame_js_1.connectFrameDecode)(frame);
66
+ (0, vitest_1.expect)(result).toHaveProperty('payloads');
67
+ (0, vitest_1.expect)(result).toHaveProperty('isEndStream');
68
+ (0, vitest_1.expect)(result.payloads).toEqual([payloadHello]);
69
+ (0, vitest_1.expect)(result.isEndStream).toBe(false);
70
+ });
71
+ (0, vitest_1.it)('returns isEndStream: false for flags=1 (compressed only)', () => {
72
+ // flags=1: compressed but not end stream
73
+ const gzipped = (0, node_zlib_1.gzipSync)(payloadHello);
74
+ const frame = Buffer.allocUnsafe(5 + gzipped.length);
75
+ frame.writeUInt8(connect_frame_js_1.FLAG_COMPRESSED, 0); // flags = 1
76
+ frame.writeUInt32BE(gzipped.length, 1);
77
+ gzipped.copy(frame, 5);
78
+ const result = (0, connect_frame_js_1.connectFrameDecode)(frame);
79
+ (0, vitest_1.expect)(result).toHaveProperty('payloads');
80
+ (0, vitest_1.expect)(result).toHaveProperty('isEndStream');
81
+ (0, vitest_1.expect)(result.payloads).toEqual([payloadHello]);
82
+ (0, vitest_1.expect)(result.isEndStream).toBe(false);
83
+ });
84
+ (0, vitest_1.it)('returns isEndStream: true for flags=2 (end stream only)', () => {
85
+ // flags=2: end stream but not compressed
86
+ const frame = Buffer.allocUnsafe(5 + payloadHello.length);
87
+ frame.writeUInt8(connect_frame_js_1.FLAG_END_STREAM, 0); // flags = 2
88
+ frame.writeUInt32BE(payloadHello.length, 1);
89
+ payloadHello.copy(frame, 5);
90
+ const result = (0, connect_frame_js_1.connectFrameDecode)(frame);
91
+ (0, vitest_1.expect)(result).toHaveProperty('payloads');
92
+ (0, vitest_1.expect)(result).toHaveProperty('isEndStream');
93
+ (0, vitest_1.expect)(result.payloads).toEqual([payloadHello]);
94
+ (0, vitest_1.expect)(result.isEndStream).toBe(true);
95
+ });
96
+ (0, vitest_1.it)('returns isEndStream: true for flags=3 (compressed + end stream)', () => {
97
+ // flags=3: both compressed and end stream
98
+ const gzipped = (0, node_zlib_1.gzipSync)(payloadHello);
99
+ const frame = Buffer.allocUnsafe(5 + gzipped.length);
100
+ frame.writeUInt8(connect_frame_js_1.FLAG_COMPRESSED | connect_frame_js_1.FLAG_END_STREAM, 0); // flags = 3
101
+ frame.writeUInt32BE(gzipped.length, 1);
102
+ gzipped.copy(frame, 5);
103
+ const result = (0, connect_frame_js_1.connectFrameDecode)(frame);
104
+ (0, vitest_1.expect)(result).toHaveProperty('payloads');
105
+ (0, vitest_1.expect)(result).toHaveProperty('isEndStream');
106
+ (0, vitest_1.expect)(result.payloads).toEqual([payloadHello]);
107
+ (0, vitest_1.expect)(result.isEndStream).toBe(true);
35
108
  });
36
109
  });
@@ -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
  ]);
@@ -18,7 +18,7 @@ const WS_LS_VER = process.env.WS_LS_VER ?? '1.9544.35';
18
18
  const SENTRY_PUBLIC_KEY = 'b813f73488da69eedec534dba1029111';
19
19
  const CONNECT_USER_AGENT = 'connect-go/1.18.1 (go1.25.5)';
20
20
  export class DevstralLanguageModel {
21
- specificationVersion = 'v3';
21
+ specificationVersion = 'v2';
22
22
  provider = 'windsurf';
23
23
  modelId;
24
24
  supportedUrls = {};
@@ -53,18 +53,15 @@ export class DevstralLanguageModel {
53
53
  const requestFrame = connectFrameEncode(requestPayload);
54
54
  const headers = createConnectHeaders(this.headers);
55
55
  const responseFrame = await this.transport.postUnary(`${this.baseURL}${API_SERVICE_PATH}${DEVSTRAL_STREAM_PATH}`, requestFrame, headers);
56
- const responsePayloads = connectFrameDecode(responseFrame);
56
+ const { payloads: responsePayloads } = connectFrameDecode(responseFrame);
57
57
  const payloads = responsePayloads.length > 0 ? responsePayloads : [responseFrame];
58
- const content = payloads.flatMap((payload) => toV3Content(convertResponse(payload)));
58
+ const content = payloads.flatMap((payload) => toV2Content(convertResponse(payload)));
59
59
  const unified = content.some((part) => part.type === 'tool-call')
60
60
  ? 'tool-calls'
61
61
  : 'stop';
62
62
  return {
63
63
  content,
64
- finishReason: {
65
- unified,
66
- raw: undefined,
67
- },
64
+ finishReason: unified,
68
65
  usage: emptyUsage(),
69
66
  warnings: [],
70
67
  };
@@ -107,7 +104,7 @@ export class DevstralLanguageModel {
107
104
  try {
108
105
  safeEnqueue(controller, { type: 'stream-start', warnings: [] });
109
106
  safeEnqueue(controller, { type: 'response-metadata', modelId: this.modelId });
110
- while (!isAborted(options.abortSignal)) {
107
+ outerLoop: while (!isAborted(options.abortSignal)) {
111
108
  const next = await reader.read();
112
109
  if (next.done) {
113
110
  break;
@@ -122,7 +119,7 @@ export class DevstralLanguageModel {
122
119
  break;
123
120
  }
124
121
  pending = frameResult.rest;
125
- const contentParts = toV3Content(convertResponse(frameResult.payload));
122
+ const contentParts = toV2Content(convertResponse(frameResult.payload));
126
123
  for (const part of contentParts) {
127
124
  if (isAborted(options.abortSignal)) {
128
125
  safeClose(controller);
@@ -159,6 +156,9 @@ export class DevstralLanguageModel {
159
156
  });
160
157
  safeEnqueue(controller, part);
161
158
  }
159
+ if (frameResult.isEndStream) {
160
+ break outerLoop;
161
+ }
162
162
  }
163
163
  }
164
164
  if (isAborted(options.abortSignal)) {
@@ -169,10 +169,7 @@ export class DevstralLanguageModel {
169
169
  const unified = hasToolCalls ? 'tool-calls' : 'stop';
170
170
  safeEnqueue(controller, {
171
171
  type: 'finish',
172
- finishReason: {
173
- unified,
174
- raw: undefined,
175
- },
172
+ finishReason: unified,
176
173
  usage: emptyUsage(),
177
174
  });
178
175
  safeClose(controller);
@@ -186,10 +183,7 @@ export class DevstralLanguageModel {
186
183
  });
187
184
  safeEnqueue(controller, {
188
185
  type: 'finish',
189
- finishReason: {
190
- unified: 'error',
191
- raw: undefined,
192
- },
186
+ finishReason: 'error',
193
187
  usage: emptyUsage(),
194
188
  });
195
189
  }
@@ -215,10 +209,11 @@ function readNextConnectFrame(buffer) {
215
209
  return null;
216
210
  }
217
211
  const frame = buffer.subarray(0, frameLength);
218
- const decoded = connectFrameDecode(frame);
212
+ const { payloads, isEndStream } = connectFrameDecode(frame);
219
213
  return {
220
- payload: decoded[0] ?? Buffer.alloc(0),
214
+ payload: payloads[0] ?? Buffer.alloc(0),
221
215
  rest: buffer.subarray(frameLength),
216
+ isEndStream,
222
217
  };
223
218
  }
224
219
  function safeEnqueue(controller, part) {
@@ -247,20 +242,12 @@ function isAborted(signal) {
247
242
  }
248
243
  function emptyUsage() {
249
244
  return {
250
- inputTokens: {
251
- total: undefined,
252
- noCache: undefined,
253
- cacheRead: undefined,
254
- cacheWrite: undefined,
255
- },
256
- outputTokens: {
257
- total: undefined,
258
- text: undefined,
259
- reasoning: undefined,
260
- },
245
+ inputTokens: undefined,
246
+ outputTokens: undefined,
247
+ totalTokens: undefined,
261
248
  };
262
249
  }
263
- function toV3Content(parts) {
250
+ function toV2Content(parts) {
264
251
  return parts.map((part) => {
265
252
  if (part.type !== 'tool-call') {
266
253
  return part;
@@ -64,8 +64,8 @@ function waitFor(ms) {
64
64
  }
65
65
  function decodeRequestPayload(body) {
66
66
  // Body is now a connect frame directly (gzip is inside the frame, not outside)
67
- const decodedFrames = connectFrameDecode(body);
68
- return decodedFrames[0] ?? Buffer.alloc(0);
67
+ const { payloads } = connectFrameDecode(body);
68
+ return payloads[0] ?? Buffer.alloc(0);
69
69
  }
70
70
  function extractToolsPayload(strings) {
71
71
  return strings.find((value) => value.startsWith('[{"type":"function"'));
@@ -131,25 +131,17 @@ describe('DevstralLanguageModel doGenerate', () => {
131
131
  });
132
132
  };
133
133
  const model = new DevstralLanguageModel({ apiKey: 'test-api-key', fetch: fakeFetch, baseURL: 'https://windsurf.test' });
134
- expect(model.specificationVersion).toBe('v3');
134
+ expect(model.specificationVersion).toBe('v2');
135
135
  expect(model.supportedUrls).toEqual({});
136
136
  const result = await model.doGenerate({
137
137
  prompt: [{ role: 'user', content: [{ type: 'text', text: 'Find auth logic.' }] }],
138
138
  });
139
139
  expect(result.content).toEqual([{ type: 'text', text: 'generated answer' }]);
140
- expect(result.finishReason).toEqual({ unified: 'stop', raw: undefined });
140
+ expect(result.finishReason).toBe('stop');
141
141
  expect(result.usage).toEqual({
142
- inputTokens: {
143
- total: undefined,
144
- noCache: undefined,
145
- cacheRead: undefined,
146
- cacheWrite: undefined,
147
- },
148
- outputTokens: {
149
- total: undefined,
150
- text: undefined,
151
- reasoning: undefined,
152
- },
142
+ inputTokens: undefined,
143
+ outputTokens: undefined,
144
+ totalTokens: undefined,
153
145
  });
154
146
  expect(calls).toHaveLength(2);
155
147
  expect(calls[0]?.url).toBe('https://windsurf.test/exa.auth_pb.AuthService/GetUserJwt');
@@ -198,7 +190,7 @@ describe('DevstralLanguageModel doGenerate', () => {
198
190
  input: '{"query":"jwt manager"}',
199
191
  },
200
192
  ]);
201
- expect(result.finishReason).toEqual({ unified: 'tool-calls', raw: undefined });
193
+ expect(result.finishReason).toBe('tool-calls');
202
194
  const strings = extractStrings(decodeRequestPayload(requestBodies[0] ?? Buffer.alloc(0)));
203
195
  const toolsPayload = extractToolsPayload(strings);
204
196
  expect(toolsPayload).toBeDefined();
@@ -248,7 +240,7 @@ describe('DevstralLanguageModel doGenerate', () => {
248
240
  },
249
241
  },
250
242
  {
251
- type: 'provider',
243
+ type: 'provider-defined',
252
244
  id: 'windsurf.restricted_exec',
253
245
  name: 'restricted_exec',
254
246
  args: { mode: 'read-only' },
@@ -368,22 +360,11 @@ describe('DevstralLanguageModel doStream', () => {
368
360
  expect(parts[4]).toMatchObject({ type: 'text-delta', delta: 'world' });
369
361
  expect(parts[6]).toEqual({
370
362
  type: 'finish',
371
- finishReason: {
372
- unified: 'stop',
373
- raw: undefined,
374
- },
363
+ finishReason: 'stop',
375
364
  usage: {
376
- inputTokens: {
377
- total: undefined,
378
- noCache: undefined,
379
- cacheRead: undefined,
380
- cacheWrite: undefined,
381
- },
382
- outputTokens: {
383
- total: undefined,
384
- text: undefined,
385
- reasoning: undefined,
386
- },
365
+ inputTokens: undefined,
366
+ outputTokens: undefined,
367
+ totalTokens: undefined,
387
368
  },
388
369
  });
389
370
  }
@@ -446,25 +427,84 @@ describe('DevstralLanguageModel doStream', () => {
446
427
  });
447
428
  expect(parts[6]).toEqual({
448
429
  type: 'finish',
449
- finishReason: {
450
- unified: 'tool-calls',
451
- raw: undefined,
452
- },
430
+ finishReason: 'tool-calls',
453
431
  usage: {
454
- inputTokens: {
455
- total: undefined,
456
- noCache: undefined,
457
- cacheRead: undefined,
458
- cacheWrite: undefined,
459
- },
460
- outputTokens: {
461
- total: undefined,
462
- text: undefined,
463
- reasoning: undefined,
464
- },
432
+ inputTokens: undefined,
433
+ outputTokens: undefined,
434
+ totalTokens: undefined,
465
435
  },
466
436
  });
467
437
  });
438
+ it('stream terminates immediately on EndStreamResponse frame (flags=2)', async () => {
439
+ const jwt = makeJwt(4_300_000_003, 'end-stream-response');
440
+ const textFrame = connectFrameEncode(Buffer.from('hello world', 'utf8'));
441
+ const endStreamPayload = Buffer.alloc(0);
442
+ const endStreamFrame = Buffer.allocUnsafe(5 + endStreamPayload.length);
443
+ endStreamFrame.writeUInt8(2, 0);
444
+ endStreamFrame.writeUInt32BE(endStreamPayload.length, 1);
445
+ endStreamPayload.copy(endStreamFrame, 5);
446
+ let releaseHttpClose;
447
+ const waitForHttpClose = new Promise((resolve) => {
448
+ releaseHttpClose = resolve;
449
+ });
450
+ const fakeFetch = async (input) => {
451
+ const url = String(input);
452
+ if (url.endsWith('/GetUserJwt')) {
453
+ return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
454
+ }
455
+ let sentTextFrame = false;
456
+ let sentEndStreamFrame = false;
457
+ return new Response(new ReadableStream({
458
+ async pull(controller) {
459
+ if (!sentTextFrame) {
460
+ controller.enqueue(Uint8Array.from(textFrame));
461
+ sentTextFrame = true;
462
+ return;
463
+ }
464
+ if (!sentEndStreamFrame) {
465
+ controller.enqueue(Uint8Array.from(endStreamFrame));
466
+ sentEndStreamFrame = true;
467
+ return;
468
+ }
469
+ await waitForHttpClose;
470
+ controller.close();
471
+ },
472
+ }), { status: 200 });
473
+ };
474
+ const model = new DevstralLanguageModel({
475
+ apiKey: 'end-stream-key',
476
+ fetch: fakeFetch,
477
+ baseURL: 'https://windsurf.test',
478
+ });
479
+ const result = await model.doStream({
480
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'Stream with end.' }] }],
481
+ });
482
+ const reader = result.stream.getReader();
483
+ const parts = [];
484
+ const resultType = await Promise.race([
485
+ (async () => {
486
+ while (true) {
487
+ const next = await reader.read();
488
+ if (next.done) {
489
+ break;
490
+ }
491
+ parts.push(next.value);
492
+ }
493
+ return 'completed';
494
+ })(),
495
+ waitFor(100).then(() => 'timed-out'),
496
+ ]);
497
+ expect(resultType).toBe('completed');
498
+ expect(parts.map((p) => p.type)).toEqual([
499
+ 'stream-start',
500
+ 'response-metadata',
501
+ 'text-start',
502
+ 'text-delta',
503
+ 'text-end',
504
+ 'finish',
505
+ ]);
506
+ releaseHttpClose?.();
507
+ });
468
508
  it('abort stops stream mid-response', async () => {
469
509
  const controller = new AbortController();
470
510
  const jwt = makeJwt(4_300_000_002, 'abort');
@@ -1,6 +1,7 @@
1
1
  import { gunzipSync, gzipSync } from 'node:zlib';
2
2
  const FRAME_HEADER_SIZE = 5;
3
- const FLAG_COMPRESSED = 1;
3
+ export const FLAG_COMPRESSED = 1;
4
+ export const FLAG_END_STREAM = 2;
4
5
  /**
5
6
  * Encode a protobuf payload into a Connect-RPC frame.
6
7
  *
@@ -20,6 +21,7 @@ export function connectFrameEncode(payload, compress = true) {
20
21
  export function connectFrameDecode(buffer) {
21
22
  const decoded = [];
22
23
  let offset = 0;
24
+ let isEndStream = false;
23
25
  while (offset + FRAME_HEADER_SIZE <= buffer.length) {
24
26
  const flags = buffer.readUInt8(offset);
25
27
  const payloadLength = buffer.readUInt32BE(offset + 1);
@@ -29,8 +31,11 @@ export function connectFrameDecode(buffer) {
29
31
  break;
30
32
  }
31
33
  const payload = buffer.subarray(payloadStart, payloadEnd);
32
- decoded.push(flags === FLAG_COMPRESSED ? gunzipSync(payload) : Buffer.from(payload));
34
+ decoded.push((flags & FLAG_COMPRESSED) !== 0 ? gunzipSync(payload) : Buffer.from(payload));
35
+ if ((flags & FLAG_END_STREAM) !== 0) {
36
+ isEndStream = true;
37
+ }
33
38
  offset = payloadEnd;
34
39
  }
35
- return decoded;
40
+ return { payloads: decoded, isEndStream };
36
41
  }
@@ -1,7 +1,8 @@
1
+ import { gzipSync } from 'node:zlib';
1
2
  import { readFileSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { describe, expect, it } from 'vitest';
4
- import { connectFrameDecode, connectFrameEncode } from './connect-frame.js';
5
+ import { connectFrameDecode, connectFrameEncode, FLAG_COMPRESSED, FLAG_END_STREAM } from './connect-frame.js';
5
6
  const fixtureSimpleFrame = readFileSync(join(process.cwd(), 'test/fixtures/connect/frame-simple.bin'));
6
7
  const fixtureGzipFrame = readFileSync(join(process.cwd(), 'test/fixtures/connect/frame-gzip.bin'));
7
8
  const fixtureMultiFrame = readFileSync(join(process.cwd(), 'test/fixtures/connect/frame-multi.bin'));
@@ -13,22 +14,94 @@ describe('connectFrameEncode', () => {
13
14
  expect(encoded.readUInt8(0)).toBe(0);
14
15
  expect(encoded.readUInt32BE(1)).toBe(payloadHello.length);
15
16
  expect(encoded.subarray(5)).toEqual(payloadHello);
16
- expect(connectFrameDecode(encoded)).toEqual([payloadHello]);
17
+ expect(connectFrameDecode(encoded)).toEqual({ payloads: [payloadHello], isEndStream: false });
17
18
  });
18
19
  it('gzip roundtrip uses compressed frame and decodes back to source payload', () => {
19
20
  const encoded = connectFrameEncode(payloadWorld, true);
20
21
  expect(encoded.readUInt8(0)).toBe(1);
21
- expect(connectFrameDecode(encoded)).toEqual([payloadWorld]);
22
+ expect(connectFrameDecode(encoded)).toEqual({ payloads: [payloadWorld], isEndStream: false });
22
23
  });
23
24
  });
24
25
  describe('connectFrameDecode fixtures', () => {
25
26
  it('decodes frame-simple fixture payload', () => {
26
- expect(connectFrameDecode(fixtureSimpleFrame)).toEqual([payloadHello]);
27
+ expect(connectFrameDecode(fixtureSimpleFrame)).toEqual({ payloads: [payloadHello], isEndStream: false });
27
28
  });
28
29
  it('decodes gzip fixture payload', () => {
29
- expect(connectFrameDecode(fixtureGzipFrame)).toEqual([payloadWorld]);
30
+ expect(connectFrameDecode(fixtureGzipFrame)).toEqual({ payloads: [payloadWorld], isEndStream: false });
30
31
  });
31
32
  it('decodes multiple concatenated frames in order', () => {
32
- expect(connectFrameDecode(fixtureMultiFrame)).toEqual([payloadHello, payloadHello]);
33
+ expect(connectFrameDecode(fixtureMultiFrame)).toEqual({ payloads: [payloadHello, payloadHello], isEndStream: false });
34
+ });
35
+ });
36
+ describe('connect frame flags', () => {
37
+ it('exports FLAG_COMPRESSED constant with value 1', () => {
38
+ expect(FLAG_COMPRESSED).toBe(1);
39
+ });
40
+ it('exports FLAG_END_STREAM constant with value 2', () => {
41
+ expect(FLAG_END_STREAM).toBe(2);
42
+ });
43
+ it('supports bit combination: FLAG_COMPRESSED | FLAG_END_STREAM === 3', () => {
44
+ expect(FLAG_COMPRESSED | FLAG_END_STREAM).toBe(3);
45
+ });
46
+ });
47
+ describe('connectFrameDecode isEndStream', () => {
48
+ /**
49
+ * TDD RED PHASE: Tests for isEndStream flag detection.
50
+ * These tests MUST FAIL because connectFrameDecode currently returns Buffer[],
51
+ * not { payloads: Buffer[], isEndStream: boolean }.
52
+ *
53
+ * Connect-RPC flag bits:
54
+ * - Bit 0 (value 1): FLAG_COMPRESSED
55
+ * - Bit 1 (value 2): FLAG_END_STREAM
56
+ */
57
+ it('returns isEndStream: false for flags=0 (no flags)', () => {
58
+ // flags=0: neither compressed nor end stream
59
+ const frame = Buffer.allocUnsafe(5 + payloadHello.length);
60
+ frame.writeUInt8(0, 0); // flags = 0
61
+ frame.writeUInt32BE(payloadHello.length, 1);
62
+ payloadHello.copy(frame, 5);
63
+ const result = connectFrameDecode(frame);
64
+ expect(result).toHaveProperty('payloads');
65
+ expect(result).toHaveProperty('isEndStream');
66
+ expect(result.payloads).toEqual([payloadHello]);
67
+ expect(result.isEndStream).toBe(false);
68
+ });
69
+ it('returns isEndStream: false for flags=1 (compressed only)', () => {
70
+ // flags=1: compressed but not end stream
71
+ const gzipped = gzipSync(payloadHello);
72
+ const frame = Buffer.allocUnsafe(5 + gzipped.length);
73
+ frame.writeUInt8(FLAG_COMPRESSED, 0); // flags = 1
74
+ frame.writeUInt32BE(gzipped.length, 1);
75
+ gzipped.copy(frame, 5);
76
+ const result = connectFrameDecode(frame);
77
+ expect(result).toHaveProperty('payloads');
78
+ expect(result).toHaveProperty('isEndStream');
79
+ expect(result.payloads).toEqual([payloadHello]);
80
+ expect(result.isEndStream).toBe(false);
81
+ });
82
+ it('returns isEndStream: true for flags=2 (end stream only)', () => {
83
+ // flags=2: end stream but not compressed
84
+ const frame = Buffer.allocUnsafe(5 + payloadHello.length);
85
+ frame.writeUInt8(FLAG_END_STREAM, 0); // flags = 2
86
+ frame.writeUInt32BE(payloadHello.length, 1);
87
+ payloadHello.copy(frame, 5);
88
+ const result = connectFrameDecode(frame);
89
+ expect(result).toHaveProperty('payloads');
90
+ expect(result).toHaveProperty('isEndStream');
91
+ expect(result.payloads).toEqual([payloadHello]);
92
+ expect(result.isEndStream).toBe(true);
93
+ });
94
+ it('returns isEndStream: true for flags=3 (compressed + end stream)', () => {
95
+ // flags=3: both compressed and end stream
96
+ const gzipped = gzipSync(payloadHello);
97
+ const frame = Buffer.allocUnsafe(5 + gzipped.length);
98
+ frame.writeUInt8(FLAG_COMPRESSED | FLAG_END_STREAM, 0); // flags = 3
99
+ frame.writeUInt32BE(gzipped.length, 1);
100
+ gzipped.copy(frame, 5);
101
+ const result = connectFrameDecode(frame);
102
+ expect(result).toHaveProperty('payloads');
103
+ expect(result).toHaveProperty('isEndStream');
104
+ expect(result.payloads).toEqual([payloadHello]);
105
+ expect(result.isEndStream).toBe(true);
33
106
  });
34
107
  });
@@ -1,4 +1,4 @@
1
- import type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3GenerateResult, LanguageModelV3StreamResult } from '@ai-sdk/provider';
1
+ import type { LanguageModelV2, LanguageModelV2CallOptions } from '@ai-sdk/provider';
2
2
  import { JwtManager } from '../auth/jwt-manager.js';
3
3
  import { DevstralTransport } from '../transport/http.js';
4
4
  import type { WindsurfProviderOptions } from '../types/index.js';
@@ -7,8 +7,10 @@ export interface DevstralLanguageModelOptions extends WindsurfProviderOptions {
7
7
  transport?: DevstralTransport;
8
8
  jwtManager?: JwtManager;
9
9
  }
10
- export declare class DevstralLanguageModel implements LanguageModelV3 {
11
- readonly specificationVersion = "v3";
10
+ type LanguageModelV2GenerateResult = Awaited<ReturnType<LanguageModelV2['doGenerate']>>;
11
+ type LanguageModelV2StreamResult = Awaited<ReturnType<LanguageModelV2['doStream']>>;
12
+ export declare class DevstralLanguageModel implements LanguageModelV2 {
13
+ readonly specificationVersion = "v2";
12
14
  readonly provider = "windsurf";
13
15
  readonly modelId: string;
14
16
  readonly supportedUrls: Record<string, RegExp[]>;
@@ -18,6 +20,7 @@ export declare class DevstralLanguageModel implements LanguageModelV3 {
18
20
  private readonly transport;
19
21
  private readonly jwtManager;
20
22
  constructor(options?: DevstralLanguageModelOptions);
21
- doGenerate(options: LanguageModelV3CallOptions): Promise<LanguageModelV3GenerateResult>;
22
- doStream(options: LanguageModelV3CallOptions): Promise<LanguageModelV3StreamResult>;
23
+ doGenerate(options: LanguageModelV2CallOptions): Promise<LanguageModelV2GenerateResult>;
24
+ doStream(options: LanguageModelV2CallOptions): Promise<LanguageModelV2StreamResult>;
23
25
  }
26
+ export {};
@@ -1,3 +1,5 @@
1
+ export declare const FLAG_COMPRESSED = 1;
2
+ export declare const FLAG_END_STREAM = 2;
1
3
  /**
2
4
  * Encode a protobuf payload into a Connect-RPC frame.
3
5
  *
@@ -7,4 +9,7 @@
7
9
  * @returns Buffer containing: [flags byte][4-byte length][payload (gzipped if compress=true)]
8
10
  */
9
11
  export declare function connectFrameEncode(payload: Buffer, compress?: boolean): Buffer;
10
- export declare function connectFrameDecode(buffer: Buffer): Buffer[];
12
+ export declare function connectFrameDecode(buffer: Buffer): {
13
+ payloads: Buffer[];
14
+ isEndStream: boolean;
15
+ };
@@ -6,7 +6,7 @@ export interface WindsurfProviderOptions {
6
6
  generateId?: () => string;
7
7
  }
8
8
  /**
9
- * Windsurf provider interface extending AI SDK V3 ProviderV3 pattern.
9
+ * Windsurf provider interface extending AI SDK V2 ProviderV2 pattern.
10
10
  * The provider is callable as a function and has a languageModel method.
11
11
  */
12
12
  export interface WindsurfProvider {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bxb1337/windsurf-fast-context",
3
- "version": "1.0.8",
3
+ "version": "1.1.0",
4
4
  "description": "AI SDK V3 provider for Windsurf's Devstral code search API",
5
5
  "type": "module",
6
6
  "scripts": {