@bxb1337/windsurf-fast-context 1.0.8 → 1.0.9
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/model/devstral-language-model.js +8 -4
- package/dist/cjs/model/devstral-language-model.test.js +72 -2
- package/dist/cjs/protocol/connect-frame.js +10 -4
- package/dist/cjs/protocol/connect-frame.test.js +78 -5
- package/dist/esm/model/devstral-language-model.js +8 -4
- package/dist/esm/model/devstral-language-model.test.js +72 -2
- package/dist/esm/protocol/connect-frame.js +8 -3
- package/dist/esm/protocol/connect-frame.test.js +79 -6
- package/dist/protocol/connect-frame.d.ts +6 -1
- package/package.json +1 -1
|
@@ -56,7 +56,7 @@ 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
61
|
const content = payloads.flatMap((payload) => toV3Content((0, response_converter_js_1.convertResponse)(payload)));
|
|
62
62
|
const unified = content.some((part) => part.type === 'tool-call')
|
|
@@ -110,7 +110,7 @@ class DevstralLanguageModel {
|
|
|
110
110
|
try {
|
|
111
111
|
safeEnqueue(controller, { type: 'stream-start', warnings: [] });
|
|
112
112
|
safeEnqueue(controller, { type: 'response-metadata', modelId: this.modelId });
|
|
113
|
-
while (!isAborted(options.abortSignal)) {
|
|
113
|
+
outerLoop: while (!isAborted(options.abortSignal)) {
|
|
114
114
|
const next = await reader.read();
|
|
115
115
|
if (next.done) {
|
|
116
116
|
break;
|
|
@@ -162,6 +162,9 @@ class DevstralLanguageModel {
|
|
|
162
162
|
});
|
|
163
163
|
safeEnqueue(controller, part);
|
|
164
164
|
}
|
|
165
|
+
if (frameResult.isEndStream) {
|
|
166
|
+
break outerLoop;
|
|
167
|
+
}
|
|
165
168
|
}
|
|
166
169
|
}
|
|
167
170
|
if (isAborted(options.abortSignal)) {
|
|
@@ -219,10 +222,11 @@ function readNextConnectFrame(buffer) {
|
|
|
219
222
|
return null;
|
|
220
223
|
}
|
|
221
224
|
const frame = buffer.subarray(0, frameLength);
|
|
222
|
-
const
|
|
225
|
+
const { payloads, isEndStream } = (0, connect_frame_js_1.connectFrameDecode)(frame);
|
|
223
226
|
return {
|
|
224
|
-
payload:
|
|
227
|
+
payload: payloads[0] ?? Buffer.alloc(0),
|
|
225
228
|
rest: buffer.subarray(frameLength),
|
|
229
|
+
isEndStream,
|
|
226
230
|
};
|
|
227
231
|
}
|
|
228
232
|
function safeEnqueue(controller, 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
|
|
70
|
-
return
|
|
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"'));
|
|
@@ -467,6 +467,76 @@ async function collectStreamParts(stream) {
|
|
|
467
467
|
},
|
|
468
468
|
});
|
|
469
469
|
});
|
|
470
|
+
(0, vitest_1.it)('stream terminates immediately on EndStreamResponse frame (flags=2)', async () => {
|
|
471
|
+
const jwt = makeJwt(4_300_000_003, 'end-stream-response');
|
|
472
|
+
const textFrame = (0, connect_frame_js_1.connectFrameEncode)(Buffer.from('hello world', 'utf8'));
|
|
473
|
+
const endStreamPayload = Buffer.alloc(0);
|
|
474
|
+
const endStreamFrame = Buffer.allocUnsafe(5 + endStreamPayload.length);
|
|
475
|
+
endStreamFrame.writeUInt8(2, 0);
|
|
476
|
+
endStreamFrame.writeUInt32BE(endStreamPayload.length, 1);
|
|
477
|
+
endStreamPayload.copy(endStreamFrame, 5);
|
|
478
|
+
let releaseHttpClose;
|
|
479
|
+
const waitForHttpClose = new Promise((resolve) => {
|
|
480
|
+
releaseHttpClose = resolve;
|
|
481
|
+
});
|
|
482
|
+
const fakeFetch = async (input) => {
|
|
483
|
+
const url = String(input);
|
|
484
|
+
if (url.endsWith('/GetUserJwt')) {
|
|
485
|
+
return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
|
|
486
|
+
}
|
|
487
|
+
let sentTextFrame = false;
|
|
488
|
+
let sentEndStreamFrame = false;
|
|
489
|
+
return new Response(new ReadableStream({
|
|
490
|
+
async pull(controller) {
|
|
491
|
+
if (!sentTextFrame) {
|
|
492
|
+
controller.enqueue(Uint8Array.from(textFrame));
|
|
493
|
+
sentTextFrame = true;
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (!sentEndStreamFrame) {
|
|
497
|
+
controller.enqueue(Uint8Array.from(endStreamFrame));
|
|
498
|
+
sentEndStreamFrame = true;
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
await waitForHttpClose;
|
|
502
|
+
controller.close();
|
|
503
|
+
},
|
|
504
|
+
}), { status: 200 });
|
|
505
|
+
};
|
|
506
|
+
const model = new devstral_language_model_js_1.DevstralLanguageModel({
|
|
507
|
+
apiKey: 'end-stream-key',
|
|
508
|
+
fetch: fakeFetch,
|
|
509
|
+
baseURL: 'https://windsurf.test',
|
|
510
|
+
});
|
|
511
|
+
const result = await model.doStream({
|
|
512
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Stream with end.' }] }],
|
|
513
|
+
});
|
|
514
|
+
const reader = result.stream.getReader();
|
|
515
|
+
const parts = [];
|
|
516
|
+
const resultType = await Promise.race([
|
|
517
|
+
(async () => {
|
|
518
|
+
while (true) {
|
|
519
|
+
const next = await reader.read();
|
|
520
|
+
if (next.done) {
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
parts.push(next.value);
|
|
524
|
+
}
|
|
525
|
+
return 'completed';
|
|
526
|
+
})(),
|
|
527
|
+
waitFor(100).then(() => 'timed-out'),
|
|
528
|
+
]);
|
|
529
|
+
(0, vitest_1.expect)(resultType).toBe('completed');
|
|
530
|
+
(0, vitest_1.expect)(parts.map((p) => p.type)).toEqual([
|
|
531
|
+
'stream-start',
|
|
532
|
+
'response-metadata',
|
|
533
|
+
'text-start',
|
|
534
|
+
'text-delta',
|
|
535
|
+
'text-end',
|
|
536
|
+
'finish',
|
|
537
|
+
]);
|
|
538
|
+
releaseHttpClose?.();
|
|
539
|
+
});
|
|
470
540
|
(0, vitest_1.it)('abort stops stream mid-response', async () => {
|
|
471
541
|
const controller = new AbortController();
|
|
472
542
|
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
|
-
|
|
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
|
|
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
|
});
|
|
@@ -53,7 +53,7 @@ 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
58
|
const content = payloads.flatMap((payload) => toV3Content(convertResponse(payload)));
|
|
59
59
|
const unified = content.some((part) => part.type === 'tool-call')
|
|
@@ -107,7 +107,7 @@ export class DevstralLanguageModel {
|
|
|
107
107
|
try {
|
|
108
108
|
safeEnqueue(controller, { type: 'stream-start', warnings: [] });
|
|
109
109
|
safeEnqueue(controller, { type: 'response-metadata', modelId: this.modelId });
|
|
110
|
-
while (!isAborted(options.abortSignal)) {
|
|
110
|
+
outerLoop: while (!isAborted(options.abortSignal)) {
|
|
111
111
|
const next = await reader.read();
|
|
112
112
|
if (next.done) {
|
|
113
113
|
break;
|
|
@@ -159,6 +159,9 @@ export class DevstralLanguageModel {
|
|
|
159
159
|
});
|
|
160
160
|
safeEnqueue(controller, part);
|
|
161
161
|
}
|
|
162
|
+
if (frameResult.isEndStream) {
|
|
163
|
+
break outerLoop;
|
|
164
|
+
}
|
|
162
165
|
}
|
|
163
166
|
}
|
|
164
167
|
if (isAborted(options.abortSignal)) {
|
|
@@ -215,10 +218,11 @@ function readNextConnectFrame(buffer) {
|
|
|
215
218
|
return null;
|
|
216
219
|
}
|
|
217
220
|
const frame = buffer.subarray(0, frameLength);
|
|
218
|
-
const
|
|
221
|
+
const { payloads, isEndStream } = connectFrameDecode(frame);
|
|
219
222
|
return {
|
|
220
|
-
payload:
|
|
223
|
+
payload: payloads[0] ?? Buffer.alloc(0),
|
|
221
224
|
rest: buffer.subarray(frameLength),
|
|
225
|
+
isEndStream,
|
|
222
226
|
};
|
|
223
227
|
}
|
|
224
228
|
function safeEnqueue(controller, 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
|
|
68
|
-
return
|
|
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"'));
|
|
@@ -465,6 +465,76 @@ describe('DevstralLanguageModel doStream', () => {
|
|
|
465
465
|
},
|
|
466
466
|
});
|
|
467
467
|
});
|
|
468
|
+
it('stream terminates immediately on EndStreamResponse frame (flags=2)', async () => {
|
|
469
|
+
const jwt = makeJwt(4_300_000_003, 'end-stream-response');
|
|
470
|
+
const textFrame = connectFrameEncode(Buffer.from('hello world', 'utf8'));
|
|
471
|
+
const endStreamPayload = Buffer.alloc(0);
|
|
472
|
+
const endStreamFrame = Buffer.allocUnsafe(5 + endStreamPayload.length);
|
|
473
|
+
endStreamFrame.writeUInt8(2, 0);
|
|
474
|
+
endStreamFrame.writeUInt32BE(endStreamPayload.length, 1);
|
|
475
|
+
endStreamPayload.copy(endStreamFrame, 5);
|
|
476
|
+
let releaseHttpClose;
|
|
477
|
+
const waitForHttpClose = new Promise((resolve) => {
|
|
478
|
+
releaseHttpClose = resolve;
|
|
479
|
+
});
|
|
480
|
+
const fakeFetch = async (input) => {
|
|
481
|
+
const url = String(input);
|
|
482
|
+
if (url.endsWith('/GetUserJwt')) {
|
|
483
|
+
return new Response(Uint8Array.from(Buffer.from(jwt, 'utf8')), { status: 200 });
|
|
484
|
+
}
|
|
485
|
+
let sentTextFrame = false;
|
|
486
|
+
let sentEndStreamFrame = false;
|
|
487
|
+
return new Response(new ReadableStream({
|
|
488
|
+
async pull(controller) {
|
|
489
|
+
if (!sentTextFrame) {
|
|
490
|
+
controller.enqueue(Uint8Array.from(textFrame));
|
|
491
|
+
sentTextFrame = true;
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (!sentEndStreamFrame) {
|
|
495
|
+
controller.enqueue(Uint8Array.from(endStreamFrame));
|
|
496
|
+
sentEndStreamFrame = true;
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
await waitForHttpClose;
|
|
500
|
+
controller.close();
|
|
501
|
+
},
|
|
502
|
+
}), { status: 200 });
|
|
503
|
+
};
|
|
504
|
+
const model = new DevstralLanguageModel({
|
|
505
|
+
apiKey: 'end-stream-key',
|
|
506
|
+
fetch: fakeFetch,
|
|
507
|
+
baseURL: 'https://windsurf.test',
|
|
508
|
+
});
|
|
509
|
+
const result = await model.doStream({
|
|
510
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Stream with end.' }] }],
|
|
511
|
+
});
|
|
512
|
+
const reader = result.stream.getReader();
|
|
513
|
+
const parts = [];
|
|
514
|
+
const resultType = await Promise.race([
|
|
515
|
+
(async () => {
|
|
516
|
+
while (true) {
|
|
517
|
+
const next = await reader.read();
|
|
518
|
+
if (next.done) {
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
parts.push(next.value);
|
|
522
|
+
}
|
|
523
|
+
return 'completed';
|
|
524
|
+
})(),
|
|
525
|
+
waitFor(100).then(() => 'timed-out'),
|
|
526
|
+
]);
|
|
527
|
+
expect(resultType).toBe('completed');
|
|
528
|
+
expect(parts.map((p) => p.type)).toEqual([
|
|
529
|
+
'stream-start',
|
|
530
|
+
'response-metadata',
|
|
531
|
+
'text-start',
|
|
532
|
+
'text-delta',
|
|
533
|
+
'text-end',
|
|
534
|
+
'finish',
|
|
535
|
+
]);
|
|
536
|
+
releaseHttpClose?.();
|
|
537
|
+
});
|
|
468
538
|
it('abort stops stream mid-response', async () => {
|
|
469
539
|
const controller = new AbortController();
|
|
470
540
|
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
|
|
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,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):
|
|
12
|
+
export declare function connectFrameDecode(buffer: Buffer): {
|
|
13
|
+
payloads: Buffer[];
|
|
14
|
+
isEndStream: boolean;
|
|
15
|
+
};
|