@animalabs/membrane 0.5.69 → 0.5.70
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.
- package/dist/formatters/index.d.ts +1 -0
- package/dist/formatters/index.d.ts.map +1 -1
- package/dist/formatters/index.js +1 -0
- package/dist/formatters/index.js.map +1 -1
- package/dist/formatters/openai-responses.d.ts +23 -0
- package/dist/formatters/openai-responses.d.ts.map +1 -0
- package/dist/formatters/openai-responses.js +176 -0
- package/dist/formatters/openai-responses.js.map +1 -0
- package/dist/membrane.d.ts.map +1 -1
- package/dist/membrane.js +21 -2
- package/dist/membrane.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +28 -3
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai-responses-api.d.ts +129 -0
- package/dist/providers/openai-responses-api.d.ts.map +1 -0
- package/dist/providers/openai-responses-api.js +475 -0
- package/dist/providers/openai-responses-api.js.map +1 -0
- package/package.json +1 -1
- package/src/formatters/index.ts +4 -0
- package/src/formatters/openai-responses.ts +206 -0
- package/src/membrane.ts +23 -4
- package/src/providers/anthropic.ts +29 -4
- package/src/providers/index.ts +11 -0
- package/src/providers/openai-responses-api.ts +644 -0
|
@@ -0,0 +1,644 @@
|
|
|
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
|
+
const completed: OpenAIResponsesAPIResponse = terminalResponse ?? {
|
|
269
|
+
model: request.model,
|
|
270
|
+
output: output.filter((item): item is OpenAIResponsesOutputItem => Boolean(item)),
|
|
271
|
+
status: 'completed',
|
|
272
|
+
stream_events: events,
|
|
273
|
+
};
|
|
274
|
+
this.assertSuccessfulAPIResponse(completed);
|
|
275
|
+
|
|
276
|
+
const parsed = this.parseResponse(completed, request.model, responsesRequest);
|
|
277
|
+
parsed.content.forEach((block, index) => callbacks.onContentBlock?.(index, block));
|
|
278
|
+
return parsed;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
throw this.handleError(error, responsesRequest);
|
|
281
|
+
} finally {
|
|
282
|
+
cleanup?.();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// --------------------------------------------------------------------------
|
|
287
|
+
// Request construction
|
|
288
|
+
// --------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
private getHeaders(): Record<string, string> {
|
|
291
|
+
return {
|
|
292
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
293
|
+
'Content-Type': 'application/json',
|
|
294
|
+
...(this.organization ? { 'OpenAI-Organization': this.organization } : {}),
|
|
295
|
+
...(this.project ? { 'OpenAI-Project': this.project } : {}),
|
|
296
|
+
...this.extraHeaders,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private buildRequest(request: ProviderRequest): OpenAIResponsesAPIRequest {
|
|
301
|
+
if (!Array.isArray(request.messages)) {
|
|
302
|
+
throw new Error('OpenAI Responses API input must be a provider-native input-item array');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const responsesRequest: OpenAIResponsesAPIRequest = {
|
|
306
|
+
model: request.model,
|
|
307
|
+
input: request.messages as OpenAIResponsesInputItem[],
|
|
308
|
+
store: false,
|
|
309
|
+
include: ['reasoning.encrypted_content'],
|
|
310
|
+
max_output_tokens: request.maxTokens || this.defaultMaxTokens,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const instructions = this.flattenInstructions(request.system);
|
|
314
|
+
if (instructions) responsesRequest.instructions = instructions;
|
|
315
|
+
if (request.temperature !== undefined) responsesRequest.temperature = request.temperature;
|
|
316
|
+
if (request.topP !== undefined) responsesRequest.top_p = request.topP;
|
|
317
|
+
if (request.tools?.length) responsesRequest.tools = this.convertTools(request.tools);
|
|
318
|
+
|
|
319
|
+
if (request.extra) {
|
|
320
|
+
const {
|
|
321
|
+
normalizedMessages,
|
|
322
|
+
prompt,
|
|
323
|
+
messages,
|
|
324
|
+
input,
|
|
325
|
+
store,
|
|
326
|
+
stream,
|
|
327
|
+
include,
|
|
328
|
+
...extra
|
|
329
|
+
} = request.extra;
|
|
330
|
+
void normalizedMessages;
|
|
331
|
+
void prompt;
|
|
332
|
+
void messages;
|
|
333
|
+
void input;
|
|
334
|
+
void store;
|
|
335
|
+
void stream;
|
|
336
|
+
Object.assign(responsesRequest, extra);
|
|
337
|
+
responsesRequest.include = this.mergeEncryptedReasoningInclude(include);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// These invariants define the adapter's stateless native-item contract and
|
|
341
|
+
// cannot be overridden through provider params.
|
|
342
|
+
responsesRequest.input = request.messages as OpenAIResponsesInputItem[];
|
|
343
|
+
responsesRequest.store = false;
|
|
344
|
+
responsesRequest.include = this.mergeEncryptedReasoningInclude(responsesRequest.include);
|
|
345
|
+
return responsesRequest;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private mergeEncryptedReasoningInclude(value: unknown): string[] {
|
|
349
|
+
const include = Array.isArray(value)
|
|
350
|
+
? value.filter((item): item is string => typeof item === 'string')
|
|
351
|
+
: [];
|
|
352
|
+
return include.includes('reasoning.encrypted_content')
|
|
353
|
+
? include
|
|
354
|
+
: [...include, 'reasoning.encrypted_content'];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private flattenInstructions(system: ProviderRequest['system']): string | undefined {
|
|
358
|
+
if (typeof system === 'string') return system || undefined;
|
|
359
|
+
if (!Array.isArray(system)) return undefined;
|
|
360
|
+
const text = system
|
|
361
|
+
.map((block: any) =>
|
|
362
|
+
block?.type === 'text' || block?.type === 'input_text' ? block.text : undefined
|
|
363
|
+
)
|
|
364
|
+
.filter((value): value is string => typeof value === 'string' && value.length > 0)
|
|
365
|
+
.join('\n');
|
|
366
|
+
return text || undefined;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private convertTools(tools: unknown[]): unknown[] {
|
|
370
|
+
return tools.map((rawTool: any) => {
|
|
371
|
+
if (rawTool?.type && rawTool.type !== 'function') return rawTool;
|
|
372
|
+
|
|
373
|
+
// Responses function definitions are flat. Accept them verbatim, while
|
|
374
|
+
// also adapting Membrane and Chat Completions function schemas.
|
|
375
|
+
if (rawTool?.type === 'function' && rawTool.name) return rawTool;
|
|
376
|
+
if (rawTool?.type === 'function' && rawTool.function) {
|
|
377
|
+
return { type: 'function', ...rawTool.function };
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
type: 'function',
|
|
381
|
+
name: rawTool?.name,
|
|
382
|
+
description: rawTool?.description,
|
|
383
|
+
parameters:
|
|
384
|
+
rawTool?.parameters ??
|
|
385
|
+
rawTool?.inputSchema ??
|
|
386
|
+
rawTool?.input_schema ??
|
|
387
|
+
{ type: 'object', properties: {} },
|
|
388
|
+
...(rawTool?.strict !== undefined ? { strict: rawTool.strict } : {}),
|
|
389
|
+
};
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// --------------------------------------------------------------------------
|
|
394
|
+
// Response conversion
|
|
395
|
+
// --------------------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
private parseResponse(
|
|
398
|
+
response: OpenAIResponsesAPIResponse,
|
|
399
|
+
requestedModel: string,
|
|
400
|
+
rawRequest: OpenAIResponsesAPIRequest
|
|
401
|
+
): OpenAIResponsesAPIProviderResponse {
|
|
402
|
+
const outputItems = Array.isArray(response.output) ? response.output : [];
|
|
403
|
+
const content = this.outputToContent(outputItems);
|
|
404
|
+
const cachedTokens = response.usage?.input_tokens_details?.cached_tokens ?? 0;
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
content,
|
|
408
|
+
outputItems,
|
|
409
|
+
stopReason: this.getStopReason(response, outputItems),
|
|
410
|
+
stopSequence: undefined,
|
|
411
|
+
usage: {
|
|
412
|
+
inputTokens: response.usage?.input_tokens ?? 0,
|
|
413
|
+
outputTokens: response.usage?.output_tokens ?? 0,
|
|
414
|
+
cacheReadTokens: cachedTokens > 0 ? cachedTokens : undefined,
|
|
415
|
+
},
|
|
416
|
+
model: response.model ?? requestedModel,
|
|
417
|
+
rawRequest,
|
|
418
|
+
raw: response,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private outputToContent(items: OpenAIResponsesOutputItem[]): OpenAIResponsesAPIContentBlock[] {
|
|
423
|
+
const content: OpenAIResponsesAPIContentBlock[] = [];
|
|
424
|
+
|
|
425
|
+
items.forEach((item, outputIndex) => {
|
|
426
|
+
if (item.type === 'message') {
|
|
427
|
+
const phase = this.asPhase(item.phase);
|
|
428
|
+
const messageContent = Array.isArray(item.content) ? item.content : [];
|
|
429
|
+
messageContent.forEach((part: any, contentIndex: number) => {
|
|
430
|
+
if (part?.type === 'output_text' && typeof part.text === 'string') {
|
|
431
|
+
content.push({
|
|
432
|
+
type: 'text',
|
|
433
|
+
text: part.text,
|
|
434
|
+
itemId: item.id,
|
|
435
|
+
outputIndex,
|
|
436
|
+
contentIndex,
|
|
437
|
+
phase,
|
|
438
|
+
rawItem: item,
|
|
439
|
+
});
|
|
440
|
+
} else if (part?.type === 'refusal' && typeof part.refusal === 'string') {
|
|
441
|
+
content.push({
|
|
442
|
+
type: 'text',
|
|
443
|
+
text: part.refusal,
|
|
444
|
+
itemId: item.id,
|
|
445
|
+
outputIndex,
|
|
446
|
+
contentIndex,
|
|
447
|
+
phase,
|
|
448
|
+
rawItem: item,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
} else if (item.type === 'reasoning') {
|
|
453
|
+
if (typeof item.encrypted_content === 'string') {
|
|
454
|
+
content.push({
|
|
455
|
+
type: 'redacted_thinking',
|
|
456
|
+
data: item.encrypted_content,
|
|
457
|
+
itemId: item.id,
|
|
458
|
+
outputIndex,
|
|
459
|
+
rawItem: item,
|
|
460
|
+
});
|
|
461
|
+
} else {
|
|
462
|
+
const thinking = this.extractReasoningText(item);
|
|
463
|
+
content.push({
|
|
464
|
+
type: 'thinking',
|
|
465
|
+
thinking,
|
|
466
|
+
itemId: item.id,
|
|
467
|
+
outputIndex,
|
|
468
|
+
rawItem: item,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
} else if (item.type === 'function_call') {
|
|
472
|
+
const callId = typeof item.call_id === 'string' ? item.call_id : item.id ?? '';
|
|
473
|
+
content.push({
|
|
474
|
+
type: 'tool_use',
|
|
475
|
+
id: callId,
|
|
476
|
+
name: typeof item.name === 'string' ? item.name : '',
|
|
477
|
+
input: safeParseJson(typeof item.arguments === 'string' ? item.arguments : '{}'),
|
|
478
|
+
itemId: item.id,
|
|
479
|
+
outputIndex,
|
|
480
|
+
rawItem: item,
|
|
481
|
+
});
|
|
482
|
+
} else if (item.type === 'function_call_output') {
|
|
483
|
+
content.push({
|
|
484
|
+
type: 'tool_result',
|
|
485
|
+
toolUseId: typeof item.call_id === 'string' ? item.call_id : '',
|
|
486
|
+
content:
|
|
487
|
+
typeof item.output === 'string' ? item.output : JSON.stringify(item.output ?? null),
|
|
488
|
+
itemId: item.id,
|
|
489
|
+
outputIndex,
|
|
490
|
+
rawItem: item,
|
|
491
|
+
});
|
|
492
|
+
} else if (item.type === 'compaction' && typeof item.encrypted_content === 'string') {
|
|
493
|
+
content.push({
|
|
494
|
+
type: 'compaction',
|
|
495
|
+
id: item.id,
|
|
496
|
+
encryptedContent: item.encrypted_content,
|
|
497
|
+
...(typeof item.created_by === 'string' ? { createdBy: item.created_by } : {}),
|
|
498
|
+
outputIndex,
|
|
499
|
+
rawItem: item,
|
|
500
|
+
});
|
|
501
|
+
} else {
|
|
502
|
+
content.push({
|
|
503
|
+
type: 'openai_response_item',
|
|
504
|
+
itemId: item.id,
|
|
505
|
+
itemType: item.type,
|
|
506
|
+
outputIndex,
|
|
507
|
+
rawItem: item,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
return content;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private extractReasoningText(item: OpenAIResponsesOutputItem): string {
|
|
516
|
+
const values = [item.summary, item.content]
|
|
517
|
+
.filter(Array.isArray)
|
|
518
|
+
.flatMap((parts) => parts as unknown[])
|
|
519
|
+
.map((part: any) => part?.text)
|
|
520
|
+
.filter((text): text is string => typeof text === 'string');
|
|
521
|
+
return values.join('\n');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private asPhase(value: unknown): 'commentary' | 'final_answer' | null | undefined {
|
|
525
|
+
return value === 'commentary' || value === 'final_answer' || value === null
|
|
526
|
+
? value
|
|
527
|
+
: undefined;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private getStopReason(
|
|
531
|
+
response: OpenAIResponsesAPIResponse,
|
|
532
|
+
output: OpenAIResponsesOutputItem[]
|
|
533
|
+
): string {
|
|
534
|
+
if (output.some((item) => item.type === 'function_call')) return 'tool_use';
|
|
535
|
+
if (output.some((item) =>
|
|
536
|
+
item.type === 'message' &&
|
|
537
|
+
Array.isArray(item.content) &&
|
|
538
|
+
item.content.some((part: any) => part?.type === 'refusal')
|
|
539
|
+
)) return 'refusal';
|
|
540
|
+
|
|
541
|
+
const reason = response.incomplete_details?.reason;
|
|
542
|
+
if (response.status === 'incomplete' && reason?.includes('max_output_tokens')) {
|
|
543
|
+
return 'max_tokens';
|
|
544
|
+
}
|
|
545
|
+
return 'end_turn';
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private applyTextDelta(output: OpenAIResponsesOutputItem[], event: any): void {
|
|
549
|
+
if (!Number.isInteger(event.output_index) || typeof event.delta !== 'string') return;
|
|
550
|
+
const outputIndex = event.output_index as number;
|
|
551
|
+
const contentIndex = Number.isInteger(event.content_index) ? event.content_index : 0;
|
|
552
|
+
const existing = output[outputIndex] as any;
|
|
553
|
+
const message = existing?.type === 'message'
|
|
554
|
+
? existing
|
|
555
|
+
: {
|
|
556
|
+
type: 'message',
|
|
557
|
+
id: event.item_id,
|
|
558
|
+
role: 'assistant',
|
|
559
|
+
status: 'in_progress',
|
|
560
|
+
content: [],
|
|
561
|
+
};
|
|
562
|
+
const part = message.content[contentIndex] ?? { type: 'output_text', text: '', annotations: [] };
|
|
563
|
+
part.text = `${part.text ?? ''}${event.delta}`;
|
|
564
|
+
message.content[contentIndex] = part;
|
|
565
|
+
output[outputIndex] = message;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private applyFunctionArgumentsDelta(output: OpenAIResponsesOutputItem[], event: any): void {
|
|
569
|
+
if (!Number.isInteger(event.output_index) || typeof event.delta !== 'string') return;
|
|
570
|
+
const outputIndex = event.output_index as number;
|
|
571
|
+
const item = output[outputIndex] as any;
|
|
572
|
+
if (item?.type === 'function_call') {
|
|
573
|
+
item.arguments = `${item.arguments ?? ''}${event.delta}`;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// --------------------------------------------------------------------------
|
|
578
|
+
// Errors
|
|
579
|
+
// --------------------------------------------------------------------------
|
|
580
|
+
|
|
581
|
+
private async assertSuccessfulHTTPResponse(response: Response): Promise<void> {
|
|
582
|
+
if (response.ok) return;
|
|
583
|
+
const errorText = await response.text();
|
|
584
|
+
throw new Error(`OpenAI Responses API error: ${response.status} ${errorText}`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
private assertSuccessfulAPIResponse(response: OpenAIResponsesAPIResponse): void {
|
|
588
|
+
if (!response.error) return;
|
|
589
|
+
throw new Error(
|
|
590
|
+
`OpenAI Responses API error: ${response.error.code ?? 'api_error'} ` +
|
|
591
|
+
`${response.error.message ?? 'Unknown error'}`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private handleError(error: unknown, rawRequest?: unknown): MembraneError {
|
|
596
|
+
if (error instanceof MembraneError) return error;
|
|
597
|
+
if (error instanceof Error) {
|
|
598
|
+
const message = error.message;
|
|
599
|
+
if (message.includes('429') || message.includes('rate_limit')) {
|
|
600
|
+
const retryMatch = message.match(/retry after (\d+)/i);
|
|
601
|
+
const retryAfter = retryMatch?.[1] ? Number(retryMatch[1]) * 1000 : undefined;
|
|
602
|
+
return rateLimitError(message, retryAfter, error, rawRequest);
|
|
603
|
+
}
|
|
604
|
+
if (
|
|
605
|
+
message.includes('401') ||
|
|
606
|
+
message.includes('invalid_api_key') ||
|
|
607
|
+
message.includes('Incorrect API key')
|
|
608
|
+
) {
|
|
609
|
+
return authError(message, error, rawRequest);
|
|
610
|
+
}
|
|
611
|
+
if (
|
|
612
|
+
message.includes('context_length') ||
|
|
613
|
+
message.includes('maximum context') ||
|
|
614
|
+
message.includes('too long')
|
|
615
|
+
) {
|
|
616
|
+
return contextLengthError(message, error, rawRequest);
|
|
617
|
+
}
|
|
618
|
+
if (
|
|
619
|
+
message.includes('500') ||
|
|
620
|
+
message.includes('502') ||
|
|
621
|
+
message.includes('503') ||
|
|
622
|
+
message.includes('server_error')
|
|
623
|
+
) {
|
|
624
|
+
return serverError(message, undefined, error, rawRequest);
|
|
625
|
+
}
|
|
626
|
+
if (error.name === 'AbortError') return abortError(undefined, rawRequest);
|
|
627
|
+
if (
|
|
628
|
+
message.includes('network') ||
|
|
629
|
+
message.includes('fetch') ||
|
|
630
|
+
message.includes('ECONNREFUSED')
|
|
631
|
+
) {
|
|
632
|
+
return networkError(message, error, rawRequest);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return new MembraneError({
|
|
637
|
+
type: 'unknown',
|
|
638
|
+
message: error instanceof Error ? error.message : String(error),
|
|
639
|
+
retryable: false,
|
|
640
|
+
rawError: error,
|
|
641
|
+
rawRequest,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|