Skip to content

Commit 753bced

Browse files
committed
fix(streaming): correctly handle trailing new lines in byte chunks (#708)
1 parent 588b30f commit 753bced

File tree

2 files changed

+49
-1
lines changed

2 files changed

+49
-1
lines changed

src/streaming.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ class SSEDecoder {
267267
*
268268
* https://github.com/encode/httpx/blob/920333ea98118e9cf617f246905d7b202510941c/httpx/_decoders.py#L258
269269
*/
270-
class LineDecoder {
270+
export class LineDecoder {
271271
// prettier-ignore
272272
static NEWLINE_CHARS = new Set(['\n', '\r', '\x0b', '\x0c', '\x1c', '\x1d', '\x1e', '\x85', '\u2028', '\u2029']);
273273
static NEWLINE_REGEXP = /\r\n|[\n\r\x0b\x0c\x1c\x1d\x1e\x85\u2028\u2029]/g;
@@ -300,6 +300,12 @@ class LineDecoder {
300300
const trailingNewline = LineDecoder.NEWLINE_CHARS.has(text[text.length - 1] || '');
301301
let lines = text.split(LineDecoder.NEWLINE_REGEXP);
302302

303+
// if there is a trailing new line then the last entry will be an empty
304+
// string which we don't care about
305+
if (trailingNewline) {
306+
lines.pop();
307+
}
308+
303309
if (lines.length === 1 && !trailingNewline) {
304310
this.buffer.push(lines[0]!);
305311
return [];

tests/streaming.test.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { LineDecoder } from 'openai/streaming';
2+
3+
function decodeChunks(chunks: string[], decoder?: LineDecoder): string[] {
4+
if (!decoder) {
5+
decoder = new LineDecoder();
6+
}
7+
8+
const lines = [];
9+
for (const chunk of chunks) {
10+
lines.push(...decoder.decode(chunk));
11+
}
12+
13+
return lines;
14+
}
15+
16+
describe('line decoder', () => {
17+
test('basic', () => {
18+
// baz is not included because the line hasn't ended yet
19+
expect(decodeChunks(['foo', ' bar\nbaz'])).toEqual(['foo bar']);
20+
});
21+
22+
test('basic with \\r', () => {
23+
// baz is not included because the line hasn't ended yet
24+
expect(decodeChunks(['foo', ' bar\r\nbaz'])).toEqual(['foo bar']);
25+
});
26+
27+
test('trailing new lines', () => {
28+
expect(decodeChunks(['foo', ' bar', 'baz\n', 'thing\n'])).toEqual(['foo barbaz', 'thing']);
29+
});
30+
31+
test('trailing new lines with \\r', () => {
32+
expect(decodeChunks(['foo', ' bar', 'baz\r\n', 'thing\r\n'])).toEqual(['foo barbaz', 'thing']);
33+
});
34+
35+
test('escaped new lines', () => {
36+
expect(decodeChunks(['foo', ' bar\\nbaz\n'])).toEqual(['foo bar\\nbaz']);
37+
});
38+
39+
test('escaped new lines with \\r', () => {
40+
expect(decodeChunks(['foo', ' bar\\r\\nbaz\n'])).toEqual(['foo bar\\r\\nbaz']);
41+
});
42+
});

0 commit comments

Comments
 (0)