@animalabs/membrane 0.5.83 → 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.
- package/dist/formatters/openai-responses.js +1 -1
- package/dist/formatters/openai-responses.js.map +1 -1
- package/dist/membrane.d.ts +3 -1
- package/dist/membrane.d.ts.map +1 -1
- package/dist/membrane.js +6 -2
- package/dist/membrane.js.map +1 -1
- package/dist/providers/anthropic.d.ts +11 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +67 -10
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/credentials.d.ts +20 -0
- package/dist/providers/credentials.d.ts.map +1 -0
- package/dist/providers/credentials.js +53 -0
- package/dist/providers/credentials.js.map +1 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/openai-responses-api.d.ts +22 -4
- package/dist/providers/openai-responses-api.d.ts.map +1 -1
- package/dist/providers/openai-responses-api.js +107 -42
- package/dist/providers/openai-responses-api.js.map +1 -1
- package/dist/providers/openai-responses.d.ts +61 -5
- package/dist/providers/openai-responses.d.ts.map +1 -1
- package/dist/providers/openai-responses.js +133 -22
- package/dist/providers/openai-responses.js.map +1 -1
- package/dist/providers/responses-input.d.ts +11 -0
- package/dist/providers/responses-input.d.ts.map +1 -0
- package/dist/providers/responses-input.js +128 -0
- package/dist/providers/responses-input.js.map +1 -0
- package/dist/providers/utils.d.ts +7 -3
- package/dist/providers/utils.d.ts.map +1 -1
- package/dist/providers/utils.js +28 -3
- package/dist/providers/utils.js.map +1 -1
- package/dist/types/provider.d.ts +4 -0
- package/dist/types/provider.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/formatters/openai-responses.ts +1 -1
- package/src/membrane.ts +6 -2
- package/src/providers/anthropic.ts +79 -13
- package/src/providers/credentials.ts +77 -0
- package/src/providers/index.ts +2 -0
- package/src/providers/openai-responses-api.ts +118 -43
- package/src/providers/openai-responses.ts +174 -25
- package/src/providers/responses-input.ts +131 -0
- package/src/providers/utils.ts +24 -3
- package/src/types/provider.ts +5 -0
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from '../types/index.js';
|
|
25
25
|
import { flattenRootSchemaUnion } from './anthropic-tool-schema.js';
|
|
26
26
|
import { assertTerminalEventObserved } from './utils.js';
|
|
27
|
+
import { fetchWithCredentials, validateCredential, type CredentialContext, type CredentialResolver } from './credentials.js';
|
|
27
28
|
import { CacheKeepalive, type CacheKeepaliveConfig } from '../cache-keepalive.js';
|
|
28
29
|
|
|
29
30
|
// ============================================================================
|
|
@@ -151,7 +152,12 @@ export interface AnthropicAdapterConfig {
|
|
|
151
152
|
* is allowed to resolve environment auth). If explicitly provided, API-key
|
|
152
153
|
* auth is disabled so requests do not send both auth schemes.
|
|
153
154
|
*/
|
|
154
|
-
authToken?: string | null;
|
|
155
|
+
authToken?: string | null | ((context: CredentialContext) => string | Promise<string>);
|
|
156
|
+
|
|
157
|
+
/** Resolve a bearer token and associated headers per HTTP attempt. Takes
|
|
158
|
+
* precedence over authToken/apiKey; refreshed once after HTTP 401. Applies
|
|
159
|
+
* to complete, stream, and cache-keepalive requests. */
|
|
160
|
+
credentials?: CredentialResolver;
|
|
155
161
|
|
|
156
162
|
/** Base URL override */
|
|
157
163
|
baseURL?: string;
|
|
@@ -199,6 +205,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
199
205
|
*/
|
|
200
206
|
readonly usageCacheConvention = 'cache-excluded' as const;
|
|
201
207
|
private client: Anthropic;
|
|
208
|
+
private readonly credentials?: CredentialResolver;
|
|
202
209
|
private defaultMaxTokens: number;
|
|
203
210
|
/** Any anthropic-beta value from defaultHeaders (e.g. the oauth beta for
|
|
204
211
|
* subscription tokens). Per-request headers REPLACE same-key defaults in
|
|
@@ -218,8 +225,19 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
218
225
|
this.defaultBeta = extractBetaHeader(config.defaultHeaders);
|
|
219
226
|
this.dynamicHeaders = config.dynamicHeaders;
|
|
220
227
|
|
|
221
|
-
|
|
222
|
-
|
|
228
|
+
const authToken = config.authToken;
|
|
229
|
+
const credentials: CredentialResolver | undefined = config.credentials ?? (typeof authToken === 'function'
|
|
230
|
+
? async (context: CredentialContext) => ({ token: await authToken(context) })
|
|
231
|
+
: undefined);
|
|
232
|
+
this.credentials = credentials;
|
|
233
|
+
if (credentials) {
|
|
234
|
+
// Satisfy SDK auth validation without freezing a real credential. The
|
|
235
|
+
// fetch seam replaces this placeholder before every network attempt,
|
|
236
|
+
// including the SDK's stream and cache-keepalive transports.
|
|
237
|
+
clientOptions.authToken = 'membrane-resolved-at-request-time';
|
|
238
|
+
clientOptions.apiKey = null;
|
|
239
|
+
} else if (authToken !== undefined) {
|
|
240
|
+
clientOptions.authToken = authToken as string | null;
|
|
223
241
|
clientOptions.apiKey = null;
|
|
224
242
|
} else {
|
|
225
243
|
clientOptions.apiKey = config.apiKey;
|
|
@@ -234,14 +252,62 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
234
252
|
// Replay path. Deliberately bypasses buildRequest(): the payload is
|
|
235
253
|
// the already-built wire request from a real call, and rebuilding it
|
|
236
254
|
// risks a byte diff that silently converts a 0.1x read into a 2x write.
|
|
237
|
-
async (wire, headers) =>
|
|
238
|
-
wire as unknown as Anthropic.MessageCreateParamsNonStreaming,
|
|
239
|
-
headers ? { headers } : undefined,
|
|
255
|
+
async (wire, headers) => this.createMessage(
|
|
256
|
+
wire as unknown as Anthropic.MessageCreateParamsNonStreaming, headers,
|
|
240
257
|
),
|
|
241
258
|
config.cacheKeepalive ?? {},
|
|
242
259
|
);
|
|
243
260
|
}
|
|
244
261
|
|
|
262
|
+
/** One SDK operation owns its failure state: concurrent calls cannot
|
|
263
|
+
* overwrite each other's auth error or cancel each other's requests. */
|
|
264
|
+
private credentialSession(signal?: AbortSignal) {
|
|
265
|
+
const credentials = this.credentials;
|
|
266
|
+
if (!credentials) return { client: this.client, signal, failure: () => undefined };
|
|
267
|
+
const abort = new AbortController();
|
|
268
|
+
const combined = signal ? AbortSignal.any([signal, abort.signal]) : abort.signal;
|
|
269
|
+
let failure: MembraneError | undefined;
|
|
270
|
+
const client = this.client.withOptions({
|
|
271
|
+
fetch: (input, init) => {
|
|
272
|
+
const headers = new Headers(init?.headers);
|
|
273
|
+
headers.delete('x-api-key');
|
|
274
|
+
return fetchWithCredentials(input, { ...init, headers }, async context => {
|
|
275
|
+
try {
|
|
276
|
+
const credential = await credentials(context);
|
|
277
|
+
validateCredential(credential);
|
|
278
|
+
const resolvedHeaders = new Headers(credential.headers);
|
|
279
|
+
resolvedHeaders.delete('x-api-key');
|
|
280
|
+
return { ...credential, headers: Object.fromEntries(resolvedHeaders) };
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (combined.aborted) throw error;
|
|
283
|
+
failure = error instanceof MembraneError ? error : authError(
|
|
284
|
+
`Credential resolution failed: ${error instanceof Error ? error.message : String(error)}`, error,
|
|
285
|
+
);
|
|
286
|
+
// SDK 0.52 retries thrown fetch errors as connection failures.
|
|
287
|
+
// Abort this operation to bypass that loop, then restore the
|
|
288
|
+
// original error at the complete/stream/keepalive boundary.
|
|
289
|
+
abort.abort(failure);
|
|
290
|
+
throw failure;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
return { client, signal: combined, failure: () => failure };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private async createMessage(
|
|
299
|
+
request: Anthropic.MessageCreateParamsNonStreaming,
|
|
300
|
+
headers?: Record<string, string>,
|
|
301
|
+
signal?: AbortSignal,
|
|
302
|
+
): Promise<Anthropic.Message> {
|
|
303
|
+
const session = this.credentialSession(signal);
|
|
304
|
+
try {
|
|
305
|
+
return await session.client.messages.create(request, { headers, signal: session.signal });
|
|
306
|
+
} catch (error) {
|
|
307
|
+
throw session.failure() ?? error;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
245
311
|
supportsModel(modelId: string): boolean {
|
|
246
312
|
return modelId.startsWith('claude-');
|
|
247
313
|
}
|
|
@@ -260,10 +326,9 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
260
326
|
);
|
|
261
327
|
|
|
262
328
|
try {
|
|
263
|
-
const response = await this.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
});
|
|
329
|
+
const response = await this.createMessage(
|
|
330
|
+
fullRequest, this.liveHeaders(headers, 'complete'), options?.signal,
|
|
331
|
+
);
|
|
267
332
|
|
|
268
333
|
return this.parseResponse(response, fullRequest);
|
|
269
334
|
} catch (error) {
|
|
@@ -331,10 +396,11 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
331
396
|
};
|
|
332
397
|
|
|
333
398
|
resetIdleTimer();
|
|
399
|
+
const session = this.credentialSession(idleAbort.signal);
|
|
334
400
|
|
|
335
401
|
try {
|
|
336
|
-
const stream = await
|
|
337
|
-
signal:
|
|
402
|
+
const stream = await session.client.messages.stream(anthropicRequest, {
|
|
403
|
+
signal: session.signal,
|
|
338
404
|
headers: this.liveHeaders(this.betaHeaders(request), 'stream'),
|
|
339
405
|
});
|
|
340
406
|
|
|
@@ -572,7 +638,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
572
638
|
rawRequest: fullRequest,
|
|
573
639
|
});
|
|
574
640
|
}
|
|
575
|
-
throw this.handleError(error, fullRequest);
|
|
641
|
+
throw this.handleError(session.failure() ?? error, fullRequest);
|
|
576
642
|
}
|
|
577
643
|
}
|
|
578
644
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { authError } from '../types/index.js';
|
|
2
|
+
|
|
3
|
+
/** Credential acquisition stays with the application; transports resolve per attempt. */
|
|
4
|
+
export interface ResolvedCredential {
|
|
5
|
+
token: string;
|
|
6
|
+
/** Headers tied to this token, e.g. ChatGPT-Account-Id. Returned atomically. */
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CredentialContext {
|
|
11
|
+
/** False for the first attempt; true for the single retry after HTTP 401. */
|
|
12
|
+
forceRefresh: boolean;
|
|
13
|
+
/** The request's combined cancellation/deadline signal. */
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type CredentialResolver = (
|
|
18
|
+
context: CredentialContext,
|
|
19
|
+
) => ResolvedCredential | Promise<ResolvedCredential>;
|
|
20
|
+
|
|
21
|
+
/** Internal shared HTTP seam: refresh only a 401, before a body is consumed.
|
|
22
|
+
* Static credentials retain their single-attempt behavior. SDK-backed callers
|
|
23
|
+
* can use this as their fetch implementation without rebuilding a client. */
|
|
24
|
+
export async function fetchWithCredentials(
|
|
25
|
+
input: string | URL | Request,
|
|
26
|
+
init: RequestInit,
|
|
27
|
+
credentials: CredentialResolver | ResolvedCredential,
|
|
28
|
+
): Promise<Response> {
|
|
29
|
+
const signal = init.signal ?? undefined;
|
|
30
|
+
for (const forceRefresh of [false, true]) {
|
|
31
|
+
signal?.throwIfAborted();
|
|
32
|
+
const credential = typeof credentials === 'function'
|
|
33
|
+
? await resolveWithSignal(credentials, forceRefresh, signal)
|
|
34
|
+
: credentials;
|
|
35
|
+
signal?.throwIfAborted();
|
|
36
|
+
validateCredential(credential);
|
|
37
|
+
const headers = new Headers(init.headers);
|
|
38
|
+
for (const [key, value] of Object.entries(credential.headers ?? {})) headers.set(key, value);
|
|
39
|
+
if (typeof credentials === 'function' || !headers.has('Authorization')) {
|
|
40
|
+
headers.set('Authorization', `Bearer ${credential.token}`);
|
|
41
|
+
}
|
|
42
|
+
const response = await fetch(input, { ...init, headers });
|
|
43
|
+
if (response.status !== 401 || typeof credentials !== 'function' || forceRefresh) return response;
|
|
44
|
+
void response.body?.cancel().catch(() => {});
|
|
45
|
+
}
|
|
46
|
+
throw new Error('Unreachable credential retry state');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Bound credential acquisition by the same cancellation/deadline as HTTP. */
|
|
50
|
+
async function resolveWithSignal(
|
|
51
|
+
resolver: CredentialResolver,
|
|
52
|
+
forceRefresh: boolean,
|
|
53
|
+
signal?: AbortSignal,
|
|
54
|
+
) {
|
|
55
|
+
if (!signal) return resolver({ forceRefresh });
|
|
56
|
+
signal.throwIfAborted();
|
|
57
|
+
let onAbort: () => void = () => {};
|
|
58
|
+
const aborted = new Promise<never>((_, reject) => {
|
|
59
|
+
onAbort = () => reject(signal.reason);
|
|
60
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
return await Promise.race([Promise.resolve().then(() => {
|
|
64
|
+
signal.throwIfAborted();
|
|
65
|
+
return resolver({ forceRefresh, signal });
|
|
66
|
+
}), aborted]);
|
|
67
|
+
} finally {
|
|
68
|
+
signal.removeEventListener('abort', onAbort);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Runtime validation also protects JavaScript callers and external resolvers. */
|
|
73
|
+
export function validateCredential(credential: ResolvedCredential): void {
|
|
74
|
+
if (!credential || typeof credential.token !== 'string' || credential.token.trim().length === 0) {
|
|
75
|
+
throw authError('Credential resolver must return a non-empty string token');
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/providers/index.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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(
|
|
167
|
-
return
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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 =
|
|
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
|
|
611
|
-
|
|
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
|
/**
|