@animalabs/membrane 0.5.69 → 0.5.71

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 (37) hide show
  1. package/dist/formatters/index.d.ts +1 -0
  2. package/dist/formatters/index.d.ts.map +1 -1
  3. package/dist/formatters/index.js +1 -0
  4. package/dist/formatters/index.js.map +1 -1
  5. package/dist/formatters/native.d.ts.map +1 -1
  6. package/dist/formatters/native.js +6 -0
  7. package/dist/formatters/native.js.map +1 -1
  8. package/dist/formatters/openai-responses.d.ts +23 -0
  9. package/dist/formatters/openai-responses.d.ts.map +1 -0
  10. package/dist/formatters/openai-responses.js +188 -0
  11. package/dist/formatters/openai-responses.js.map +1 -0
  12. package/dist/membrane.d.ts.map +1 -1
  13. package/dist/membrane.js +29 -2
  14. package/dist/membrane.js.map +1 -1
  15. package/dist/providers/anthropic.d.ts.map +1 -1
  16. package/dist/providers/anthropic.js +32 -3
  17. package/dist/providers/anthropic.js.map +1 -1
  18. package/dist/providers/index.d.ts +1 -0
  19. package/dist/providers/index.d.ts.map +1 -1
  20. package/dist/providers/index.js +1 -0
  21. package/dist/providers/index.js.map +1 -1
  22. package/dist/providers/openai-responses-api.d.ts +129 -0
  23. package/dist/providers/openai-responses-api.d.ts.map +1 -0
  24. package/dist/providers/openai-responses-api.js +479 -0
  25. package/dist/providers/openai-responses-api.js.map +1 -0
  26. package/dist/types/content.d.ts +26 -0
  27. package/dist/types/content.d.ts.map +1 -1
  28. package/dist/types/content.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/formatters/index.ts +4 -0
  31. package/src/formatters/native.ts +5 -0
  32. package/src/formatters/openai-responses.ts +220 -0
  33. package/src/membrane.ts +28 -2
  34. package/src/providers/anthropic.ts +32 -4
  35. package/src/providers/index.ts +11 -0
  36. package/src/providers/openai-responses-api.ts +653 -0
  37. package/src/types/content.ts +26 -0
@@ -0,0 +1,653 @@
1
+ /**
2
+ * OpenAI Responses API adapter.
3
+ *
4
+ * This is intentionally separate from `openai-responses.ts`. Despite its name,
5
+ * that compatibility-sensitive adapter targets the Images API.
6
+ *
7
+ * This adapter is stateless and provider-native: `ProviderRequest.messages` is
8
+ * sent verbatim as the Responses API `input` item array, and `outputItems`
9
+ * exposes the response's ordered output array verbatim for the next turn.
10
+ */
11
+
12
+ import type {
13
+ ContentBlock,
14
+ ProviderAdapter,
15
+ ProviderRequest,
16
+ ProviderRequestOptions,
17
+ ProviderResponse,
18
+ StreamCallbacks,
19
+ } from '../types/index.js';
20
+ import {
21
+ MembraneError,
22
+ abortError,
23
+ authError,
24
+ contextLengthError,
25
+ networkError,
26
+ rateLimitError,
27
+ serverError,
28
+ } from '../types/index.js';
29
+ import { createCombinedSignal, SSELineParser, safeParseJson } from './utils.js';
30
+
31
+ // ============================================================================
32
+ // Provider-native Responses API types
33
+ // ============================================================================
34
+
35
+ export interface OpenAIResponsesInputItem {
36
+ type?: string;
37
+ id?: string | null;
38
+ [key: string]: unknown;
39
+ }
40
+
41
+ export interface OpenAIResponsesOutputItem {
42
+ type: string;
43
+ id?: string;
44
+ [key: string]: unknown;
45
+ }
46
+
47
+ export interface OpenAIResponsesAPIRequest {
48
+ model: string;
49
+ input: OpenAIResponsesInputItem[];
50
+ store: false;
51
+ include: string[];
52
+ instructions?: string;
53
+ max_output_tokens?: number;
54
+ temperature?: number;
55
+ top_p?: number;
56
+ tools?: unknown[];
57
+ stream?: boolean;
58
+ [key: string]: unknown;
59
+ }
60
+
61
+ export interface OpenAIResponsesAPIResponse {
62
+ id?: string;
63
+ object?: string;
64
+ model?: string;
65
+ output: OpenAIResponsesOutputItem[];
66
+ status?: string;
67
+ incomplete_details?: { reason?: string | null } | null;
68
+ error?: { code?: string | null; message?: string | null } | null;
69
+ usage?: {
70
+ input_tokens?: number;
71
+ output_tokens?: number;
72
+ total_tokens?: number;
73
+ input_tokens_details?: { cached_tokens?: number } | null;
74
+ output_tokens_details?: { reasoning_tokens?: number } | null;
75
+ } | null;
76
+ [key: string]: unknown;
77
+ }
78
+
79
+ export type OpenAIResponsesAPIContentBlock =
80
+ | (ContentBlock & {
81
+ itemId?: string;
82
+ outputIndex: number;
83
+ contentIndex?: number;
84
+ phase?: 'commentary' | 'final_answer' | null;
85
+ rawItem?: OpenAIResponsesOutputItem;
86
+ })
87
+ | {
88
+ type: 'compaction';
89
+ id?: string;
90
+ encryptedContent: string;
91
+ createdBy?: string;
92
+ outputIndex: number;
93
+ rawItem: OpenAIResponsesOutputItem;
94
+ }
95
+ | {
96
+ type: 'openai_response_item';
97
+ itemId?: string;
98
+ itemType: string;
99
+ outputIndex: number;
100
+ rawItem: OpenAIResponsesOutputItem;
101
+ };
102
+
103
+ export interface OpenAIResponsesAPIProviderResponse extends Omit<ProviderResponse, 'content' | 'raw'> {
104
+ content: OpenAIResponsesAPIContentBlock[];
105
+ /** Ordered, provider-native output items. Append these verbatim to the next input. */
106
+ outputItems: OpenAIResponsesOutputItem[];
107
+ raw: OpenAIResponsesAPIResponse | Record<string, unknown>;
108
+ }
109
+
110
+ // ============================================================================
111
+ // Configuration
112
+ // ============================================================================
113
+
114
+ export interface OpenAIResponsesAPIAdapterConfig {
115
+ /** API key (defaults to OPENAI_API_KEY). */
116
+ apiKey?: string;
117
+ /** API base URL (default: https://api.openai.com/v1). */
118
+ baseURL?: string;
119
+ /** Optional OpenAI organization ID. */
120
+ organization?: string;
121
+ /** Optional OpenAI project ID. */
122
+ project?: string;
123
+ /** Default maximum output tokens when the request does not provide one. */
124
+ defaultMaxTokens?: number;
125
+ /** Additional HTTP headers. */
126
+ extraHeaders?: Record<string, string>;
127
+ }
128
+
129
+ // ============================================================================
130
+ // Adapter
131
+ // ============================================================================
132
+
133
+ export class OpenAIResponsesAPIAdapter implements ProviderAdapter {
134
+ readonly name = 'openai-responses-api';
135
+
136
+ private readonly apiKey: string;
137
+ private readonly baseURL: string;
138
+ private readonly organization?: string;
139
+ private readonly project?: string;
140
+ private readonly defaultMaxTokens: number;
141
+ private readonly extraHeaders: Record<string, string>;
142
+
143
+ constructor(config: OpenAIResponsesAPIAdapterConfig = {}) {
144
+ this.apiKey = config.apiKey ?? process.env.OPENAI_API_KEY ?? '';
145
+ this.baseURL = (config.baseURL ?? 'https://api.openai.com/v1').replace(/\/$/, '');
146
+ this.organization = config.organization;
147
+ this.project = config.project;
148
+ this.defaultMaxTokens = config.defaultMaxTokens ?? 4096;
149
+ this.extraHeaders = config.extraHeaders ?? {};
150
+
151
+ if (!this.apiKey) {
152
+ throw new Error('OpenAI API key not provided');
153
+ }
154
+ }
155
+
156
+ supportsModel(_modelId: string): boolean {
157
+ return true;
158
+ }
159
+
160
+ async complete(
161
+ request: ProviderRequest,
162
+ options?: ProviderRequestOptions
163
+ ): Promise<OpenAIResponsesAPIProviderResponse> {
164
+ const responsesRequest = this.buildRequest(request);
165
+ options?.onRequest?.(responsesRequest);
166
+
167
+ const { signal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
168
+ try {
169
+ const response = await fetch(`${this.baseURL}/responses`, {
170
+ method: 'POST',
171
+ headers: this.getHeaders(),
172
+ body: JSON.stringify(responsesRequest),
173
+ signal,
174
+ });
175
+
176
+ await this.assertSuccessfulHTTPResponse(response);
177
+ const data = (await response.json()) as OpenAIResponsesAPIResponse;
178
+ this.assertSuccessfulAPIResponse(data);
179
+ return this.parseResponse(data, request.model, responsesRequest);
180
+ } catch (error) {
181
+ throw this.handleError(error, responsesRequest);
182
+ } finally {
183
+ cleanup?.();
184
+ }
185
+ }
186
+
187
+ async stream(
188
+ request: ProviderRequest,
189
+ callbacks: StreamCallbacks,
190
+ options?: ProviderRequestOptions
191
+ ): Promise<OpenAIResponsesAPIProviderResponse> {
192
+ const responsesRequest = this.buildRequest(request);
193
+ responsesRequest.stream = true;
194
+ options?.onRequest?.(responsesRequest);
195
+
196
+ const { signal, cleanup } = createCombinedSignal(options?.signal, options?.timeoutMs);
197
+ try {
198
+ const response = await fetch(`${this.baseURL}/responses`, {
199
+ method: 'POST',
200
+ headers: this.getHeaders(),
201
+ body: JSON.stringify(responsesRequest),
202
+ signal,
203
+ });
204
+
205
+ await this.assertSuccessfulHTTPResponse(response);
206
+ const reader = response.body?.getReader();
207
+ if (!reader) throw new Error('OpenAI Responses API returned no response body');
208
+
209
+ const decoder = new TextDecoder();
210
+ const parser = new SSELineParser();
211
+ const events: unknown[] = [];
212
+ const output: OpenAIResponsesOutputItem[] = [];
213
+ let terminalResponse: OpenAIResponsesAPIResponse | undefined;
214
+
215
+ const processData = (data: string): void => {
216
+ if (!data || data === '[DONE]') return;
217
+
218
+ let event: any;
219
+ try {
220
+ event = JSON.parse(data);
221
+ } catch {
222
+ return;
223
+ }
224
+ events.push(event);
225
+
226
+ if (event.type === 'response.output_text.delta') {
227
+ const delta = typeof event.delta === 'string' ? event.delta : '';
228
+ if (delta) callbacks.onChunk(delta);
229
+ this.applyTextDelta(output, event);
230
+ } else if (event.type === 'response.function_call_arguments.delta') {
231
+ this.applyFunctionArgumentsDelta(output, event);
232
+ } else if (
233
+ event.type === 'response.output_item.added' ||
234
+ event.type === 'response.output_item.done'
235
+ ) {
236
+ if (Number.isInteger(event.output_index) && event.item) {
237
+ output[event.output_index] = event.item;
238
+ }
239
+ } else if (
240
+ event.type === 'response.completed' ||
241
+ event.type === 'response.incomplete'
242
+ ) {
243
+ terminalResponse = event.response;
244
+ } else if (event.type === 'response.failed') {
245
+ const failed = event.response as OpenAIResponsesAPIResponse | undefined;
246
+ throw new Error(
247
+ `OpenAI Responses API error: ${failed?.error?.code ?? 'response_failed'} ` +
248
+ `${failed?.error?.message ?? 'Response failed'}`
249
+ );
250
+ } else if (event.type === 'error') {
251
+ throw new Error(
252
+ `OpenAI Responses API error: ${event.code ?? 'stream_error'} ` +
253
+ `${event.message ?? 'Streaming request failed'}`
254
+ );
255
+ }
256
+ };
257
+
258
+ while (true) {
259
+ const { done, value } = await reader.read();
260
+ if (done) break;
261
+ for (const data of parser.feed(decoder.decode(value, { stream: true }))) {
262
+ processData(data);
263
+ }
264
+ }
265
+ for (const data of parser.feed(decoder.decode())) processData(data);
266
+ for (const data of parser.flush()) processData(data);
267
+
268
+ // A well-formed stream always ends with a terminal event
269
+ // (response.completed / response.incomplete; response.failed and error
270
+ // throw above). Reaching EOF without one means the connection was
271
+ // dropped mid-stream (proxy/LB close, early termination). Fabricating a
272
+ // 'completed' response here would persist a silently truncated turn
273
+ // with end_turn/zero usage — signal a retryable stream failure instead.
274
+ if (!terminalResponse) {
275
+ throw networkError(
276
+ 'OpenAI Responses API stream ended before a terminal response event ' +
277
+ `(connection dropped after ${events.length} events)`,
278
+ undefined,
279
+ responsesRequest
280
+ );
281
+ }
282
+
283
+ this.assertSuccessfulAPIResponse(terminalResponse);
284
+
285
+ const parsed = this.parseResponse(terminalResponse, request.model, responsesRequest);
286
+ parsed.content.forEach((block, index) => callbacks.onContentBlock?.(index, block));
287
+ return parsed;
288
+ } catch (error) {
289
+ throw this.handleError(error, responsesRequest);
290
+ } finally {
291
+ cleanup?.();
292
+ }
293
+ }
294
+
295
+ // --------------------------------------------------------------------------
296
+ // Request construction
297
+ // --------------------------------------------------------------------------
298
+
299
+ private getHeaders(): Record<string, string> {
300
+ return {
301
+ Authorization: `Bearer ${this.apiKey}`,
302
+ 'Content-Type': 'application/json',
303
+ ...(this.organization ? { 'OpenAI-Organization': this.organization } : {}),
304
+ ...(this.project ? { 'OpenAI-Project': this.project } : {}),
305
+ ...this.extraHeaders,
306
+ };
307
+ }
308
+
309
+ private buildRequest(request: ProviderRequest): OpenAIResponsesAPIRequest {
310
+ if (!Array.isArray(request.messages)) {
311
+ throw new Error('OpenAI Responses API input must be a provider-native input-item array');
312
+ }
313
+
314
+ const responsesRequest: OpenAIResponsesAPIRequest = {
315
+ model: request.model,
316
+ input: request.messages as OpenAIResponsesInputItem[],
317
+ store: false,
318
+ include: ['reasoning.encrypted_content'],
319
+ max_output_tokens: request.maxTokens || this.defaultMaxTokens,
320
+ };
321
+
322
+ const instructions = this.flattenInstructions(request.system);
323
+ if (instructions) responsesRequest.instructions = instructions;
324
+ if (request.temperature !== undefined) responsesRequest.temperature = request.temperature;
325
+ if (request.topP !== undefined) responsesRequest.top_p = request.topP;
326
+ if (request.tools?.length) responsesRequest.tools = this.convertTools(request.tools);
327
+
328
+ if (request.extra) {
329
+ const {
330
+ normalizedMessages,
331
+ prompt,
332
+ messages,
333
+ input,
334
+ store,
335
+ stream,
336
+ include,
337
+ ...extra
338
+ } = request.extra;
339
+ void normalizedMessages;
340
+ void prompt;
341
+ void messages;
342
+ void input;
343
+ void store;
344
+ void stream;
345
+ Object.assign(responsesRequest, extra);
346
+ responsesRequest.include = this.mergeEncryptedReasoningInclude(include);
347
+ }
348
+
349
+ // These invariants define the adapter's stateless native-item contract and
350
+ // cannot be overridden through provider params.
351
+ responsesRequest.input = request.messages as OpenAIResponsesInputItem[];
352
+ responsesRequest.store = false;
353
+ responsesRequest.include = this.mergeEncryptedReasoningInclude(responsesRequest.include);
354
+ return responsesRequest;
355
+ }
356
+
357
+ private mergeEncryptedReasoningInclude(value: unknown): string[] {
358
+ const include = Array.isArray(value)
359
+ ? value.filter((item): item is string => typeof item === 'string')
360
+ : [];
361
+ return include.includes('reasoning.encrypted_content')
362
+ ? include
363
+ : [...include, 'reasoning.encrypted_content'];
364
+ }
365
+
366
+ private flattenInstructions(system: ProviderRequest['system']): string | undefined {
367
+ if (typeof system === 'string') return system || undefined;
368
+ if (!Array.isArray(system)) return undefined;
369
+ const text = system
370
+ .map((block: any) =>
371
+ block?.type === 'text' || block?.type === 'input_text' ? block.text : undefined
372
+ )
373
+ .filter((value): value is string => typeof value === 'string' && value.length > 0)
374
+ .join('\n');
375
+ return text || undefined;
376
+ }
377
+
378
+ private convertTools(tools: unknown[]): unknown[] {
379
+ return tools.map((rawTool: any) => {
380
+ if (rawTool?.type && rawTool.type !== 'function') return rawTool;
381
+
382
+ // Responses function definitions are flat. Accept them verbatim, while
383
+ // also adapting Membrane and Chat Completions function schemas.
384
+ if (rawTool?.type === 'function' && rawTool.name) return rawTool;
385
+ if (rawTool?.type === 'function' && rawTool.function) {
386
+ return { type: 'function', ...rawTool.function };
387
+ }
388
+ return {
389
+ type: 'function',
390
+ name: rawTool?.name,
391
+ description: rawTool?.description,
392
+ parameters:
393
+ rawTool?.parameters ??
394
+ rawTool?.inputSchema ??
395
+ rawTool?.input_schema ??
396
+ { type: 'object', properties: {} },
397
+ ...(rawTool?.strict !== undefined ? { strict: rawTool.strict } : {}),
398
+ };
399
+ });
400
+ }
401
+
402
+ // --------------------------------------------------------------------------
403
+ // Response conversion
404
+ // --------------------------------------------------------------------------
405
+
406
+ private parseResponse(
407
+ response: OpenAIResponsesAPIResponse,
408
+ requestedModel: string,
409
+ rawRequest: OpenAIResponsesAPIRequest
410
+ ): OpenAIResponsesAPIProviderResponse {
411
+ const outputItems = Array.isArray(response.output) ? response.output : [];
412
+ const content = this.outputToContent(outputItems);
413
+ const cachedTokens = response.usage?.input_tokens_details?.cached_tokens ?? 0;
414
+
415
+ return {
416
+ content,
417
+ outputItems,
418
+ stopReason: this.getStopReason(response, outputItems),
419
+ stopSequence: undefined,
420
+ usage: {
421
+ inputTokens: response.usage?.input_tokens ?? 0,
422
+ outputTokens: response.usage?.output_tokens ?? 0,
423
+ cacheReadTokens: cachedTokens > 0 ? cachedTokens : undefined,
424
+ },
425
+ model: response.model ?? requestedModel,
426
+ rawRequest,
427
+ raw: response,
428
+ };
429
+ }
430
+
431
+ private outputToContent(items: OpenAIResponsesOutputItem[]): OpenAIResponsesAPIContentBlock[] {
432
+ const content: OpenAIResponsesAPIContentBlock[] = [];
433
+
434
+ items.forEach((item, outputIndex) => {
435
+ if (item.type === 'message') {
436
+ const phase = this.asPhase(item.phase);
437
+ const messageContent = Array.isArray(item.content) ? item.content : [];
438
+ messageContent.forEach((part: any, contentIndex: number) => {
439
+ if (part?.type === 'output_text' && typeof part.text === 'string') {
440
+ content.push({
441
+ type: 'text',
442
+ text: part.text,
443
+ itemId: item.id,
444
+ outputIndex,
445
+ contentIndex,
446
+ phase,
447
+ rawItem: item,
448
+ });
449
+ } else if (part?.type === 'refusal' && typeof part.refusal === 'string') {
450
+ content.push({
451
+ type: 'text',
452
+ text: part.refusal,
453
+ itemId: item.id,
454
+ outputIndex,
455
+ contentIndex,
456
+ phase,
457
+ rawItem: item,
458
+ });
459
+ }
460
+ });
461
+ } else if (item.type === 'reasoning') {
462
+ if (typeof item.encrypted_content === 'string') {
463
+ content.push({
464
+ type: 'redacted_thinking',
465
+ data: item.encrypted_content,
466
+ itemId: item.id,
467
+ outputIndex,
468
+ rawItem: item,
469
+ });
470
+ } else {
471
+ const thinking = this.extractReasoningText(item);
472
+ content.push({
473
+ type: 'thinking',
474
+ thinking,
475
+ itemId: item.id,
476
+ outputIndex,
477
+ rawItem: item,
478
+ });
479
+ }
480
+ } else if (item.type === 'function_call') {
481
+ const callId = typeof item.call_id === 'string' ? item.call_id : item.id ?? '';
482
+ content.push({
483
+ type: 'tool_use',
484
+ id: callId,
485
+ name: typeof item.name === 'string' ? item.name : '',
486
+ input: safeParseJson(typeof item.arguments === 'string' ? item.arguments : '{}'),
487
+ itemId: item.id,
488
+ outputIndex,
489
+ rawItem: item,
490
+ });
491
+ } else if (item.type === 'function_call_output') {
492
+ content.push({
493
+ type: 'tool_result',
494
+ toolUseId: typeof item.call_id === 'string' ? item.call_id : '',
495
+ content:
496
+ typeof item.output === 'string' ? item.output : JSON.stringify(item.output ?? null),
497
+ itemId: item.id,
498
+ outputIndex,
499
+ rawItem: item,
500
+ });
501
+ } else if (item.type === 'compaction' && typeof item.encrypted_content === 'string') {
502
+ content.push({
503
+ type: 'compaction',
504
+ id: item.id,
505
+ encryptedContent: item.encrypted_content,
506
+ ...(typeof item.created_by === 'string' ? { createdBy: item.created_by } : {}),
507
+ outputIndex,
508
+ rawItem: item,
509
+ });
510
+ } else {
511
+ content.push({
512
+ type: 'openai_response_item',
513
+ itemId: item.id,
514
+ itemType: item.type,
515
+ outputIndex,
516
+ rawItem: item,
517
+ });
518
+ }
519
+ });
520
+
521
+ return content;
522
+ }
523
+
524
+ private extractReasoningText(item: OpenAIResponsesOutputItem): string {
525
+ const values = [item.summary, item.content]
526
+ .filter(Array.isArray)
527
+ .flatMap((parts) => parts as unknown[])
528
+ .map((part: any) => part?.text)
529
+ .filter((text): text is string => typeof text === 'string');
530
+ return values.join('\n');
531
+ }
532
+
533
+ private asPhase(value: unknown): 'commentary' | 'final_answer' | null | undefined {
534
+ return value === 'commentary' || value === 'final_answer' || value === null
535
+ ? value
536
+ : undefined;
537
+ }
538
+
539
+ private getStopReason(
540
+ response: OpenAIResponsesAPIResponse,
541
+ output: OpenAIResponsesOutputItem[]
542
+ ): string {
543
+ if (output.some((item) => item.type === 'function_call')) return 'tool_use';
544
+ if (output.some((item) =>
545
+ item.type === 'message' &&
546
+ Array.isArray(item.content) &&
547
+ item.content.some((part: any) => part?.type === 'refusal')
548
+ )) return 'refusal';
549
+
550
+ const reason = response.incomplete_details?.reason;
551
+ if (response.status === 'incomplete' && reason?.includes('max_output_tokens')) {
552
+ return 'max_tokens';
553
+ }
554
+ return 'end_turn';
555
+ }
556
+
557
+ private applyTextDelta(output: OpenAIResponsesOutputItem[], event: any): void {
558
+ if (!Number.isInteger(event.output_index) || typeof event.delta !== 'string') return;
559
+ const outputIndex = event.output_index as number;
560
+ const contentIndex = Number.isInteger(event.content_index) ? event.content_index : 0;
561
+ const existing = output[outputIndex] as any;
562
+ const message = existing?.type === 'message'
563
+ ? existing
564
+ : {
565
+ type: 'message',
566
+ id: event.item_id,
567
+ role: 'assistant',
568
+ status: 'in_progress',
569
+ content: [],
570
+ };
571
+ const part = message.content[contentIndex] ?? { type: 'output_text', text: '', annotations: [] };
572
+ part.text = `${part.text ?? ''}${event.delta}`;
573
+ message.content[contentIndex] = part;
574
+ output[outputIndex] = message;
575
+ }
576
+
577
+ private applyFunctionArgumentsDelta(output: OpenAIResponsesOutputItem[], event: any): void {
578
+ if (!Number.isInteger(event.output_index) || typeof event.delta !== 'string') return;
579
+ const outputIndex = event.output_index as number;
580
+ const item = output[outputIndex] as any;
581
+ if (item?.type === 'function_call') {
582
+ item.arguments = `${item.arguments ?? ''}${event.delta}`;
583
+ }
584
+ }
585
+
586
+ // --------------------------------------------------------------------------
587
+ // Errors
588
+ // --------------------------------------------------------------------------
589
+
590
+ private async assertSuccessfulHTTPResponse(response: Response): Promise<void> {
591
+ if (response.ok) return;
592
+ const errorText = await response.text();
593
+ throw new Error(`OpenAI Responses API error: ${response.status} ${errorText}`);
594
+ }
595
+
596
+ private assertSuccessfulAPIResponse(response: OpenAIResponsesAPIResponse): void {
597
+ if (!response.error) return;
598
+ throw new Error(
599
+ `OpenAI Responses API error: ${response.error.code ?? 'api_error'} ` +
600
+ `${response.error.message ?? 'Unknown error'}`
601
+ );
602
+ }
603
+
604
+ private handleError(error: unknown, rawRequest?: unknown): MembraneError {
605
+ if (error instanceof MembraneError) return error;
606
+ if (error instanceof Error) {
607
+ const message = error.message;
608
+ if (message.includes('429') || message.includes('rate_limit')) {
609
+ const retryMatch = message.match(/retry after (\d+)/i);
610
+ const retryAfter = retryMatch?.[1] ? Number(retryMatch[1]) * 1000 : undefined;
611
+ return rateLimitError(message, retryAfter, error, rawRequest);
612
+ }
613
+ if (
614
+ message.includes('401') ||
615
+ message.includes('invalid_api_key') ||
616
+ message.includes('Incorrect API key')
617
+ ) {
618
+ return authError(message, error, rawRequest);
619
+ }
620
+ if (
621
+ message.includes('context_length') ||
622
+ message.includes('maximum context') ||
623
+ message.includes('too long')
624
+ ) {
625
+ return contextLengthError(message, error, rawRequest);
626
+ }
627
+ if (
628
+ message.includes('500') ||
629
+ message.includes('502') ||
630
+ message.includes('503') ||
631
+ message.includes('server_error')
632
+ ) {
633
+ return serverError(message, undefined, error, rawRequest);
634
+ }
635
+ if (error.name === 'AbortError') return abortError(undefined, rawRequest);
636
+ if (
637
+ message.includes('network') ||
638
+ message.includes('fetch') ||
639
+ message.includes('ECONNREFUSED')
640
+ ) {
641
+ return networkError(message, error, rawRequest);
642
+ }
643
+ }
644
+
645
+ return new MembraneError({
646
+ type: 'unknown',
647
+ message: error instanceof Error ? error.message : String(error),
648
+ retryable: false,
649
+ rawError: error,
650
+ rawRequest,
651
+ });
652
+ }
653
+ }