|
1 | 1 | import type { Response } from '@anthropic-ai/sdk/_shims/fetch';
|
| 2 | + |
2 | 3 | import { APIResponse, Headers, createResponseHeaders } from './core';
|
| 4 | + |
3 | 5 | import { safeJSON } from '@anthropic-ai/sdk/core';
|
4 | 6 | import { APIError } from '@anthropic-ai/sdk/error';
|
5 | 7 |
|
| 8 | +type Bytes = string | ArrayBuffer | Uint8Array | Buffer | null | undefined; |
| 9 | + |
6 | 10 | type ServerSentEvent = {
|
7 | 11 | event: string | null;
|
8 | 12 | data: string;
|
@@ -85,19 +89,11 @@ export class Stream<Item> implements AsyncIterable<Item>, APIResponse<Stream<Ite
|
85 | 89 | this.controller.abort();
|
86 | 90 | throw new Error(`Attempted to iterate over a response with no body`);
|
87 | 91 | }
|
88 |
| - |
89 | 92 | const lineDecoder = new LineDecoder();
|
90 | 93 |
|
91 |
| - // @ts-ignore |
92 |
| - for await (const chunk of this.response.body) { |
93 |
| - let text; |
94 |
| - if (chunk instanceof Buffer) { |
95 |
| - text = chunk.toString(); |
96 |
| - } else if ((chunk as any) instanceof Uint8Array) { |
97 |
| - text = Buffer.from(chunk).toString(); |
98 |
| - } else { |
99 |
| - text = chunk; |
100 |
| - } |
| 94 | + const iter = readableStreamAsyncIterable<Bytes>(this.response.body); |
| 95 | + for await (const chunk of iter) { |
| 96 | + const text = decodeText(chunk); |
101 | 97 |
|
102 | 98 | for (const line of lineDecoder.decode(text)) {
|
103 | 99 | const sse = this.decoder.decode(line);
|
@@ -218,3 +214,67 @@ function partition(str: string, delimiter: string): [string, string, string] {
|
218 | 214 |
|
219 | 215 | return [str, '', ''];
|
220 | 216 | }
|
| 217 | + |
| 218 | +let _textDecoder; |
| 219 | +function decodeText(bytes: Bytes): string { |
| 220 | + if (bytes == null) return ''; |
| 221 | + if (typeof bytes === 'string') return bytes; |
| 222 | + |
| 223 | + // Node: |
| 224 | + if (typeof Buffer !== 'undefined') { |
| 225 | + if (bytes instanceof Buffer) { |
| 226 | + return bytes.toString(); |
| 227 | + } |
| 228 | + if (bytes instanceof Uint8Array) { |
| 229 | + return Buffer.from(bytes).toString(); |
| 230 | + } |
| 231 | + |
| 232 | + throw new Error(`Unexpected: received non-Uint8Array (${bytes.constructor.name}) in Node.`); |
| 233 | + } |
| 234 | + |
| 235 | + // Browser |
| 236 | + if (typeof TextDecoder !== 'undefined') { |
| 237 | + if (bytes instanceof Uint8Array || bytes instanceof ArrayBuffer) { |
| 238 | + _textDecoder ??= new TextDecoder('utf8'); |
| 239 | + return _textDecoder.decode(bytes); |
| 240 | + } |
| 241 | + |
| 242 | + throw new Error( |
| 243 | + `Unexpected: received non-Uint8Array/ArrayBuffer (${ |
| 244 | + (bytes as any).constructor.name |
| 245 | + }) in a web platform.`, |
| 246 | + ); |
| 247 | + } |
| 248 | + |
| 249 | + throw new Error(`Unexpected: neither Buffer nor TextDecoder are available as globals.`); |
| 250 | +} |
| 251 | + |
| 252 | +/** |
| 253 | + * Most browsers don't yet have async iterable support for ReadableStream, |
| 254 | + * and Node has a very different way of reading bytes from its "ReadableStream". |
| 255 | + * |
| 256 | + * This polyfill was pulled from https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1624185965 |
| 257 | + * |
| 258 | + * We make extensive use of "any" here to avoid pulling in either "node" or "dom" types |
| 259 | + * to library users' type scopes. |
| 260 | + */ |
| 261 | +function readableStreamAsyncIterable<T>(stream: any): AsyncIterableIterator<T> { |
| 262 | + if (stream[Symbol.asyncIterator]) { |
| 263 | + return stream[Symbol.asyncIterator]; |
| 264 | + } |
| 265 | + |
| 266 | + const reader = stream.getReader(); |
| 267 | + |
| 268 | + return { |
| 269 | + next() { |
| 270 | + return reader.read(); |
| 271 | + }, |
| 272 | + async return() { |
| 273 | + reader.releaseLock(); |
| 274 | + return { done: true, value: undefined }; |
| 275 | + }, |
| 276 | + [Symbol.asyncIterator]() { |
| 277 | + return this; |
| 278 | + }, |
| 279 | + }; |
| 280 | +} |
0 commit comments