@animalabs/membrane 0.5.84 → 0.5.85

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 (45) hide show
  1. package/dist/formatters/openai-responses.js +1 -1
  2. package/dist/formatters/openai-responses.js.map +1 -1
  3. package/dist/membrane.d.ts +3 -1
  4. package/dist/membrane.d.ts.map +1 -1
  5. package/dist/membrane.js +6 -2
  6. package/dist/membrane.js.map +1 -1
  7. package/dist/providers/anthropic.d.ts +11 -1
  8. package/dist/providers/anthropic.d.ts.map +1 -1
  9. package/dist/providers/anthropic.js +67 -10
  10. package/dist/providers/anthropic.js.map +1 -1
  11. package/dist/providers/credentials.d.ts +20 -0
  12. package/dist/providers/credentials.d.ts.map +1 -0
  13. package/dist/providers/credentials.js +53 -0
  14. package/dist/providers/credentials.js.map +1 -0
  15. package/dist/providers/index.d.ts +1 -0
  16. package/dist/providers/index.d.ts.map +1 -1
  17. package/dist/providers/openai-responses-api.d.ts +22 -4
  18. package/dist/providers/openai-responses-api.d.ts.map +1 -1
  19. package/dist/providers/openai-responses-api.js +107 -42
  20. package/dist/providers/openai-responses-api.js.map +1 -1
  21. package/dist/providers/openai-responses.d.ts +2 -0
  22. package/dist/providers/openai-responses.d.ts.map +1 -1
  23. package/dist/providers/openai-responses.js +16 -7
  24. package/dist/providers/openai-responses.js.map +1 -1
  25. package/dist/providers/responses-input.d.ts +11 -0
  26. package/dist/providers/responses-input.d.ts.map +1 -0
  27. package/dist/providers/responses-input.js +128 -0
  28. package/dist/providers/responses-input.js.map +1 -0
  29. package/dist/providers/utils.d.ts +7 -3
  30. package/dist/providers/utils.d.ts.map +1 -1
  31. package/dist/providers/utils.js +28 -3
  32. package/dist/providers/utils.js.map +1 -1
  33. package/dist/types/provider.d.ts +4 -0
  34. package/dist/types/provider.d.ts.map +1 -1
  35. package/package.json +1 -1
  36. package/src/formatters/openai-responses.ts +1 -1
  37. package/src/membrane.ts +6 -2
  38. package/src/providers/anthropic.ts +79 -13
  39. package/src/providers/credentials.ts +77 -0
  40. package/src/providers/index.ts +2 -0
  41. package/src/providers/openai-responses-api.ts +118 -43
  42. package/src/providers/openai-responses.ts +15 -6
  43. package/src/providers/responses-input.ts +131 -0
  44. package/src/providers/utils.ts +24 -3
  45. package/src/types/provider.ts +5 -0
@@ -9,6 +9,9 @@
9
9
  * exposes the response's ordered output array verbatim for the next turn.
10
10
  */
11
11
 
12
+ import { normalizeResponsesInput } from './responses-input.js';
13
+ import { fetchWithCredentials, type CredentialResolver } from './credentials.js';
14
+
12
15
  import type {
13
16
  ContentBlock,
14
17
  ProviderAdapter,
@@ -114,7 +117,16 @@ export interface OpenAIResponsesAPIProviderResponse extends Omit<ProviderRespons
114
117
  export interface OpenAIResponsesAPIAdapterConfig {
115
118
  /** API key (defaults to OPENAI_API_KEY). */
116
119
  apiKey?: string;
117
- /** API base URL (default: https://api.openai.com/v1). */
120
+ /** Resolve a bearer token and associated headers for each request/401 retry.
121
+ * Overrides apiKey; acquiring and persisting credentials belongs to the caller. */
122
+ credentials?: CredentialResolver;
123
+ /** ChatGPT subscription transport requires credentials and always uses SSE. */
124
+ mode?: 'api' | 'subscription';
125
+ /** Request priority service in subscription mode (default: false). */
126
+ fastMode?: boolean;
127
+ /** Called once if a subscription response reports a non-priority tier. */
128
+ onFastModeFallback?: (serviceTier: string) => void;
129
+ /** API base URL (defaults to the selected mode's endpoint). */
118
130
  baseURL?: string;
119
131
  /** Optional OpenAI organization ID. */
120
132
  organization?: string;
@@ -131,7 +143,7 @@ export interface OpenAIResponsesAPIAdapterConfig {
131
143
  // ============================================================================
132
144
 
133
145
  export class OpenAIResponsesAPIAdapter implements ProviderAdapter {
134
- readonly name = 'openai-responses-api';
146
+ readonly name: string = 'openai-responses-api';
135
147
 
136
148
  /**
137
149
  * Reads `usage.input_tokens_details.cached_tokens` from OpenAI's account-wide
@@ -144,6 +156,12 @@ export class OpenAIResponsesAPIAdapter implements ProviderAdapter {
144
156
  readonly usageCacheConvention = 'cache-inclusive' as const;
145
157
 
146
158
  private readonly apiKey: string;
159
+ private readonly credentials?: CredentialResolver;
160
+ private readonly subscription: boolean;
161
+ readonly requiresNativeResponsesInput: boolean;
162
+ private fastMode: boolean;
163
+ private readonly onFastModeFallback?: (serviceTier: string) => void;
164
+ private warnedFastFallback = false;
147
165
  private readonly baseURL: string;
148
166
  private readonly organization?: string;
149
167
  private readonly project?: string;
@@ -151,39 +169,51 @@ export class OpenAIResponsesAPIAdapter implements ProviderAdapter {
151
169
  private readonly extraHeaders: Record<string, string>;
152
170
 
153
171
  constructor(config: OpenAIResponsesAPIAdapterConfig = {}) {
172
+ this.subscription = config.mode === 'subscription';
173
+ this.requiresNativeResponsesInput = !this.subscription;
174
+ this.credentials = config.credentials;
175
+ if (this.subscription && !this.credentials) {
176
+ throw new Error('Subscription mode requires a credential resolver');
177
+ }
178
+ this.fastMode = config.fastMode ?? false;
179
+ this.onFastModeFallback = config.onFastModeFallback;
154
180
  this.apiKey = config.apiKey ?? process.env.OPENAI_API_KEY ?? '';
155
- this.baseURL = (config.baseURL ?? 'https://api.openai.com/v1').replace(/\/$/, '');
181
+ this.baseURL = (config.baseURL ?? (this.subscription ? 'https://chatgpt.com/backend-api/codex' : 'https://api.openai.com/v1')).replace(/\/$/, '');
156
182
  this.organization = config.organization;
157
183
  this.project = config.project;
158
184
  this.defaultMaxTokens = config.defaultMaxTokens ?? 4096;
159
185
  this.extraHeaders = config.extraHeaders ?? {};
160
186
 
161
- if (!this.apiKey) {
187
+ if (!this.apiKey && !this.credentials) {
162
188
  throw new Error('OpenAI API key not provided');
163
189
  }
164
190
  }
165
191
 
166
- supportsModel(_modelId: string): boolean {
167
- return true;
192
+ supportsModel(modelId: string): boolean {
193
+ return !this.subscription || modelId.startsWith('gpt-') || modelId.includes('codex');
194
+ }
195
+
196
+ isFastMode(): boolean {
197
+ return this.fastMode;
198
+ }
199
+
200
+ setFastMode(enabled: boolean): void {
201
+ this.fastMode = enabled;
168
202
  }
169
203
 
170
204
  async complete(
171
205
  request: ProviderRequest,
172
206
  options?: ProviderRequestOptions
173
207
  ): Promise<OpenAIResponsesAPIProviderResponse> {
208
+ if (this.subscription) return this.stream(request, { onChunk: () => {} }, options);
174
209
  const responsesRequest = this.buildRequest(request);
175
210
  options?.onRequest?.(responsesRequest);
176
211
 
177
212
  const { signal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
178
213
  try {
179
- const response = await fetch(`${this.baseURL}/responses`, {
180
- method: 'POST',
181
- headers: this.getHeaders(),
182
- body: JSON.stringify(responsesRequest),
183
- signal,
184
- });
185
-
186
- await this.assertSuccessfulHTTPResponse(response);
214
+ const response = await this.fetchResponse(responsesRequest, signal);
215
+
216
+ await this.assertSuccessfulHTTPResponse(response, responsesRequest);
187
217
  const data = (await response.json()) as OpenAIResponsesAPIResponse;
188
218
  this.assertSuccessfulAPIResponse(data, responsesRequest, 'response error');
189
219
  return this.parseResponse(data, request.model, responsesRequest);
@@ -205,19 +235,14 @@ export class OpenAIResponsesAPIAdapter implements ProviderAdapter {
205
235
 
206
236
  const { signal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
207
237
  try {
208
- const response = await fetch(`${this.baseURL}/responses`, {
209
- method: 'POST',
210
- headers: this.getHeaders(),
211
- body: JSON.stringify(responsesRequest),
212
- signal,
213
- });
214
-
215
- await this.assertSuccessfulHTTPResponse(response);
238
+ const response = await this.fetchResponse(responsesRequest, signal);
239
+
240
+ await this.assertSuccessfulHTTPResponse(response, responsesRequest);
216
241
  const reader = response.body?.getReader();
217
242
  if (!reader) throw new Error('OpenAI Responses API returned no response body');
218
243
 
219
244
  const decoder = new TextDecoder();
220
- const parser = new SSELineParser();
245
+ const parser = new SSELineParser({ multiline: true });
221
246
  const events: unknown[] = [];
222
247
  const output: OpenAIResponsesOutputItem[] = [];
223
248
  let terminalResponse: OpenAIResponsesAPIResponse | undefined;
@@ -266,22 +291,27 @@ export class OpenAIResponsesAPIAdapter implements ProviderAdapter {
266
291
  // provider classification, so only code and message are handed over.
267
292
  // The payload object is always present, so this always throws.
268
293
  throwOnStreamErrorFrame(
269
- { error: { code: event.code, message: event.message ?? 'Streaming request failed' } },
294
+ { error: event.error ?? { code: event.code, message: event.message ?? 'Streaming request failed' } },
270
295
  'OpenAI Responses API',
271
296
  responsesRequest
272
297
  );
273
298
  }
274
299
  };
275
300
 
276
- while (true) {
277
- const { done, value } = await reader.read();
278
- if (done) break;
279
- for (const data of parser.feed(decoder.decode(value, { stream: true }))) {
280
- processData(data);
301
+ try {
302
+ while (true) {
303
+ const { done, value } = await reader.read();
304
+ if (done) break;
305
+ for (const data of parser.feed(decoder.decode(value, { stream: true }))) {
306
+ processData(data);
307
+ }
281
308
  }
309
+ for (const data of parser.feed(decoder.decode())) processData(data);
310
+ for (const data of parser.flush()) processData(data);
311
+ } finally {
312
+ void reader.cancel().catch(() => {});
313
+ reader.releaseLock();
282
314
  }
283
- for (const data of parser.feed(decoder.decode())) processData(data);
284
- for (const data of parser.flush()) processData(data);
285
315
 
286
316
  // A well-formed stream always ends with a terminal event
287
317
  // (response.completed / response.incomplete; response.failed and error
@@ -300,6 +330,18 @@ export class OpenAIResponsesAPIAdapter implements ProviderAdapter {
300
330
 
301
331
  this.assertSuccessfulAPIResponse(terminalResponse, responsesRequest);
302
332
 
333
+ // Codex may deliver authoritative items only via output_item.done.
334
+ // A non-empty terminal output remains authoritative when supplied.
335
+ if (!terminalResponse.output?.length) {
336
+ terminalResponse = { ...terminalResponse, output: output.filter(Boolean) };
337
+ }
338
+ const returnedTier = terminalResponse.service_tier;
339
+ if (this.subscription && responsesRequest.service_tier === 'priority' &&
340
+ typeof returnedTier === 'string' && returnedTier && returnedTier !== 'priority' &&
341
+ !this.warnedFastFallback) {
342
+ this.warnedFastFallback = true;
343
+ this.onFastModeFallback?.(returnedTier);
344
+ }
303
345
  const parsed = this.parseResponse(terminalResponse, request.model, responsesRequest);
304
346
  parsed.content.forEach((block, index) => callbacks.onContentBlock?.(index, block));
305
347
  return parsed;
@@ -314,14 +356,21 @@ export class OpenAIResponsesAPIAdapter implements ProviderAdapter {
314
356
  // Request construction
315
357
  // --------------------------------------------------------------------------
316
358
 
317
- private getHeaders(): Record<string, string> {
318
- return {
319
- Authorization: `Bearer ${this.apiKey}`,
320
- 'Content-Type': 'application/json',
321
- ...(this.organization ? { 'OpenAI-Organization': this.organization } : {}),
322
- ...(this.project ? { 'OpenAI-Project': this.project } : {}),
323
- ...this.extraHeaders,
324
- };
359
+ private async fetchResponse(
360
+ request: OpenAIResponsesAPIRequest,
361
+ signal?: AbortSignal,
362
+ ): Promise<Response> {
363
+ return fetchWithCredentials(`${this.baseURL}/responses`, {
364
+ method: 'POST',
365
+ body: JSON.stringify(request),
366
+ signal,
367
+ headers: {
368
+ 'Content-Type': 'application/json',
369
+ ...(this.organization ? { 'OpenAI-Organization': this.organization } : {}),
370
+ ...(this.project ? { 'OpenAI-Project': this.project } : {}),
371
+ ...this.extraHeaders,
372
+ },
373
+ }, this.credentials ?? { token: this.apiKey });
325
374
  }
326
375
 
327
376
  private buildRequest(request: ProviderRequest): OpenAIResponsesAPIRequest {
@@ -366,9 +415,18 @@ export class OpenAIResponsesAPIAdapter implements ProviderAdapter {
366
415
 
367
416
  // These invariants define the adapter's stateless native-item contract and
368
417
  // cannot be overridden through provider params.
369
- responsesRequest.input = request.messages as OpenAIResponsesInputItem[];
418
+ responsesRequest.input = this.subscription
419
+ ? normalizeResponsesInput(request.messages)
420
+ : request.messages as OpenAIResponsesInputItem[];
370
421
  responsesRequest.store = false;
371
422
  responsesRequest.include = this.mergeEncryptedReasoningInclude(responsesRequest.include);
423
+ if (this.subscription) {
424
+ for (const key of ['temperature', 'top_p', 'top_k', 'max_output_tokens', 'max_tokens', 'max_completion_tokens']) {
425
+ delete responsesRequest[key];
426
+ }
427
+ if (this.fastMode) responsesRequest.service_tier = 'priority';
428
+ else delete responsesRequest.service_tier;
429
+ }
372
430
  return responsesRequest;
373
431
  }
374
432
 
@@ -605,10 +663,27 @@ export class OpenAIResponsesAPIAdapter implements ProviderAdapter {
605
663
  // Errors
606
664
  // --------------------------------------------------------------------------
607
665
 
608
- private async assertSuccessfulHTTPResponse(response: Response): Promise<void> {
666
+ private async assertSuccessfulHTTPResponse(response: Response, rawRequest: unknown): Promise<void> {
609
667
  if (response.ok) return;
610
- const errorText = await response.text();
611
- throw new Error(`OpenAI Responses API error: ${response.status} ${errorText}`);
668
+ const detail = await response.text();
669
+ const message = `OpenAI Responses API error: ${response.status} ${detail}`;
670
+ const status = response.status;
671
+ if (status === 401 || status === 403) {
672
+ throw new MembraneError({ type: 'auth', message, retryable: false, httpStatus: status, rawError: detail, rawRequest });
673
+ }
674
+ if (status === 429) {
675
+ const retryAfter = response.headers.get('retry-after');
676
+ const seconds = retryAfter == null ? NaN : Number(retryAfter);
677
+ const date = retryAfter == null ? NaN : Date.parse(retryAfter);
678
+ const delay = Number.isFinite(seconds) ? Math.max(0, seconds * 1000)
679
+ : Number.isFinite(date) ? Math.max(0, date - Date.now()) : undefined;
680
+ throw rateLimitError(message, delay, detail, rawRequest);
681
+ }
682
+ if (status >= 500) throw serverError(message, status, detail, rawRequest);
683
+ if (status === 400 && /context_length|maximum context|token limit|too long/i.test(detail)) {
684
+ throw contextLengthError(message, detail, rawRequest);
685
+ }
686
+ throw new MembraneError({ type: 'invalid_request', message, retryable: false, httpStatus: status, rawError: detail, rawRequest });
612
687
  }
613
688
 
614
689
  /**
@@ -307,6 +307,8 @@ export class OpenAIResponsesAdapter implements ProviderAdapter {
307
307
  /**
308
308
  * Collect every base64 image in the conversation, in order, remembering
309
309
  * which message/block it came from so the transcript can reference it.
310
+ * Both `image` blocks and `generated_image` blocks (this adapter's own
311
+ * earlier outputs, when a consumer carries them forward as-is) count.
310
312
  */
311
313
  private collectImages(request: ProviderRequest): ImageRef[] {
312
314
  const refs: ImageRef[] = [];
@@ -315,11 +317,18 @@ export class OpenAIResponsesAdapter implements ProviderAdapter {
315
317
  (request.messages as any[]).forEach((msg, msgIndex) => {
316
318
  if (!Array.isArray(msg.content)) return;
317
319
  msg.content.forEach((block: any, blockIndex: number) => {
318
- if (block?.type !== 'image') return;
319
- const source = block.source;
320
- if (source?.type === 'base64' && source.data) {
321
- const mimeType = source.media_type ?? source.mediaType ?? 'image/png';
322
- refs.push({ dataUrl: `data:${mimeType};base64,${source.data}`, msgIndex, blockIndex });
320
+ if (block?.type === 'image') {
321
+ const source = block.source;
322
+ if (source?.type === 'base64' && source.data) {
323
+ const mimeType = source.media_type ?? source.mediaType ?? 'image/png';
324
+ refs.push({ dataUrl: `data:${mimeType};base64,${source.data}`, msgIndex, blockIndex });
325
+ }
326
+ } else if (block?.type === 'generated_image' && typeof block.data === 'string' && block.data) {
327
+ // A previous output of this adapter carried forward verbatim in
328
+ // history (consumers that keep ProviderResponse content rather
329
+ // than rebuilding from a channel). It is an image like any other.
330
+ const mimeType = block.mimeType ?? 'image/png';
331
+ refs.push({ dataUrl: `data:${mimeType};base64,${block.data}`, msgIndex, blockIndex });
323
332
  }
324
333
  });
325
334
  });
@@ -481,7 +490,7 @@ export class OpenAIResponsesAdapter implements ProviderAdapter {
481
490
  msg.content.forEach((block: any, blockIndex: number) => {
482
491
  if (block?.type === 'text' && block.text) {
483
492
  lines.push(block.text);
484
- } else if (block?.type === 'image') {
493
+ } else if (block?.type === 'image' || block?.type === 'generated_image') {
485
494
  const n = attachedIndex.get(`${msgIndex}:${blockIndex}`);
486
495
  lines.push(n !== undefined ? `[Image ${n}]` : '[Image omitted]');
487
496
  }
@@ -0,0 +1,131 @@
1
+ import type { ProviderRequest } from '../types/index.js';
2
+ import type { OpenAIResponsesInputItem } from './openai-responses-api.js';
3
+
4
+ type JsonObject = Record<string, unknown>;
5
+
6
+ /**
7
+ * Most agent turns arrive already formatted as provider-native Responses
8
+ * items. Internal maintenance calls, however, can bypass that formatter and
9
+ * carry Membrane's normalized `text`/`image`/tool blocks. Normalize at the
10
+ * final transport boundary so every call shape accepted by ProviderAdapter is
11
+ * valid on the Codex Responses endpoint.
12
+ */
13
+ export function normalizeResponsesInput(messages: ProviderRequest['messages']): OpenAIResponsesInputItem[] {
14
+ const output: unknown[] = [];
15
+
16
+ for (const rawMessage of messages as unknown[]) {
17
+ if (!isObject(rawMessage)) {
18
+ output.push(rawMessage);
19
+ continue;
20
+ }
21
+ if (rawMessage.type !== 'message' && rawMessage.role === undefined) {
22
+ output.push(normalizeStandaloneItem(rawMessage));
23
+ continue;
24
+ }
25
+
26
+ // Native messages (including phase, status and developer/system roles)
27
+ // must survive replay verbatim. Only translate normalized content blocks.
28
+ if (Array.isArray(rawMessage.content) && !rawMessage.content.some((block) =>
29
+ isObject(block) && ['text', 'image', 'tool_use', 'tool_result', 'redacted_thinking'].includes(asString(block.type))
30
+ )) {
31
+ output.push(rawMessage);
32
+ continue;
33
+ }
34
+ const role = typeof rawMessage.role === 'string' ? rawMessage.role : 'user';
35
+ const blocks = Array.isArray(rawMessage.content)
36
+ ? rawMessage.content
37
+ : typeof rawMessage.content === 'string'
38
+ ? [{ type: 'text', text: rawMessage.content }]
39
+ : [];
40
+ let parts: unknown[] = [];
41
+ const flush = () => {
42
+ if (parts.length === 0) return;
43
+ output.push({
44
+ type: 'message',
45
+ ...rawMessage,
46
+ role,
47
+ content: parts,
48
+ });
49
+ parts = [];
50
+ };
51
+
52
+ for (const rawBlock of blocks) {
53
+ if (!isObject(rawBlock)) continue;
54
+ if (rawBlock.type === 'text') {
55
+ parts.push({ type: role === 'assistant' ? 'output_text' : 'input_text', text: asString(rawBlock.text) });
56
+ } else if (rawBlock.type === 'image') {
57
+ const imageUrl = responsesImageUrl(rawBlock);
58
+ if (imageUrl && role !== 'assistant') parts.push({ type: 'input_image', image_url: imageUrl });
59
+ } else if (rawBlock.type === 'tool_use') {
60
+ flush();
61
+ output.push(normalizeStandaloneItem(rawBlock));
62
+ } else if (rawBlock.type === 'tool_result') {
63
+ flush();
64
+ output.push(normalizeStandaloneItem(rawBlock));
65
+ } else if (rawBlock.type === 'redacted_thinking') {
66
+ flush();
67
+ output.push(reasoningInputItem(rawBlock));
68
+ } else {
69
+ // Already-native input_text/output_text/input_image/refusal parts.
70
+ parts.push(rawBlock);
71
+ }
72
+ }
73
+ flush();
74
+ }
75
+
76
+ return output as OpenAIResponsesInputItem[];
77
+ }
78
+
79
+ function normalizeStandaloneItem(item: JsonObject): unknown {
80
+ if (item.type === 'tool_use') {
81
+ return {
82
+ type: 'function_call',
83
+ call_id: asString(item.id),
84
+ name: asString(item.name),
85
+ arguments: JSON.stringify(isObject(item.input) ? item.input : {}),
86
+ };
87
+ }
88
+ if (item.type === 'tool_result') {
89
+ const content = item.content;
90
+ return {
91
+ type: 'function_call_output',
92
+ call_id: asString(item.toolUseId) || asString(item.tool_use_id),
93
+ output: typeof content === 'string' ? content : JSON.stringify(content ?? null),
94
+ };
95
+ }
96
+ if (item.type === 'redacted_thinking') {
97
+ return reasoningInputItem(item);
98
+ }
99
+ return item;
100
+ }
101
+
102
+ /** Replay a captured reasoning carrier as a Responses input item.
103
+ *
104
+ * Prefer the provider-native item verbatim when the block still carries it
105
+ * (`rawItem` from response parsing). Otherwise reconstruct the minimum the
106
+ * Responses API accepts: `summary` is a REQUIRED field on reasoning input
107
+ * items (empty array = "no summaries") — omitting it 400s with
108
+ * "Missing required parameter: 'input[N].summary'". */
109
+ function reasoningInputItem(block: JsonObject): unknown {
110
+ const raw = block.rawItem;
111
+ if (isObject(raw) && raw.type === 'reasoning') return raw;
112
+ return { type: 'reasoning', summary: [], encrypted_content: asString(block.data) };
113
+ }
114
+
115
+ function responsesImageUrl(block: JsonObject): string | undefined {
116
+ const source = isObject(block.source) ? block.source : undefined;
117
+ if (!source) return typeof block.image_url === 'string' ? block.image_url : undefined;
118
+ if (source.type === 'url') return asString(source.url) || undefined;
119
+ if (source.type !== 'base64') return undefined;
120
+ const mediaType = asString(source.mediaType) || asString(source.media_type) || 'image/png';
121
+ const data = asString(source.data);
122
+ return data ? `data:${mediaType};base64,${data}` : undefined;
123
+ }
124
+
125
+ function asString(value: unknown): string {
126
+ return typeof value === 'string' ? value : '';
127
+ }
128
+
129
+ function isObject(value: unknown): value is JsonObject {
130
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
131
+ }
@@ -291,6 +291,9 @@ export function createCombinedSignal(
291
291
  */
292
292
  export class SSELineParser {
293
293
  private buffer: string = '';
294
+ private data: string[] = [];
295
+
296
+ constructor(private readonly options: { multiline?: boolean } = {}) {}
294
297
 
295
298
  /**
296
299
  * Feed a raw chunk from the stream reader and get back complete SSE data lines.
@@ -305,6 +308,10 @@ export class SSELineParser {
305
308
  this.buffer = lines.pop() || '';
306
309
 
307
310
  for (const line of lines) {
311
+ if (this.options.multiline) {
312
+ this.processEventLine(line.replace(/\r$/, ''), results);
313
+ continue;
314
+ }
308
315
  const trimmed = line.trim();
309
316
  if (trimmed.startsWith('data: ')) {
310
317
  results.push(trimmed.slice(6));
@@ -315,10 +322,24 @@ export class SSELineParser {
315
322
  return results;
316
323
  }
317
324
 
318
- /**
319
- * Flush any remaining buffered content (call when stream ends).
320
- */
325
+ private processEventLine(line: string, results: string[]): void {
326
+ if (line === '') {
327
+ if (this.data.length) results.push(this.data.join('\n'));
328
+ this.data = [];
329
+ } else if (line.startsWith('data:')) {
330
+ this.data.push(line.slice(5).replace(/^ /, ''));
331
+ }
332
+ }
333
+
334
+ /** Flush any remaining event/tail when the stream ends. */
321
335
  flush(): string[] {
336
+ if (this.options.multiline) {
337
+ const results: string[] = [];
338
+ if (this.buffer) this.processEventLine(this.buffer.replace(/\r$/, ''), results);
339
+ this.buffer = '';
340
+ this.processEventLine('', results);
341
+ return results;
342
+ }
322
343
  if (!this.buffer.trim()) return [];
323
344
  const trimmed = this.buffer.trim();
324
345
  this.buffer = '';
@@ -207,6 +207,11 @@ export interface ProviderAdapter {
207
207
  */
208
208
  usageCacheConvention?: UsageCacheConvention;
209
209
 
210
+ /** Whether this transport requires the configured Responses formatter.
211
+ * False permits generic per-request formatter overrides (e.g. named
212
+ * maintenance messages). Wrappers must forward this capability. */
213
+ readonly requiresNativeResponsesInput?: boolean;
214
+
210
215
  /** Check if this adapter handles a model */
211
216
  supportsModel(modelId: string): boolean;
212
217