diff --git a/packages/stream-text/src/index.ts b/packages/stream-text/src/index.ts index 6871565..f9e1475 100644 --- a/packages/stream-text/src/index.ts +++ b/packages/stream-text/src/index.ts @@ -44,8 +44,7 @@ export interface StreamTextResult { usage?: Usage } -const chunkHeaderPrefix = 'data: ' -const chunkErrorPrefix = `{"error":` +const chunkHeaderPrefix = 'data:' /** * @experimental WIP, does not support function calling (tools). @@ -64,30 +63,51 @@ export const streamText = async (options: StreamTextOptions): Promise { buffer += decoder.decode(chunk, { stream: true }) - const lines = buffer.split('\n\n') - buffer = lines.pop() || '' - - for (const line of lines) { - // Some cases: - // - Empty chunk - // - :ROUTER PROCESSING from OpenRouter - if (!line || !line.startsWith(chunkHeaderPrefix)) { - continue + const events = buffer.split('\n\n') + // Since the stream must ended with '\n\n', so it's safe to pop the last "event"(it should be empty) + buffer = events.pop() || '' + + for (const event of events) { + // Handle multi-line data + const lines = event.split('\n') + const dataLines: string[] = [] + + for (const line of lines) { + // Skip empty lines + if (!line) + continue + // Check for comments + if (line.startsWith(':')) + continue + // Check if line starts with data: (allowing spaces) + if (line.startsWith(chunkHeaderPrefix)) { + // Extract content after data: (remove leading single space if present) + const content = line.slice(chunkHeaderPrefix.length) + dataLines.push(content.length > 0 && content[0] === ' ' ? content.slice(1) : content) + } + else { + break // event ended, dispatch + } } - const lineWithoutPrefix = line.slice(chunkHeaderPrefix.length) - if (lineWithoutPrefix.startsWith(chunkErrorPrefix)) { - // About controller error: https://developer.mozilla.org/en-US/docs/Web/API/TransformStreamDefaultController/error - controller.error(new Error(`Error from server: ${lineWithoutPrefix}`)) + // Skip this event if no data lines found + if (dataLines.length === 0) + continue + + const data = dataLines.join('\n') + + if (data === '[DONE]') { + controller.terminate() break } - if (lineWithoutPrefix === '[DONE]') { - controller.terminate() + // Maybe we should use `JSON.parse` to check if it's a valid JSON + if (data.startsWith('{') && data.includes('"error"')) { + controller.error(new Error(`Error from server: ${data}`)) break } - const chunk: ChunkResult = JSON.parse(lineWithoutPrefix) + const chunk: ChunkResult = JSON.parse(data) controller.enqueue(chunk) if (options.onChunk)