@fgv/ts-extras 5.1.0-15 → 5.1.0-17

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.
Files changed (53) hide show
  1. package/dist/index.browser.js +2 -1
  2. package/dist/packlets/ai-assist/apiClient.js +570 -58
  3. package/dist/packlets/ai-assist/chatRequestBuilders.js +180 -0
  4. package/dist/packlets/ai-assist/index.js +4 -3
  5. package/dist/packlets/ai-assist/model.js +20 -3
  6. package/dist/packlets/ai-assist/registry.js +66 -10
  7. package/dist/packlets/ai-assist/sseParser.js +122 -0
  8. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +192 -0
  9. package/dist/packlets/ai-assist/streamingAdapters/common.js +77 -0
  10. package/dist/packlets/ai-assist/streamingAdapters/gemini.js +160 -0
  11. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +149 -0
  12. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +163 -0
  13. package/dist/packlets/ai-assist/streamingAdapters/proxy.js +157 -0
  14. package/dist/packlets/ai-assist/streamingClient.js +88 -0
  15. package/dist/packlets/conversion/converters.js +1 -1
  16. package/dist/packlets/crypto-utils/keystore/keyStore.js +74 -42
  17. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +2 -2
  18. package/dist/ts-extras.d.ts +531 -5
  19. package/lib/index.browser.d.ts +2 -1
  20. package/lib/index.browser.js +3 -1
  21. package/lib/packlets/ai-assist/apiClient.d.ts +103 -1
  22. package/lib/packlets/ai-assist/apiClient.js +574 -58
  23. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +89 -0
  24. package/lib/packlets/ai-assist/chatRequestBuilders.js +189 -0
  25. package/lib/packlets/ai-assist/index.d.ts +4 -3
  26. package/lib/packlets/ai-assist/index.js +10 -1
  27. package/lib/packlets/ai-assist/model.d.ts +271 -2
  28. package/lib/packlets/ai-assist/model.js +21 -3
  29. package/lib/packlets/ai-assist/registry.d.ts +10 -1
  30. package/lib/packlets/ai-assist/registry.js +67 -11
  31. package/lib/packlets/ai-assist/sseParser.d.ts +45 -0
  32. package/lib/packlets/ai-assist/sseParser.js +127 -0
  33. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +18 -0
  34. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +195 -0
  35. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +71 -0
  36. package/lib/packlets/ai-assist/streamingAdapters/common.js +81 -0
  37. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts +19 -0
  38. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +163 -0
  39. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +18 -0
  40. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +152 -0
  41. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +19 -0
  42. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +166 -0
  43. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts +34 -0
  44. package/lib/packlets/ai-assist/streamingAdapters/proxy.js +160 -0
  45. package/lib/packlets/ai-assist/streamingClient.d.ts +33 -0
  46. package/lib/packlets/ai-assist/streamingClient.js +93 -0
  47. package/lib/packlets/conversion/converters.d.ts +1 -1
  48. package/lib/packlets/conversion/converters.js +1 -1
  49. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +19 -0
  50. package/lib/packlets/crypto-utils/keystore/keyStore.js +74 -42
  51. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +2 -2
  52. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +2 -2
  53. package/package.json +7 -7
@@ -0,0 +1,180 @@
1
+ // Copyright (c) 2026 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ /**
21
+ * Per-format chat request shape builders. Shared between the synchronous
22
+ * (`apiClient.ts`) and streaming (`streamingClient.ts`) paths so the wire
23
+ * shapes stay consistent.
24
+ *
25
+ * @packageDocumentation
26
+ */
27
+ import { toDataUrl } from './model';
28
+ /**
29
+ * Builds the messages array from prompt + optional head/tail messages.
30
+ * The caller supplies the user content (string for text-only, parts array
31
+ * for vision prompts) since the parts shape differs by format.
32
+ *
33
+ * @internal
34
+ */
35
+ export function buildMessages(systemPrompt, userContent, options) {
36
+ const messages = [
37
+ { role: 'system', content: systemPrompt }
38
+ ];
39
+ if (options === null || options === void 0 ? void 0 : options.head) {
40
+ for (const msg of options.head) {
41
+ messages.push({ role: msg.role, content: msg.content });
42
+ }
43
+ }
44
+ messages.push({ role: 'user', content: userContent });
45
+ if (options === null || options === void 0 ? void 0 : options.tail) {
46
+ for (const msg of options.tail) {
47
+ messages.push({ role: msg.role, content: msg.content });
48
+ }
49
+ }
50
+ return messages;
51
+ }
52
+ /**
53
+ * Builds the user content for OpenAI Chat Completions when attachments are
54
+ * present. Returns a string when there are no attachments.
55
+ *
56
+ * @internal
57
+ */
58
+ export function buildOpenAiChatUserContent(prompt) {
59
+ if (prompt.attachments.length === 0) {
60
+ return prompt.user;
61
+ }
62
+ return [
63
+ { type: 'text', text: prompt.user },
64
+ ...prompt.attachments.map((att) => ({
65
+ type: 'image_url',
66
+ image_url: Object.assign({ url: toDataUrl(att) }, (att.detail !== undefined ? { detail: att.detail } : {}))
67
+ }))
68
+ ];
69
+ }
70
+ /**
71
+ * Builds the user content for OpenAI / xAI Responses API when attachments
72
+ * are present. Responses API uses `input_text` / `input_image` part types,
73
+ * distinct from Chat Completions' `text` / `image_url`.
74
+ *
75
+ * @internal
76
+ */
77
+ export function buildOpenAiResponsesUserContent(prompt) {
78
+ if (prompt.attachments.length === 0) {
79
+ return prompt.user;
80
+ }
81
+ return [
82
+ { type: 'input_text', text: prompt.user },
83
+ ...prompt.attachments.map((att) => (Object.assign({ type: 'input_image', image_url: toDataUrl(att) }, (att.detail !== undefined ? { detail: att.detail } : {}))))
84
+ ];
85
+ }
86
+ /**
87
+ * Builds the user-message content for Anthropic when attachments are present.
88
+ *
89
+ * @internal
90
+ */
91
+ export function buildAnthropicUserContent(prompt) {
92
+ if (prompt.attachments.length === 0) {
93
+ return prompt.user;
94
+ }
95
+ return [
96
+ { type: 'text', text: prompt.user },
97
+ ...prompt.attachments.map((att) => ({
98
+ type: 'image',
99
+ source: {
100
+ type: 'base64',
101
+ media_type: att.mimeType,
102
+ data: att.base64
103
+ }
104
+ }))
105
+ ];
106
+ }
107
+ /**
108
+ * Builds the Gemini `parts` array for the user turn, including any image
109
+ * attachments as `inlineData` parts.
110
+ *
111
+ * @internal
112
+ */
113
+ export function buildGeminiUserParts(prompt) {
114
+ const parts = [{ text: prompt.user }];
115
+ for (const att of prompt.attachments) {
116
+ parts.push({ inlineData: { mimeType: att.mimeType, data: att.base64 } });
117
+ }
118
+ return parts;
119
+ }
120
+ /**
121
+ * Builds the Anthropic messages array, weaving any `head` messages between
122
+ * implicit system + the prompt's user message and appending `tail` messages
123
+ * after. System messages are filtered out (Anthropic uses a top-level system
124
+ * field).
125
+ *
126
+ * @internal
127
+ */
128
+ export function buildAnthropicMessages(prompt, options) {
129
+ const messages = [];
130
+ if (options === null || options === void 0 ? void 0 : options.head) {
131
+ for (const msg of options.head) {
132
+ if (msg.role !== 'system') {
133
+ messages.push({ role: msg.role, content: msg.content });
134
+ }
135
+ }
136
+ }
137
+ messages.push({ role: 'user', content: buildAnthropicUserContent(prompt) });
138
+ if (options === null || options === void 0 ? void 0 : options.tail) {
139
+ for (const msg of options.tail) {
140
+ if (msg.role !== 'system') {
141
+ messages.push({ role: msg.role, content: msg.content });
142
+ }
143
+ }
144
+ }
145
+ return messages;
146
+ }
147
+ /**
148
+ * Builds the Gemini `contents` array, weaving any `head` messages before the
149
+ * prompt's user parts and appending `tail` messages after. System messages
150
+ * are filtered out (Gemini uses a top-level systemInstruction field) and
151
+ * assistant roles are mapped to Gemini's `model` role.
152
+ *
153
+ * @internal
154
+ */
155
+ export function buildGeminiContents(prompt, options) {
156
+ const contents = [];
157
+ if (options === null || options === void 0 ? void 0 : options.head) {
158
+ for (const msg of options.head) {
159
+ if (msg.role !== 'system') {
160
+ contents.push({
161
+ role: msg.role === 'assistant' ? 'model' : msg.role,
162
+ parts: [{ text: msg.content }]
163
+ });
164
+ }
165
+ }
166
+ }
167
+ contents.push({ role: 'user', parts: buildGeminiUserParts(prompt) });
168
+ if (options === null || options === void 0 ? void 0 : options.tail) {
169
+ for (const msg of options.tail) {
170
+ if (msg.role !== 'system') {
171
+ contents.push({
172
+ role: msg.role === 'assistant' ? 'model' : msg.role,
173
+ parts: [{ text: msg.content }]
174
+ });
175
+ }
176
+ }
177
+ }
178
+ return contents;
179
+ }
180
+ //# sourceMappingURL=chatRequestBuilders.js.map
@@ -2,9 +2,10 @@
2
2
  * AI assist packlet - provider registry, prompt class, settings, and API client.
3
3
  * @packageDocumentation
4
4
  */
5
- export { AiPrompt, DEFAULT_AI_ASSIST, allModelSpecKeys, MODEL_SPEC_BASE_KEY, resolveModel } from './model';
6
- export { allProviderIds, getProviderDescriptors, getProviderDescriptor } from './registry';
7
- export { callProviderCompletion, callProxiedCompletion } from './apiClient';
5
+ export { AiPrompt, DEFAULT_AI_ASSIST, allModelSpecKeys, MODEL_SPEC_BASE_KEY, resolveModel, toDataUrl } from './model';
6
+ export { allProviderIds, getProviderDescriptors, getProviderDescriptor, DEFAULT_MODEL_CAPABILITY_CONFIG } from './registry';
7
+ export { callProviderCompletion, callProxiedCompletion, callProviderImageGeneration, callProxiedImageGeneration, callProviderListModels, callProxiedListModels } from './apiClient';
8
+ export { callProviderCompletionStream, callProxiedCompletionStream } from './streamingClient';
8
9
  export { aiProviderId, aiServerToolType, aiWebSearchToolConfig, aiServerToolConfig, aiToolEnablement, aiAssistProviderConfig, aiAssistSettings, modelSpecKey, modelSpec } from './converters';
9
10
  export { resolveEffectiveTools } from './toolFormats';
10
11
  //# sourceMappingURL=index.js.map
@@ -17,6 +17,15 @@
17
17
  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
18
  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
19
  // SOFTWARE.
20
+ /**
21
+ * Formats an {@link IAiImageData} as a `data:` URL suitable for browser display.
22
+ * @param image - The image to format
23
+ * @returns A `data:<mime>;base64,<data>` URL string
24
+ * @public
25
+ */
26
+ export function toDataUrl(image) {
27
+ return `data:${image.mimeType};base64,${image.base64}`;
28
+ }
20
29
  // ============================================================================
21
30
  // AiPrompt
22
31
  // ============================================================================
@@ -26,13 +35,21 @@
26
35
  * @public
27
36
  */
28
37
  export class AiPrompt {
29
- constructor(user, system) {
38
+ constructor(user, system, attachments) {
30
39
  this.system = system;
31
40
  this.user = user;
41
+ this.attachments = attachments !== null && attachments !== void 0 ? attachments : [];
32
42
  }
33
- /** Combined single-string version (user + system joined) for copy/paste. */
43
+ /**
44
+ * Combined single-string version (user + system joined) for copy/paste.
45
+ * When attachments are present, includes a sentinel noting they aren't
46
+ * part of the copied text.
47
+ */
34
48
  get combined() {
35
- return `${this.user}\n\n${this.system}`;
49
+ const sentinel = this.attachments.length > 0
50
+ ? `\n\n[${this.attachments.length} image attachment(s) — not included in copied text]`
51
+ : '';
52
+ return `${this.user}${sentinel}\n\n${this.system}`;
36
53
  }
37
54
  }
38
55
  /**
@@ -39,7 +39,9 @@ const BUILTIN_PROVIDERS = [
39
39
  baseUrl: '',
40
40
  defaultModel: '',
41
41
  supportedTools: [],
42
- corsRestricted: false
42
+ corsRestricted: false,
43
+ streamingCorsRestricted: false,
44
+ acceptsImageInput: false
43
45
  },
44
46
  {
45
47
  id: 'anthropic',
@@ -50,7 +52,9 @@ const BUILTIN_PROVIDERS = [
50
52
  baseUrl: 'https://api.anthropic.com/v1',
51
53
  defaultModel: 'claude-sonnet-4-5-20250929',
52
54
  supportedTools: ['web_search'],
53
- corsRestricted: false
55
+ corsRestricted: false,
56
+ streamingCorsRestricted: false,
57
+ acceptsImageInput: true
54
58
  },
55
59
  {
56
60
  id: 'google-gemini',
@@ -59,9 +63,12 @@ const BUILTIN_PROVIDERS = [
59
63
  needsSecret: true,
60
64
  apiFormat: 'gemini',
61
65
  baseUrl: 'https://generativelanguage.googleapis.com/v1beta',
62
- defaultModel: 'gemini-2.5-flash',
66
+ defaultModel: { base: 'gemini-2.5-flash', image: 'imagen-3.0-generate-002' },
63
67
  supportedTools: ['web_search'],
64
- corsRestricted: false
68
+ corsRestricted: false,
69
+ streamingCorsRestricted: false,
70
+ acceptsImageInput: true,
71
+ imageApiFormat: 'gemini-imagen'
65
72
  },
66
73
  {
67
74
  id: 'groq',
@@ -72,7 +79,9 @@ const BUILTIN_PROVIDERS = [
72
79
  baseUrl: 'https://api.groq.com/openai/v1',
73
80
  defaultModel: 'llama-3.3-70b-versatile',
74
81
  supportedTools: [],
75
- corsRestricted: false
82
+ corsRestricted: false,
83
+ streamingCorsRestricted: false,
84
+ acceptsImageInput: false
76
85
  },
77
86
  {
78
87
  id: 'mistral',
@@ -83,7 +92,9 @@ const BUILTIN_PROVIDERS = [
83
92
  baseUrl: 'https://api.mistral.ai/v1',
84
93
  defaultModel: 'mistral-large-latest',
85
94
  supportedTools: [],
86
- corsRestricted: false
95
+ corsRestricted: false,
96
+ streamingCorsRestricted: false,
97
+ acceptsImageInput: false
87
98
  },
88
99
  {
89
100
  id: 'openai',
@@ -92,9 +103,12 @@ const BUILTIN_PROVIDERS = [
92
103
  needsSecret: true,
93
104
  apiFormat: 'openai',
94
105
  baseUrl: 'https://api.openai.com/v1',
95
- defaultModel: 'gpt-4o',
106
+ defaultModel: { base: 'gpt-4o', image: 'dall-e-3' },
96
107
  supportedTools: ['web_search'],
97
- corsRestricted: false
108
+ corsRestricted: false,
109
+ streamingCorsRestricted: false,
110
+ acceptsImageInput: true,
111
+ imageApiFormat: 'openai-images'
98
112
  },
99
113
  {
100
114
  id: 'xai-grok',
@@ -103,9 +117,16 @@ const BUILTIN_PROVIDERS = [
103
117
  needsSecret: true,
104
118
  apiFormat: 'openai',
105
119
  baseUrl: 'https://api.x.ai/v1',
106
- defaultModel: { base: 'grok-4-1-fast', tools: 'grok-4-1-fast-reasoning' },
120
+ defaultModel: {
121
+ base: 'grok-4-1-fast',
122
+ tools: 'grok-4-1-fast-reasoning',
123
+ image: 'grok-2-image-1212'
124
+ },
107
125
  supportedTools: ['web_search'],
108
- corsRestricted: true
126
+ corsRestricted: true,
127
+ streamingCorsRestricted: true,
128
+ acceptsImageInput: true,
129
+ imageApiFormat: 'xai-images'
109
130
  }
110
131
  ];
111
132
  /**
@@ -142,4 +163,39 @@ export function getProviderDescriptor(id) {
142
163
  }
143
164
  return succeed(descriptor);
144
165
  }
166
+ // ============================================================================
167
+ // Default model capability config
168
+ // ============================================================================
169
+ /**
170
+ * Default capability config used by `callProviderListModels` when callers
171
+ * don't supply their own. Patterns are intentionally narrow — false
172
+ * positives are worse than missing a model. Caller can override per call
173
+ * via {@link IProviderListModelsParams.capabilityConfig}.
174
+ *
175
+ * @public
176
+ */
177
+ export const DEFAULT_MODEL_CAPABILITY_CONFIG = {
178
+ perProvider: {
179
+ openai: [
180
+ { idPattern: /^dall-e/, capabilities: ['image-generation'] },
181
+ { idPattern: /^gpt-image/, capabilities: ['image-generation'] },
182
+ { idPattern: /^gpt-4/, capabilities: ['chat', 'tools', 'vision'] },
183
+ { idPattern: /^gpt-3\.5/, capabilities: ['chat'] },
184
+ { idPattern: /^o\d/, capabilities: ['chat', 'tools'] }
185
+ ],
186
+ 'xai-grok': [
187
+ { idPattern: /-image/, capabilities: ['image-generation'] },
188
+ { idPattern: /^grok-4/, capabilities: ['chat', 'tools', 'vision'] },
189
+ { idPattern: /^grok-3/, capabilities: ['chat', 'tools'] },
190
+ { idPattern: /^grok-2/, capabilities: ['chat', 'vision'] }
191
+ ],
192
+ 'google-gemini': [
193
+ { idPattern: /^imagen/, capabilities: ['image-generation'] },
194
+ { idPattern: /^gemini-/, capabilities: ['chat', 'tools', 'vision'] }
195
+ ],
196
+ anthropic: [{ idPattern: /^claude-/, capabilities: ['chat', 'tools', 'vision'] }],
197
+ groq: [{ idPattern: /./, capabilities: ['chat'] }],
198
+ mistral: [{ idPattern: /./, capabilities: ['chat'] }]
199
+ }
200
+ };
145
201
  //# sourceMappingURL=registry.js.map
@@ -0,0 +1,122 @@
1
+ // Copyright (c) 2026 Erik Fortune
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+ var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
21
+ var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
22
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
23
+ var g = generator.apply(thisArg, _arguments || []), i, q = [];
24
+ return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
25
+ function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
26
+ function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
27
+ function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
28
+ function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
29
+ function fulfill(value) { resume("next", value); }
30
+ function reject(value) { resume("throw", value); }
31
+ function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
32
+ };
33
+ /**
34
+ * Parses a single SSE message (the text between blank-line separators) into
35
+ * an {@link ISseEvent}. Returns undefined for messages with no `data:` lines
36
+ * (comments, heartbeats).
37
+ *
38
+ * @internal
39
+ */
40
+ export function parseSseEvent(chunk) {
41
+ let event;
42
+ const dataLines = [];
43
+ for (const line of chunk.split('\n')) {
44
+ if (line.startsWith('event:')) {
45
+ event = line.slice(6).trim();
46
+ }
47
+ else if (line.startsWith('data:')) {
48
+ // Per the SSE spec the value starts after the colon, with one optional leading space stripped.
49
+ const value = line.slice(5);
50
+ dataLines.push(value.startsWith(' ') ? value.slice(1) : value);
51
+ }
52
+ }
53
+ if (dataLines.length === 0) {
54
+ return undefined;
55
+ }
56
+ return event !== undefined ? { event, data: dataLines.join('\n') } : { data: dataLines.join('\n') };
57
+ }
58
+ /**
59
+ * Reads an SSE response body and yields parsed events. Buffers across read()
60
+ * boundaries so a message split mid-chunk still parses cleanly. Terminates
61
+ * when the stream closes (normal EOF or aborted fetch).
62
+ *
63
+ * @internal
64
+ */
65
+ export function readSseEvents(body) {
66
+ return __asyncGenerator(this, arguments, function* readSseEvents_1() {
67
+ var _a;
68
+ const reader = body.getReader();
69
+ const decoder = new TextDecoder();
70
+ let buffer = '';
71
+ try {
72
+ let streaming = true;
73
+ while (streaming) {
74
+ const { value, done } = yield __await(reader.read());
75
+ if (done) {
76
+ streaming = false;
77
+ if (buffer.length > 0) {
78
+ const tail = parseSseEvent(buffer.replace(/\r\n/g, '\n'));
79
+ if (tail) {
80
+ yield yield __await(tail);
81
+ }
82
+ }
83
+ break;
84
+ }
85
+ buffer += decoder.decode(value, { stream: true });
86
+ // SSE messages are separated by a blank line; some servers use \r\n.
87
+ const normalized = buffer.replace(/\r\n/g, '\n');
88
+ const parts = normalized.split('\n\n');
89
+ // Last element is the partial chunk (no terminating blank line yet); buffer it.
90
+ buffer = (_a = parts.pop()) !== null && _a !== void 0 ? _a : '';
91
+ for (const chunk of parts) {
92
+ const event = parseSseEvent(chunk);
93
+ if (event) {
94
+ yield yield __await(event);
95
+ }
96
+ }
97
+ }
98
+ }
99
+ finally {
100
+ reader.releaseLock();
101
+ }
102
+ });
103
+ }
104
+ /**
105
+ * Parses the `data` payload of an SSE event as JSON. Returns undefined for
106
+ * the OpenAI `[DONE]` sentinel and for any payload that fails to parse —
107
+ * adapters treat both as "skip this event."
108
+ *
109
+ * @internal
110
+ */
111
+ export function parseSseEventJson(data) {
112
+ if (data === '[DONE]') {
113
+ return undefined;
114
+ }
115
+ try {
116
+ return JSON.parse(data);
117
+ }
118
+ catch (_a) {
119
+ return undefined;
120
+ }
121
+ }
122
+ //# sourceMappingURL=sseParser.js.map