{"_id":"@bndynet/sse-parser","_rev":"5-ca5fb6d5c5ee4c37ce0e1d387cff9b07","name":"@bndynet/sse-parser","dist-tags":{"latest":"1.1.0"},"versions":{"0.1.0":{"name":"@bndynet/sse-parser","version":"0.1.0","keywords":["sse","server-sent-events","ndjson","streaming","openai","anthropic","gemini","ollama","parser"],"author":{"name":"Bendy Zhang"},"license":"MIT","_id":"@bndynet/sse-parser@0.1.0","maintainers":[{"name":"bndy","email":"zb@bndy.net"}],"homepage":"https://github.com/bndynet/sse-parser/tree/main/packages/sse-parser#readme","bugs":{"url":"https://github.com/bndynet/sse-parser/issues"},"dist":{"shasum":"52dd551919b949b41468b5b507fa029594dba1e6","tarball":"https://registry.npmjs.org/@bndynet/sse-parser/-/sse-parser-0.1.0.tgz","fileCount":8,"integrity":"sha512-oJrU57a4QcmShEJRZznuweaEgi8C2nmWVTPDqK03ZUrcpTDPkKee6uCoj4EYMVZcbAuCQmQDqG9d0Hn8V7WthA==","signatures":[{"sig":"MEQCICnqsHC5O5trYLYj6KbZf/QzoU8NjtBttC3hBmB8yjKcAiBKlJSgTDD7GGEGs0X1rEcTkQ+X4UcPiKrm06LSIOzf5Q==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bndynet%2fsse-parser@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":118091},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"55427fd76cf5a84860c14961665d39c74bcc7666","scripts":{"lint":"eslint \"src/**/*.ts\"","test":"npm run typecheck","build":"tsup","clean":"rm -rf dist","lint:fix":"eslint \"src/**/*.ts\" --fix","typecheck":"tsc --noEmit","prepublishOnly":"npm run clean && npm run lint && npm run test && npm run build"},"_npmUser":{"name":"bndy","email":"zb@bndy.net"},"repository":{"url":"git+https://github.com/bndynet/sse-parser.git","type":"git"},"_npmVersion":"11.9.0","description":"Zero-dependency SSE and NDJSON stream parser with AI API adapters (OpenAI, Anthropic, Gemini, Ollama)","directories":{},"_nodeVersion":"24.14.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","eslint":"^10.1.0","typescript":"^5.7.0","@typescript-eslint/parser":"^8.57.2","@typescript-eslint/eslint-plugin":"^8.57.2"},"_npmOperationalInternal":{"tmp":"tmp/sse-parser_0.1.0_1774525855131_0.7719026325443734","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@bndynet/sse-parser","version":"0.1.1","keywords":["sse","server-sent-events","ndjson","streaming","openai","anthropic","gemini","ollama","parser"],"author":{"name":"Bendy Zhang"},"license":"MIT","_id":"@bndynet/sse-parser@0.1.1","maintainers":[{"name":"bndy","email":"zb@bndy.net"}],"homepage":"https://github.com/bndynet/sse-parser/tree/main/packages/sse-parser#readme","bugs":{"url":"https://github.com/bndynet/sse-parser/issues"},"dist":{"shasum":"13ae72ba40db4bbc92b75c6fc21deecd7be047eb","tarball":"https://registry.npmjs.org/@bndynet/sse-parser/-/sse-parser-0.1.1.tgz","fileCount":8,"integrity":"sha512-Fi0+hKOGK6fgdFVn76KcBgn9BpQGkcLgspok1ChbAZTv9JzlzyAw+iDdysBCs5WJRymg5siZHJ08gwbe9XiAjQ==","signatures":[{"sig":"MEQCIDTWrNAn0HWgKxRGrQCoYrbo25nt2KTNaMllL7dULy2dAiANX1XFmED5kxvuO3Oo7AvO085xG0F2b9YLnAmMX79fAQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bndynet%2fsse-parser@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":118091},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"4daea652d6fde1611ba534a279aa0a949d34a472","scripts":{"lint":"eslint \"src/**/*.ts\"","test":"npm run typecheck","build":"tsup","clean":"rm -rf dist","lint:fix":"eslint \"src/**/*.ts\" --fix","typecheck":"tsc --noEmit","prepublishOnly":"npm run clean && npm run lint && npm run test && npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:711b1e01-ffc1-4a5e-b786-676e7da559da"}},"repository":{"url":"git+https://github.com/bndynet/sse-parser.git","type":"git"},"_npmVersion":"11.9.0","description":"Zero-dependency SSE and NDJSON stream parser with AI API adapters (OpenAI, Anthropic, Gemini, Ollama)","directories":{},"_nodeVersion":"24.14.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","eslint":"^10.1.0","typescript":"^5.7.0","@typescript-eslint/parser":"^8.57.2","@typescript-eslint/eslint-plugin":"^8.57.2"},"_npmOperationalInternal":{"tmp":"tmp/sse-parser_0.1.1_1774527802667_0.8948532396922162","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@bndynet/sse-parser","version":"0.1.2","keywords":["sse","server-sent-events","ndjson","streaming","openai","anthropic","gemini","ollama","parser"],"author":{"name":"Bendy Zhang"},"license":"MIT","_id":"@bndynet/sse-parser@0.1.2","maintainers":[{"name":"bndy","email":"zb@bndy.net"}],"homepage":"https://github.com/bndynet/sse-parser/tree/main/packages/sse-parser#readme","bugs":{"url":"https://github.com/bndynet/sse-parser/issues"},"dist":{"shasum":"07e9d93c23d4889a96f24e2dc03cfec5cab5f327","tarball":"https://registry.npmjs.org/@bndynet/sse-parser/-/sse-parser-0.1.2.tgz","fileCount":10,"integrity":"sha512-Lvqqmqj2U2pdpUUZ+fghaghSq9JwEGRJkT2M5YbhJ6WvJUfzQb7sNMDJ4csQWgwAFt1hmyu/3YJ415rWYHNGLw==","signatures":[{"sig":"MEYCIQDMNNWrIMUj3uLuZ2swwMUZKrjCZoCzMYFi42Nwx9nMLgIhAMvVOXxwFqzi6kkoDVYKnRMZKNFA1uGZ85oNzwoaf6/V","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bndynet%2fsse-parser@0.1.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":165662},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"029961797a44366b9d8cf06e9125649ca903cab8","scripts":{"lint":"eslint \"src/**/*.ts\"","test":"npm run typecheck","build":"tsup","clean":"rm -rf dist","lint:fix":"eslint \"src/**/*.ts\" --fix","typecheck":"tsc --noEmit","prepublishOnly":"npm run clean && npm run lint && npm run test && npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:711b1e01-ffc1-4a5e-b786-676e7da559da"}},"repository":{"url":"git+https://github.com/bndynet/sse-parser.git","type":"git"},"_npmVersion":"11.9.0","description":"Zero-dependency SSE and NDJSON stream parser with AI API adapters (OpenAI, Anthropic, Gemini, Ollama)","directories":{},"_nodeVersion":"24.14.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","eslint":"^10.1.0","typescript":"^5.7.0","@typescript-eslint/parser":"^8.57.2","@typescript-eslint/eslint-plugin":"^8.57.2"},"_npmOperationalInternal":{"tmp":"tmp/sse-parser_0.1.2_1774665904038_0.6483677063503641","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@bndynet/sse-parser","version":"1.0.0","keywords":["sse","server-sent-events","ndjson","streaming","openai","anthropic","gemini","ollama","parser"],"author":{"name":"Bendy Zhang"},"license":"MIT","_id":"@bndynet/sse-parser@1.0.0","maintainers":[{"name":"bndy","email":"zb@bndy.net"}],"homepage":"https://github.com/bndynet/sse-parser/tree/main/packages/sse-parser#readme","bugs":{"url":"https://github.com/bndynet/sse-parser/issues"},"dist":{"shasum":"301fb1cfbee1f1ee54d42ad27ce5c3f030b35be2","tarball":"https://registry.npmjs.org/@bndynet/sse-parser/-/sse-parser-1.0.0.tgz","fileCount":10,"integrity":"sha512-JduitepntoRP1OsWBe13QY1BfETH1o5y2pKQDX1NwH3aA6OnTqZ1k2aDn1E5yzylRAwxIlrYbnfNGfjFmsOFYg==","signatures":[{"sig":"MEUCIQCsgsUJE1RAW2MKy++/2L91sFXN30MUqVZqmtJ7zQK1wQIgIumy4JS9TOU+YGC7ak/IH6VL0OS8vPkui0zz5F/1R0o=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bndynet%2fsse-parser@1.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":250355},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"b52eec722d5cbbd305ec51515486742e6e3054ee","scripts":{"lint":"eslint \"src/**/*.ts\"","test":"npm run typecheck && vitest run","build":"tsup","clean":"rm -rf dist","lint:fix":"eslint \"src/**/*.ts\" --fix","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"npm run clean && npm run lint && npm run test && npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:711b1e01-ffc1-4a5e-b786-676e7da559da"}},"repository":{"url":"git+https://github.com/bndynet/sse-parser.git","type":"git"},"_npmVersion":"11.13.0","description":"Zero-dependency SSE and NDJSON stream parser with AI API adapters (OpenAI, Anthropic, Gemini, Ollama)","directories":{},"_nodeVersion":"24.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","eslint":"^10.1.0","vitest":"^4.1.7","typescript":"^5.7.0","@typescript-eslint/parser":"^8.57.2","@typescript-eslint/eslint-plugin":"^8.57.2"},"_npmOperationalInternal":{"tmp":"tmp/sse-parser_1.0.0_1780320935841_0.6894913253055712","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@bndynet/sse-parser","version":"1.1.0","description":"Zero-dependency SSE and NDJSON stream parser with AI API adapters (OpenAI, DeepSeek, Anthropic, Gemini, Ollama)","type":"module","author":{"name":"Bendy Zhang"},"repository":{"type":"git","url":"git+https://github.com/bndynet/sse-parser.git"},"bugs":{"url":"https://github.com/bndynet/sse-parser/issues"},"homepage":"https://github.com/bndynet/sse-parser/tree/main/packages/sse-parser#readme","engines":{"node":">=18"},"main":"dist/index.cjs","module":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"lint":"eslint \"src/**/*.ts\"","lint:fix":"eslint \"src/**/*.ts\" --fix","typecheck":"tsc --noEmit","test":"npm run typecheck && vitest run","test:watch":"vitest","build":"tsup","clean":"rm -rf dist","prepublishOnly":"npm run clean && npm run lint && npm run test && npm run build"},"publishConfig":{"access":"public"},"devDependencies":{"@typescript-eslint/eslint-plugin":"^8.57.2","@typescript-eslint/parser":"^8.57.2","eslint":"^10.1.0","tsup":"^8.5.1","typescript":"^5.7.0","vitest":"^4.1.7"},"keywords":["sse","server-sent-events","ndjson","streaming","openai","deepseek","anthropic","gemini","ollama","parser"],"license":"MIT","gitHead":"0b6ef0189c5a5e5da83d9b2e7d21f90ab6b4665d","_id":"@bndynet/sse-parser@1.1.0","_nodeVersion":"24.16.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-AtX3IUO1GF+ua8EBiZr0WXTplKPoKkW2ux1lbt2bHZFEuOzMZggMQMpNhWIw5zy5GrcT2hM5zYQKqTBw2BnrjA==","shasum":"b786f1afe1ae491b2ab38a482fa04cd6d6a7b9e4","tarball":"https://registry.npmjs.org/@bndynet/sse-parser/-/sse-parser-1.1.0.tgz","fileCount":10,"unpackedSize":273068,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bndynet%2fsse-parser@1.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIBsnu9TcAAZftus2S4L7BYfmJEmvSaZhSaRqLj1Piuq6AiEAiWb8/KEZ1I05QWgBArdwHHCXwC4r3Y4HPfWmIM0k7IU="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:711b1e01-ffc1-4a5e-b786-676e7da559da"}},"directories":{},"maintainers":[{"name":"bndy","email":"zb@bndy.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sse-parser_1.1.0_1781619947805_0.8181117098534922"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-26T11:50:55.027Z","modified":"2026-06-16T14:25:48.264Z","0.1.0":"2026-03-26T11:50:55.382Z","0.1.1":"2026-03-26T12:23:22.822Z","0.1.2":"2026-03-28T02:45:04.228Z","1.0.0":"2026-06-01T13:35:36.024Z","1.1.0":"2026-06-16T14:25:47.958Z"},"bugs":{"url":"https://github.com/bndynet/sse-parser/issues"},"author":{"name":"Bendy Zhang"},"license":"MIT","homepage":"https://github.com/bndynet/sse-parser/tree/main/packages/sse-parser#readme","keywords":["sse","server-sent-events","ndjson","streaming","openai","deepseek","anthropic","gemini","ollama","parser"],"repository":{"type":"git","url":"git+https://github.com/bndynet/sse-parser.git"},"description":"Zero-dependency SSE and NDJSON stream parser with AI API adapters (OpenAI, DeepSeek, Anthropic, Gemini, Ollama)","maintainers":[{"name":"bndy","email":"zb@bndy.net"}],"readme":"# @bndynet/sse-parser\n\nZero-dependency TypeScript SDK for parsing **Server-Sent Events (SSE)** and **NDJSON** streams, with built-in adapters for common AI APIs.\n\n## Features\n\n- WHATWG-spec-compliant SSE parser (CR/LF/CRLF, BOM, multi-line data, event/id/retry)\n- NDJSON parser for Ollama-style streams\n- Accepts a `fetch` Response, a raw `ReadableStream`, **or** any `AsyncIterable` of bytes/text — bridged to an `AsyncGenerator` with timeout and AbortSignal support\n- AI adapters that normalize vendor payloads into a unified `ChatStreamEvent`:\n  - **OpenAI** — Chat Completions (`/v1/chat/completions`) and the newer Responses API (`/v1/responses`)\n  - OpenAI-compatible: Azure, Groq, Together, vLLM, LiteLLM\n  - **DeepSeek** — OpenAI-format Chat Completions with thinking token support\n  - **Anthropic** (Claude Messages API)\n  - **Google Gemini**\n  - **Ollama**\n- Full TypeScript types, zero runtime dependencies\n\n## Install\n\n```bash\nnpm install @bndynet/sse-parser\n```\n\n## Quick Start\n\n### Unified entry (`chatStream`)\n\nIf you'd rather not import a per-vendor function, use `chatStream` and pass an\nexplicit `provider`:\n\n```typescript\nimport { chatStream } from '@bndynet/sse-parser';\n\nfor await (const event of chatStream(res, { provider: 'openai' })) {\n  if (event.type === 'text') process.stdout.write(event.content);\n}\n```\n\nProvider cheat sheet — pick the one matching the endpoint you call:\n\n| API / endpoint | `provider` |\n|---|---|\n| OpenAI Chat Completions (`/v1/chat/completions`), Azure, Groq, Together, vLLM, LiteLLM | `'openai'` |\n| OpenAI Responses (`/v1/responses`) | `'openai-responses'` |\n| DeepSeek OpenAI-format Chat Completions (`/chat/completions`) | `'deepseek'` |\n| Anthropic Messages (`/v1/messages`) | `'anthropic'` |\n| Google Gemini (`:streamGenerateContent?alt=sse`) | `'gemini'` |\n| Ollama (`/api/chat`, NDJSON) | `'ollama'` |\n\n`chatStream` accepts the same `StreamReaderOptions` (`timeoutMs`, `signal`,\n`doneSentinel`) as the individual adapters.\n\n### OpenAI\n\n```typescript\nimport { openaiStream } from '@bndynet/sse-parser';\n\nconst res = await fetch('https://api.openai.com/v1/chat/completions', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n    Authorization: `Bearer ${apiKey}`,\n  },\n  body: JSON.stringify({\n    model: 'gpt-4o',\n    messages: [{ role: 'user', content: 'Hello' }],\n    stream: true,\n  }),\n});\n\nfor await (const event of openaiStream(res)) {\n  switch (event.type) {\n    case 'text':\n      process.stdout.write(event.content);\n      break;\n    case 'error':\n      console.error(event.message);\n      break;\n    case 'done':\n      console.log('\\nUsage:', event.usage);\n      break;\n  }\n}\n```\n\n### DeepSeek\n\n```typescript\nimport { deepseekStream } from '@bndynet/sse-parser';\n\nconst res = await fetch('https://api.deepseek.com/chat/completions', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n    Authorization: `Bearer ${apiKey}`,\n  },\n  body: JSON.stringify({\n    model: 'deepseek-v4-pro',\n    messages: [{ role: 'user', content: 'Explain SSE' }],\n    stream: true,\n  }),\n});\n\nfor await (const event of deepseekStream(res)) {\n  if (event.type === 'reasoning') process.stderr.write(event.content);\n  if (event.type === 'text') process.stdout.write(event.content);\n  if (event.type === 'done') console.log('\\nUsage:', event.usage);\n}\n```\n\n`deepseekStream` parses DeepSeek's OpenAI-format SSE chunks, including\n`delta.reasoning_content`, streamed tool calls, cache hit/miss usage, and\nreasoning token usage.\n\n### OpenAI Responses API\n\n```typescript\nimport { openaiResponsesStream } from '@bndynet/sse-parser';\n\nconst res = await fetch('https://api.openai.com/v1/responses', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n    Authorization: `Bearer ${apiKey}`,\n  },\n  body: JSON.stringify({\n    model: 'gpt-4o',\n    input: 'Hello',\n    stream: true,\n  }),\n});\n\nfor await (const event of openaiResponsesStream(res)) {\n  if (event.type === 'text') process.stdout.write(event.content);\n  if (event.type === 'reasoning') process.stdout.write(event.content);\n}\n```\n\n### Anthropic\n\n```typescript\nimport { anthropicStream } from '@bndynet/sse-parser';\n\nconst res = await fetch('https://api.anthropic.com/v1/messages', { /* ... */ });\n\nfor await (const event of anthropicStream(res)) {\n  if (event.type === 'text') process.stdout.write(event.content);\n}\n```\n\n### Google Gemini\n\n```typescript\nimport { geminiStream } from '@bndynet/sse-parser';\n\nconst res = await fetch(\n  `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent?alt=sse&key=${apiKey}`,\n  { method: 'POST', /* ... */ }\n);\n\nfor await (const event of geminiStream(res)) {\n  if (event.type === 'text') process.stdout.write(event.content);\n}\n```\n\n### Ollama (NDJSON)\n\n```typescript\nimport { ollamaStream } from '@bndynet/sse-parser';\n\nconst res = await fetch('http://localhost:11434/api/chat', {\n  method: 'POST',\n  body: JSON.stringify({\n    model: 'llama3',\n    messages: [{ role: 'user', content: 'Hello' }],\n  }),\n});\n\nfor await (const event of ollamaStream(res)) {\n  if (event.type === 'text') process.stdout.write(event.content);\n}\n```\n\n### Low-level SSE parsing\n\n```typescript\nimport { readSSEStream } from '@bndynet/sse-parser';\n\nconst res = await fetch('/my-sse-endpoint');\n\nfor await (const sseEvent of readSSEStream(res)) {\n  console.log(sseEvent.event, sseEvent.data, sseEvent.id);\n}\n```\n\n## Input sources\n\nEvery reader and adapter (`readSSEStream`, `readNDJSONStream`, `openaiStream`,\n`deepseekStream`, `chatStream`, …) accepts a `StreamInput`:\n\n```typescript\ntype StreamInput =\n  | Response                              // fetch Response — HTTP status is validated\n  | ReadableStream<Uint8Array>            // raw Web stream (e.g. response.body, pipeThrough output)\n  | AsyncIterable<Uint8Array | string>;   // Node stream, file stream, child process, test stub…\n```\n\nA `fetch` Response is the common case in the browser. The other two unlock\nnon-fetch sources — useful in Angular (`HttpClient` doesn't hand you a\nResponse), SSR/Node (where the upstream may be a Node `http`/`undici` stream),\nand tests (feed an `async function*` instead of mocking a whole Response).\n\n> When you pass a `ReadableStream` or `AsyncIterable`, no HTTP status check is\n> performed (there's no transport metadata to inspect) — the caller is\n> responsible for any connection/status validation.\n\n```typescript\nimport { openaiStream } from '@bndynet/sse-parser';\n\n// 1. A raw ReadableStream (e.g. after a transform)\nconst stream = response.body!.pipeThrough(new DecompressionStream('gzip'));\nfor await (const ev of openaiStream(stream)) { /* ... */ }\n\n// 2. Any AsyncIterable — a Node response stream, or a test stub\nasync function* fake() {\n  yield 'data: {\"choices\":[{\"delta\":{\"content\":\"hi\"}}]}\\n\\n';\n  yield 'data: [DONE]\\n\\n';\n}\nfor await (const ev of openaiStream(fake())) { /* ... */ }\n```\n\n## Unified `ChatStreamEvent`\n\nAll AI adapters yield the same discriminated union:\n\n```typescript\ntype ChatStreamEvent =\n  | { type: 'text'; content: string; raw?: unknown }\n  | { type: 'reasoning'; content: string; raw?: unknown }\n  | { type: 'tool_call'; id: string; name: string; arguments: string; index?: number; raw?: unknown }\n  | { type: 'error'; message: string; code?: string; raw?: unknown }\n  | { type: 'done'; usage?: TokenUsage; finishReason?: string; raw?: unknown };\n```\n\nEvery event carries an optional `raw` — the vendor's parsed JSON chunk that\nproduced it — so you can read provider-specific fields (logprobs, citations,\nannotations, etc.) that the normalized shape omits. The terminal `done` event\nalso exposes the vendor's `finishReason` when available.\n\n### Reassembling streamed tool calls\n\nVendors stream a tool call's `id`/`name` once and its `arguments` across many\nchunks. Group fragments by `index` to rebuild each call:\n\n```typescript\nconst calls = new Map<number, { id: string; name: string; arguments: string }>();\n\nfor await (const event of openaiStream(res)) {\n  if (event.type === 'tool_call') {\n    const i = event.index ?? 0;\n    const call = calls.get(i) ?? { id: '', name: '', arguments: '' };\n    if (event.id) call.id = event.id;\n    if (event.name) call.name = event.name;\n    call.arguments += event.arguments;\n    calls.set(i, call);\n  }\n}\n// calls now holds the fully reassembled tool calls\n```\n\n### `done` is emitted exactly once\n\nEvery adapter yields a single terminal `done` event when the stream completes\nsuccessfully, carrying the final `usage` when the vendor provides it. (Errors\nsurface as non-fatal `error` events or, for connection/timeout failures, throw.)\n\n### Token usage fields\n\n`usage` normalizes common token counters across providers. The core fields are\n`promptTokens`, `completionTokens`, and `totalTokens`; adapters also fill\noptional breakdown fields when the provider reports them:\n\n```typescript\ninterface TokenUsage {\n  promptTokens?: number;\n  completionTokens?: number;\n  totalTokens?: number;\n  cachedPromptTokens?: number;\n  uncachedPromptTokens?: number;\n  cacheCreationPromptTokens?: number;\n  reasoningTokens?: number;\n  toolUsePromptTokens?: number;\n}\n```\n\nProvider-specific payloads are still available on `event.raw` for callers that\nneed exact vendor field names.\n\n## Error Handling\n\nThe SDK provides a typed error hierarchy — all extend from `SSEError`:\n\n```typescript\nimport {\n  SSEError,            // base class\n  SSEParseError,       // malformed SSE line or invalid JSON\n  SSEConnectionError,  // network failure, HTTP non-2xx, abort\n  SSETimeoutError,     // idle timeout exceeded\n} from '@bndynet/sse-parser';\n```\n\n### Fatal vs non-fatal errors\n\n| Scenario | Error type | Fatal? | Behavior |\n|---|---|---|---|\n| HTTP non-2xx response | `SSEConnectionError` | Yes | Thrown before any event is yielded |\n| Network disconnection | `SSEConnectionError` | Yes | Generator throws, `for await` exits |\n| `AbortSignal` aborted | `SSEConnectionError` | Yes | Generator throws, `for await` exits |\n| Idle timeout exceeded | `SSETimeoutError` | Yes | Generator throws, `for await` exits |\n| Bad JSON in one `data:` line | — | No | Adapter yields `{ type: 'error' }`, stream continues |\n| API-level error (e.g. rate limit) | — | No | Adapter yields `{ type: 'error', code }`, stream continues |\n\n### Catching fatal errors\n\n```typescript\nimport {\n  openaiStream,\n  SSEConnectionError,\n  SSETimeoutError,\n} from '@bndynet/sse-parser';\n\ntry {\n  for await (const event of openaiStream(res)) {\n    if (event.type === 'error') {\n      // Non-fatal: bad JSON or API error — stream continues\n      console.warn('API error:', event.message, event.code);\n    }\n    if (event.type === 'text') {\n      process.stdout.write(event.content);\n    }\n  }\n} catch (err) {\n  if (err instanceof SSETimeoutError) {\n    console.error(`Stream timed out after ${err.timeoutMs}ms`);\n  } else if (err instanceof SSEConnectionError) {\n    console.error(`Connection failed (HTTP ${err.status}):`, err.message);\n  }\n}\n```\n\n### `SSEParseError` details\n\nWhen the low-level parser encounters a malformed line, it creates an `SSEParseError` with the raw line attached:\n\n```typescript\nimport { SSEParser, SSEParseError } from '@bndynet/sse-parser';\n\nconst parser = new SSEParser({\n  onEvent(evt) { /* ... */ },\n  onError(err) {\n    if (err instanceof SSEParseError) {\n      console.warn('Bad line:', err.line, '—', err.message);\n    }\n  },\n});\n```\n\n### Reconnection hints (`retry:`)\n\nA `retry:` field does not produce a standalone event. Its reconnection-time\nhint (ms) is delivered via the optional `onRetry` callback and is also attached\nto the next dispatched event's `retry` field:\n\n```typescript\nconst parser = new SSEParser({\n  onEvent(evt) {\n    if (evt.retry !== undefined) console.log('server suggests retry in', evt.retry, 'ms');\n  },\n  onRetry(ms) { /* update your reconnect backoff */ },\n});\n```\n\n## Options\n\nAll stream readers and adapters accept an optional `StreamReaderOptions`:\n\n```typescript\ninterface StreamReaderOptions {\n  /** Idle timeout in ms. Default: 60000. Set 0 to disable. */\n  timeoutMs?: number;\n  /** AbortSignal for external cancellation. */\n  signal?: AbortSignal;\n  /** Sentinel that ends the stream. Default: \"[DONE]\". Set null to disable. */\n  doneSentinel?: string | null;\n}\n```\n\n### AbortController cancellation\n\n```typescript\nconst controller = new AbortController();\nsetTimeout(() => controller.abort(), 10_000); // cancel after 10s\n\ntry {\n  for await (const ev of openaiStream(res, { signal: controller.signal })) {\n    // ...\n  }\n} catch (err) {\n  // SSEConnectionError with message \"Stream aborted by caller\"\n}\n```\n\n### Custom timeout\n\n```typescript\n// 2 minute timeout\nfor await (const ev of openaiStream(res, { timeoutMs: 120_000 })) { /* ... */ }\n\n// No timeout at all\nfor await (const ev of openaiStream(res, { timeoutMs: 0 })) { /* ... */ }\n```\n\n### Custom done sentinel\n\n```typescript\n// Disable [DONE] detection (e.g. for Anthropic / Gemini — adapters do this automatically)\nfor await (const sse of readSSEStream(res, { doneSentinel: null })) { /* ... */ }\n\n// Use a different sentinel\nfor await (const sse of readSSEStream(res, { doneSentinel: '[END]' })) { /* ... */ }\n```\n\n## API\n\n| Export | Description |\n|---|---|\n| `SSEParser` | Low-level push parser — call `feed(chunk)` |\n| `NDJSONParser` | Low-level NDJSON push parser |\n| `readSSEStream(input, opts?)` | `AsyncGenerator<SSEEvent>` from a `StreamInput` |\n| `readNDJSONStream(input, opts?)` | `AsyncGenerator<T>` from a `StreamInput` |\n| `chatStream(input, { provider, ...opts })` | Unified entry — dispatches to the adapter for `provider` |\n| `openaiStream(res, opts?)` | OpenAI Chat Completions adapter → `AsyncGenerator<ChatStreamEvent>` |\n| `openaiResponsesStream(res, opts?)` | OpenAI Responses API adapter |\n| `deepseekStream(res, opts?)` | DeepSeek OpenAI-format Chat Completions adapter |\n| `anthropicStream(res, opts?)` | Anthropic adapter |\n| `geminiStream(res, opts?)` | Gemini adapter |\n| `ollamaStream(res, opts?)` | Ollama adapter |\n| `SSEError` | Base error class |\n| `SSEParseError` | Malformed SSE / JSON — has `.line` |\n| `SSEConnectionError` | Network / HTTP / abort — has `.status?` |\n| `SSETimeoutError` | Idle timeout — has `.timeoutMs` |\n\n## License\n\nMIT\n","readmeFilename":"README.md"}