@proteinjs/conversation 2.6.0 → 2.7.0
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/CHANGELOG.md +18 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/src/Conversation.d.ts.map +1 -1
- package/dist/src/Conversation.js +12 -16
- package/dist/src/Conversation.js.map +1 -1
- package/dist/src/OpenAi.js +3 -3
- package/dist/src/OpenAi.js.map +1 -1
- package/dist/src/OpenAiResponses.d.ts +41 -4
- package/dist/src/OpenAiResponses.d.ts.map +1 -1
- package/dist/src/OpenAiResponses.js +757 -77
- package/dist/src/OpenAiResponses.js.map +1 -1
- package/dist/src/OpenAiStreamProcessor.js +4 -4
- package/dist/src/OpenAiStreamProcessor.js.map +1 -1
- package/dist/src/UsageData.d.ts +39 -4
- package/dist/src/UsageData.d.ts.map +1 -1
- package/dist/src/UsageData.js +302 -11
- package/dist/src/UsageData.js.map +1 -1
- package/dist/src/fs/conversation_fs/ConversationFsModule.d.ts.map +1 -1
- package/dist/src/fs/conversation_fs/ConversationFsModule.js +1 -0
- package/dist/src/fs/conversation_fs/ConversationFsModule.js.map +1 -1
- package/dist/src/fs/conversation_fs/FsFunctions.d.ts +26 -0
- package/dist/src/fs/conversation_fs/FsFunctions.d.ts.map +1 -1
- package/dist/src/fs/conversation_fs/FsFunctions.js +68 -27
- package/dist/src/fs/conversation_fs/FsFunctions.js.map +1 -1
- package/index.ts +1 -1
- package/package.json +4 -4
- package/src/Conversation.ts +14 -17
- package/src/OpenAi.ts +3 -3
- package/src/OpenAiResponses.ts +905 -112
- package/src/OpenAiStreamProcessor.ts +3 -3
- package/src/UsageData.ts +376 -13
- package/src/fs/conversation_fs/ConversationFsModule.ts +2 -0
- package/src/fs/conversation_fs/FsFunctions.ts +32 -2
package/src/OpenAiResponses.ts
CHANGED
|
@@ -6,11 +6,23 @@ import type { Function } from './Function';
|
|
|
6
6
|
import { UsageData, UsageDataAccumulator } from './UsageData';
|
|
7
7
|
import { ChatCompletionMessageParamFactory } from './ChatCompletionMessageParamFactory';
|
|
8
8
|
import type { GenerateResponseReturn, ToolInvocationProgressEvent, ToolInvocationResult } from './OpenAi';
|
|
9
|
-
import {
|
|
9
|
+
import { TiktokenModel } from 'tiktoken';
|
|
10
10
|
|
|
11
|
-
export const DEFAULT_RESPONSES_MODEL = 'gpt-5.2';
|
|
11
|
+
export const DEFAULT_RESPONSES_MODEL = 'gpt-5.2' as TiktokenModel;
|
|
12
12
|
export const DEFAULT_MAX_TOOL_CALLS = 50;
|
|
13
13
|
|
|
14
|
+
/** Default hard cap for background-mode polling duration (ms): 1 hour. */
|
|
15
|
+
export const DEFAULT_MAX_BACKGROUND_WAIT_MS = 60 * 60 * 1000;
|
|
16
|
+
|
|
17
|
+
/** Best-effort timeout for cancel calls (avoid hanging abort/timeout paths). */
|
|
18
|
+
const DEFAULT_CANCEL_TIMEOUT_MS = 10_000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Responses API service tier.
|
|
22
|
+
* See: Responses API `service_tier` request param and response field.
|
|
23
|
+
*/
|
|
24
|
+
export type OpenAiServiceTier = 'auto' | 'default' | 'flex' | 'priority' | (string & {});
|
|
25
|
+
|
|
14
26
|
export type OpenAiResponsesParams = {
|
|
15
27
|
modules?: ConversationModule[];
|
|
16
28
|
/** If provided, only these functions will be exposed to the model. */
|
|
@@ -18,18 +30,24 @@ export type OpenAiResponsesParams = {
|
|
|
18
30
|
logLevel?: LogLevel;
|
|
19
31
|
|
|
20
32
|
/** Default model when none is provided per call. */
|
|
21
|
-
defaultModel?:
|
|
33
|
+
defaultModel?: TiktokenModel;
|
|
22
34
|
|
|
23
35
|
/** Default cap for tool calls (per call). */
|
|
24
36
|
maxToolCalls?: number;
|
|
37
|
+
|
|
38
|
+
/** Default hard cap for background-mode polling duration (ms). Default: 1 hour. */
|
|
39
|
+
maxBackgroundWaitMs?: number;
|
|
25
40
|
};
|
|
26
41
|
|
|
27
42
|
export type GenerateTextParams = {
|
|
28
43
|
messages: (string | ChatCompletionMessageParam)[];
|
|
29
|
-
model?:
|
|
44
|
+
model?: TiktokenModel;
|
|
30
45
|
|
|
31
46
|
abortSignal?: AbortSignal;
|
|
32
47
|
|
|
48
|
+
/** Hard cap for background-mode polling duration (ms). Default: 1 hour. */
|
|
49
|
+
maxBackgroundWaitMs?: number;
|
|
50
|
+
|
|
33
51
|
/** Sampling & limits */
|
|
34
52
|
temperature?: number;
|
|
35
53
|
topP?: number;
|
|
@@ -49,14 +67,20 @@ export type GenerateTextParams = {
|
|
|
49
67
|
|
|
50
68
|
/** If true, run using Responses API background mode (polling). */
|
|
51
69
|
backgroundMode?: boolean;
|
|
70
|
+
|
|
71
|
+
/** Optional Responses API service tier override (per-request). */
|
|
72
|
+
serviceTier?: OpenAiServiceTier;
|
|
52
73
|
};
|
|
53
74
|
|
|
54
75
|
export type ResponsesGenerateObjectParams<S> = {
|
|
55
76
|
messages: (string | ChatCompletionMessageParam)[];
|
|
56
|
-
model?:
|
|
77
|
+
model?: TiktokenModel;
|
|
57
78
|
|
|
58
79
|
abortSignal?: AbortSignal;
|
|
59
80
|
|
|
81
|
+
/** Hard cap for background-mode polling duration (ms). Default: 1 hour. */
|
|
82
|
+
maxBackgroundWaitMs?: number;
|
|
83
|
+
|
|
60
84
|
/** Zod schema or JSON Schema */
|
|
61
85
|
schema: S;
|
|
62
86
|
|
|
@@ -79,6 +103,9 @@ export type ResponsesGenerateObjectParams<S> = {
|
|
|
79
103
|
|
|
80
104
|
/** If true, run using Responses API background mode (polling). */
|
|
81
105
|
backgroundMode?: boolean;
|
|
106
|
+
|
|
107
|
+
/** Optional Responses API service tier override (per-request). */
|
|
108
|
+
serviceTier?: OpenAiServiceTier;
|
|
82
109
|
};
|
|
83
110
|
|
|
84
111
|
/**
|
|
@@ -96,8 +123,9 @@ export class OpenAiResponses {
|
|
|
96
123
|
|
|
97
124
|
private readonly modules: ConversationModule[];
|
|
98
125
|
private readonly allowedFunctionNames?: string[];
|
|
99
|
-
private readonly defaultModel:
|
|
126
|
+
private readonly defaultModel: TiktokenModel;
|
|
100
127
|
private readonly defaultMaxToolCalls: number;
|
|
128
|
+
private readonly defaultMaxBackgroundWaitMs: number;
|
|
101
129
|
|
|
102
130
|
private modulesProcessed = false;
|
|
103
131
|
private processingModulesPromise: Promise<void> | null = null;
|
|
@@ -112,8 +140,15 @@ export class OpenAiResponses {
|
|
|
112
140
|
this.modules = opts.modules ?? [];
|
|
113
141
|
this.allowedFunctionNames = opts.allowedFunctionNames;
|
|
114
142
|
|
|
115
|
-
this.defaultModel =
|
|
143
|
+
this.defaultModel = opts.defaultModel ?? DEFAULT_RESPONSES_MODEL;
|
|
116
144
|
this.defaultMaxToolCalls = typeof opts.maxToolCalls === 'number' ? opts.maxToolCalls : DEFAULT_MAX_TOOL_CALLS;
|
|
145
|
+
|
|
146
|
+
this.defaultMaxBackgroundWaitMs =
|
|
147
|
+
typeof opts.maxBackgroundWaitMs === 'number' &&
|
|
148
|
+
Number.isFinite(opts.maxBackgroundWaitMs) &&
|
|
149
|
+
opts.maxBackgroundWaitMs > 0
|
|
150
|
+
? Math.floor(opts.maxBackgroundWaitMs)
|
|
151
|
+
: DEFAULT_MAX_BACKGROUND_WAIT_MS;
|
|
117
152
|
}
|
|
118
153
|
|
|
119
154
|
/** Plain text generation (supports tool calling). */
|
|
@@ -128,6 +163,7 @@ export class OpenAiResponses {
|
|
|
128
163
|
});
|
|
129
164
|
|
|
130
165
|
const maxToolCalls = typeof args.maxToolCalls === 'number' ? args.maxToolCalls : this.defaultMaxToolCalls;
|
|
166
|
+
const maxBackgroundWaitMs = this.resolveMaxBackgroundWaitMs(args.maxBackgroundWaitMs);
|
|
131
167
|
|
|
132
168
|
const result = await this.run({
|
|
133
169
|
model,
|
|
@@ -140,7 +176,9 @@ export class OpenAiResponses {
|
|
|
140
176
|
reasoningEffort: args.reasoningEffort,
|
|
141
177
|
maxToolCalls,
|
|
142
178
|
backgroundMode,
|
|
179
|
+
maxBackgroundWaitMs,
|
|
143
180
|
textFormat: undefined,
|
|
181
|
+
serviceTier: args.serviceTier,
|
|
144
182
|
});
|
|
145
183
|
|
|
146
184
|
if (args.onUsageData) {
|
|
@@ -167,6 +205,7 @@ export class OpenAiResponses {
|
|
|
167
205
|
});
|
|
168
206
|
|
|
169
207
|
const maxToolCalls = typeof args.maxToolCalls === 'number' ? args.maxToolCalls : this.defaultMaxToolCalls;
|
|
208
|
+
const maxBackgroundWaitMs = this.resolveMaxBackgroundWaitMs(args.maxBackgroundWaitMs);
|
|
170
209
|
const textFormat = this.buildTextFormat(args.schema);
|
|
171
210
|
|
|
172
211
|
const result = await this.run({
|
|
@@ -180,10 +219,17 @@ export class OpenAiResponses {
|
|
|
180
219
|
reasoningEffort: args.reasoningEffort,
|
|
181
220
|
maxToolCalls,
|
|
182
221
|
backgroundMode,
|
|
222
|
+
maxBackgroundWaitMs,
|
|
183
223
|
textFormat,
|
|
224
|
+
serviceTier: args.serviceTier,
|
|
184
225
|
});
|
|
185
226
|
|
|
186
|
-
const object = this.parseAndValidateStructuredOutput<T>(result.message, args.schema
|
|
227
|
+
const object = this.parseAndValidateStructuredOutput<T>(result.message, args.schema, {
|
|
228
|
+
model,
|
|
229
|
+
maxOutputTokens: args.maxTokens,
|
|
230
|
+
requestedServiceTier: args.serviceTier,
|
|
231
|
+
serviceTier: result.serviceTier,
|
|
232
|
+
});
|
|
187
233
|
|
|
188
234
|
const outcome = {
|
|
189
235
|
object,
|
|
@@ -202,7 +248,7 @@ export class OpenAiResponses {
|
|
|
202
248
|
// -----------------------------------------
|
|
203
249
|
|
|
204
250
|
private async run(args: {
|
|
205
|
-
model:
|
|
251
|
+
model: TiktokenModel;
|
|
206
252
|
messages: (string | ChatCompletionMessageParam)[];
|
|
207
253
|
|
|
208
254
|
temperature?: number;
|
|
@@ -216,12 +262,15 @@ export class OpenAiResponses {
|
|
|
216
262
|
|
|
217
263
|
maxToolCalls: number;
|
|
218
264
|
backgroundMode: boolean;
|
|
265
|
+
maxBackgroundWaitMs: number;
|
|
219
266
|
|
|
220
267
|
textFormat?: unknown;
|
|
221
|
-
|
|
268
|
+
|
|
269
|
+
serviceTier?: OpenAiServiceTier;
|
|
270
|
+
}): Promise<GenerateResponseReturn & { serviceTier?: OpenAiServiceTier }> {
|
|
222
271
|
// UsageDataAccumulator is typed around TiktokenModel; keep accumulator model stable,
|
|
223
272
|
// and (optionally) report the actual model via upstream telemetry if you later choose to.
|
|
224
|
-
const usage = new UsageDataAccumulator({ model:
|
|
273
|
+
const usage = new UsageDataAccumulator({ model: args.model });
|
|
225
274
|
const toolInvocations: ToolInvocationResult[] = [];
|
|
226
275
|
|
|
227
276
|
const tools = this.buildResponseTools(this.functions);
|
|
@@ -235,7 +284,8 @@ export class OpenAiResponses {
|
|
|
235
284
|
for (;;) {
|
|
236
285
|
const response = await this.createResponseAndMaybeWait({
|
|
237
286
|
model: args.model,
|
|
238
|
-
instructions
|
|
287
|
+
// Always pass instructions; they are not carried over with previous_response_id.
|
|
288
|
+
instructions,
|
|
239
289
|
input: nextInput,
|
|
240
290
|
previousResponseId,
|
|
241
291
|
tools,
|
|
@@ -245,10 +295,22 @@ export class OpenAiResponses {
|
|
|
245
295
|
reasoningEffort: args.reasoningEffort,
|
|
246
296
|
textFormat: args.textFormat,
|
|
247
297
|
backgroundMode: args.backgroundMode,
|
|
298
|
+
maxBackgroundWaitMs: args.maxBackgroundWaitMs,
|
|
248
299
|
abortSignal: args.abortSignal,
|
|
300
|
+
serviceTier: args.serviceTier,
|
|
249
301
|
});
|
|
250
302
|
|
|
251
|
-
this.addUsageFromResponse(response, usage);
|
|
303
|
+
this.addUsageFromResponse(response, usage, { requestedServiceTier: args.serviceTier });
|
|
304
|
+
|
|
305
|
+
// For structured outputs we should not attempt to parse incomplete/failed/cancelled responses.
|
|
306
|
+
// For plain-text generation, we allow "incomplete" to pass through (partial output),
|
|
307
|
+
// but still fail on other non-completed statuses.
|
|
308
|
+
this.throwIfResponseUnusable(response as any, {
|
|
309
|
+
allowIncomplete: !args.textFormat,
|
|
310
|
+
model: args.model,
|
|
311
|
+
maxOutputTokens: args.maxTokens,
|
|
312
|
+
requestedServiceTier: args.serviceTier,
|
|
313
|
+
});
|
|
252
314
|
|
|
253
315
|
const functionCalls = this.extractFunctionCalls(response);
|
|
254
316
|
if (functionCalls.length < 1) {
|
|
@@ -256,7 +318,12 @@ export class OpenAiResponses {
|
|
|
256
318
|
if (!message) {
|
|
257
319
|
throw new Error(`Response was empty`);
|
|
258
320
|
}
|
|
259
|
-
return {
|
|
321
|
+
return {
|
|
322
|
+
message,
|
|
323
|
+
usagedata: usage.usageData,
|
|
324
|
+
toolInvocations,
|
|
325
|
+
serviceTier: response.service_tier ? response.service_tier : undefined,
|
|
326
|
+
};
|
|
260
327
|
}
|
|
261
328
|
|
|
262
329
|
if (toolCallsExecuted + functionCalls.length > args.maxToolCalls) {
|
|
@@ -287,6 +354,263 @@ export class OpenAiResponses {
|
|
|
287
354
|
}
|
|
288
355
|
}
|
|
289
356
|
|
|
357
|
+
private throwIfResponseUnusable(
|
|
358
|
+
response: any,
|
|
359
|
+
opts: {
|
|
360
|
+
allowIncomplete: boolean;
|
|
361
|
+
model?: string;
|
|
362
|
+
maxOutputTokens?: number;
|
|
363
|
+
requestedServiceTier?: OpenAiServiceTier;
|
|
364
|
+
}
|
|
365
|
+
): void {
|
|
366
|
+
const statusRaw = typeof response?.status === 'string' ? String(response.status) : '';
|
|
367
|
+
const status = statusRaw.toLowerCase();
|
|
368
|
+
|
|
369
|
+
if (!status || status === 'completed') {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (status === 'incomplete' && opts.allowIncomplete) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const id = typeof response?.id === 'string' ? response.id : '';
|
|
378
|
+
const reason = response?.incomplete_details?.reason;
|
|
379
|
+
const apiErr = response?.error;
|
|
380
|
+
|
|
381
|
+
const serviceTier =
|
|
382
|
+
typeof response?.service_tier === 'string' && response.service_tier.trim() ? response.service_tier.trim() : '';
|
|
383
|
+
|
|
384
|
+
const directOutputText = typeof response?.output_text === 'string' ? response.output_text : '';
|
|
385
|
+
const assistantText = this.extractAssistantText(response as any);
|
|
386
|
+
|
|
387
|
+
const outTextLen = directOutputText ? directOutputText.length : 0;
|
|
388
|
+
const assistantLen = assistantText ? assistantText.length : 0;
|
|
389
|
+
|
|
390
|
+
const usage = response?.usage;
|
|
391
|
+
const inputTokens = typeof usage?.input_tokens === 'number' ? usage.input_tokens : undefined;
|
|
392
|
+
const outputTokens = typeof usage?.output_tokens === 'number' ? usage.output_tokens : undefined;
|
|
393
|
+
const totalTokens =
|
|
394
|
+
typeof usage?.total_tokens === 'number'
|
|
395
|
+
? usage.total_tokens
|
|
396
|
+
: typeof inputTokens === 'number' && typeof outputTokens === 'number'
|
|
397
|
+
? inputTokens + outputTokens
|
|
398
|
+
: undefined;
|
|
399
|
+
|
|
400
|
+
let msg = `Responses API returned status="${status}"`;
|
|
401
|
+
if (id) {
|
|
402
|
+
msg += ` (id=${id})`;
|
|
403
|
+
}
|
|
404
|
+
msg += `.`;
|
|
405
|
+
|
|
406
|
+
const details: Record<string, unknown> = {
|
|
407
|
+
response_id: id || undefined,
|
|
408
|
+
status,
|
|
409
|
+
model: typeof opts.model === 'string' && opts.model.trim() ? opts.model : undefined,
|
|
410
|
+
max_output_tokens: typeof opts.maxOutputTokens === 'number' ? opts.maxOutputTokens : undefined,
|
|
411
|
+
|
|
412
|
+
requested_service_tier:
|
|
413
|
+
typeof opts.requestedServiceTier === 'string' && opts.requestedServiceTier.trim()
|
|
414
|
+
? opts.requestedServiceTier.trim()
|
|
415
|
+
: undefined,
|
|
416
|
+
service_tier: serviceTier || undefined,
|
|
417
|
+
|
|
418
|
+
incomplete_reason: typeof reason === 'string' && reason.trim() ? reason : undefined,
|
|
419
|
+
api_error: apiErr ?? undefined,
|
|
420
|
+
|
|
421
|
+
usage_input_tokens: inputTokens,
|
|
422
|
+
usage_output_tokens: outputTokens,
|
|
423
|
+
usage_total_tokens: totalTokens,
|
|
424
|
+
|
|
425
|
+
output_text_len: outTextLen || undefined,
|
|
426
|
+
output_text_tail: outTextLen > 0 ? truncateTail(directOutputText, 400) : undefined,
|
|
427
|
+
|
|
428
|
+
assistant_text_len: assistantLen || undefined,
|
|
429
|
+
assistant_text_tail: assistantLen > 0 ? truncateTail(assistantText, 400) : undefined,
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const extra: string[] = [];
|
|
433
|
+
if (details.model) {
|
|
434
|
+
extra.push(`model=${details.model}`);
|
|
435
|
+
}
|
|
436
|
+
if (typeof details.max_output_tokens === 'number') {
|
|
437
|
+
extra.push(`max_output_tokens=${details.max_output_tokens}`);
|
|
438
|
+
}
|
|
439
|
+
if (typeof details.requested_service_tier === 'string') {
|
|
440
|
+
extra.push(`requested_service_tier=${details.requested_service_tier}`);
|
|
441
|
+
}
|
|
442
|
+
if (typeof details.service_tier === 'string') {
|
|
443
|
+
extra.push(`service_tier=${details.service_tier}`);
|
|
444
|
+
}
|
|
445
|
+
if (details.incomplete_reason) {
|
|
446
|
+
extra.push(`reason=${details.incomplete_reason}`);
|
|
447
|
+
}
|
|
448
|
+
if (typeof details.output_text_len === 'number') {
|
|
449
|
+
extra.push(`output_text_len=${details.output_text_len}`);
|
|
450
|
+
}
|
|
451
|
+
if (typeof details.assistant_text_len === 'number') {
|
|
452
|
+
extra.push(`assistant_text_len=${details.assistant_text_len}`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (extra.length > 0) {
|
|
456
|
+
msg += ` ${extra.join(' ')}.`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
throw new OpenAiResponsesError({
|
|
460
|
+
code: 'RESPONSE_STATUS',
|
|
461
|
+
message: msg,
|
|
462
|
+
details,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private toOpenAiApiError(
|
|
467
|
+
error: unknown,
|
|
468
|
+
meta: {
|
|
469
|
+
operation: 'responses.create' | 'responses.retrieve' | 'responses.cancel';
|
|
470
|
+
model?: string;
|
|
471
|
+
reasoningEffort?: OpenAIApi.Chat.Completions.ChatCompletionReasoningEffort;
|
|
472
|
+
backgroundMode?: boolean;
|
|
473
|
+
responseId?: string;
|
|
474
|
+
previousResponseId?: string;
|
|
475
|
+
pollAttempt?: number;
|
|
476
|
+
aborted?: boolean;
|
|
477
|
+
waitedMs?: number;
|
|
478
|
+
maxWaitMs?: number;
|
|
479
|
+
lastStatus?: string;
|
|
480
|
+
requestedServiceTier?: OpenAiServiceTier;
|
|
481
|
+
serviceTier?: string;
|
|
482
|
+
}
|
|
483
|
+
): OpenAiResponsesError {
|
|
484
|
+
const status = extractHttpStatus(error);
|
|
485
|
+
const requestId = extractRequestId(error);
|
|
486
|
+
const retryable = isRetryableHttpStatus(status);
|
|
487
|
+
|
|
488
|
+
const errMsg = error instanceof Error ? error.message : String(error ?? '');
|
|
489
|
+
const errName = error instanceof Error ? error.name : undefined;
|
|
490
|
+
|
|
491
|
+
const aborted = meta.aborted === true || isAbortError(error);
|
|
492
|
+
|
|
493
|
+
let msg = `OpenAI ${meta.operation} failed.`;
|
|
494
|
+
const extra: string[] = [];
|
|
495
|
+
|
|
496
|
+
if (aborted) {
|
|
497
|
+
extra.push(`aborted=true`);
|
|
498
|
+
}
|
|
499
|
+
if (typeof status === 'number') {
|
|
500
|
+
extra.push(`status=${status}`);
|
|
501
|
+
}
|
|
502
|
+
if (requestId) {
|
|
503
|
+
extra.push(`requestId=${requestId}`);
|
|
504
|
+
}
|
|
505
|
+
if (meta.responseId) {
|
|
506
|
+
extra.push(`responseId=${meta.responseId}`);
|
|
507
|
+
}
|
|
508
|
+
if (meta.backgroundMode) {
|
|
509
|
+
extra.push(`background=true`);
|
|
510
|
+
}
|
|
511
|
+
if (typeof meta.pollAttempt === 'number') {
|
|
512
|
+
extra.push(`pollAttempt=${meta.pollAttempt}`);
|
|
513
|
+
}
|
|
514
|
+
if (typeof meta.waitedMs === 'number') {
|
|
515
|
+
extra.push(`waitedMs=${meta.waitedMs}`);
|
|
516
|
+
}
|
|
517
|
+
if (typeof meta.maxWaitMs === 'number') {
|
|
518
|
+
extra.push(`maxWaitMs=${meta.maxWaitMs}`);
|
|
519
|
+
}
|
|
520
|
+
if (typeof meta.lastStatus === 'string' && meta.lastStatus.trim()) {
|
|
521
|
+
extra.push(`lastStatus=${meta.lastStatus.trim()}`);
|
|
522
|
+
}
|
|
523
|
+
if (typeof meta.model === 'string' && meta.model.trim()) {
|
|
524
|
+
extra.push(`model=${meta.model.trim()}`);
|
|
525
|
+
}
|
|
526
|
+
if (meta.reasoningEffort) {
|
|
527
|
+
extra.push(`reasoningEffort=${meta.reasoningEffort}`);
|
|
528
|
+
}
|
|
529
|
+
if (typeof meta.requestedServiceTier === 'string' && meta.requestedServiceTier.trim()) {
|
|
530
|
+
extra.push(`requested_service_tier=${meta.requestedServiceTier.trim()}`);
|
|
531
|
+
}
|
|
532
|
+
if (typeof meta.serviceTier === 'string' && meta.serviceTier.trim()) {
|
|
533
|
+
extra.push(`service_tier=${meta.serviceTier.trim()}`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (extra.length > 0) {
|
|
537
|
+
msg += ` ${extra.join(' ')}.`;
|
|
538
|
+
}
|
|
539
|
+
if (errMsg) {
|
|
540
|
+
msg += ` error=${JSON.stringify(errMsg)}.`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const details: Record<string, unknown> = {
|
|
544
|
+
operation: meta.operation,
|
|
545
|
+
status: typeof status === 'number' ? status : undefined,
|
|
546
|
+
request_id: requestId,
|
|
547
|
+
response_id: meta.responseId,
|
|
548
|
+
previous_response_id: meta.previousResponseId,
|
|
549
|
+
background: meta.backgroundMode ? true : undefined,
|
|
550
|
+
poll_attempt: meta.pollAttempt,
|
|
551
|
+
waited_ms: meta.waitedMs,
|
|
552
|
+
max_wait_ms: meta.maxWaitMs,
|
|
553
|
+
last_status: typeof meta.lastStatus === 'string' && meta.lastStatus.trim() ? meta.lastStatus.trim() : undefined,
|
|
554
|
+
model: typeof meta.model === 'string' && meta.model.trim() ? meta.model.trim() : undefined,
|
|
555
|
+
reasoning_effort: meta.reasoningEffort,
|
|
556
|
+
requested_service_tier:
|
|
557
|
+
typeof meta.requestedServiceTier === 'string' && meta.requestedServiceTier.trim()
|
|
558
|
+
? meta.requestedServiceTier.trim()
|
|
559
|
+
: undefined,
|
|
560
|
+
service_tier:
|
|
561
|
+
typeof meta.serviceTier === 'string' && meta.serviceTier.trim() ? meta.serviceTier.trim() : undefined,
|
|
562
|
+
error_name: errName,
|
|
563
|
+
aborted: aborted ? true : undefined,
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
return new OpenAiResponsesError({
|
|
567
|
+
code: 'OPENAI_API',
|
|
568
|
+
message: msg,
|
|
569
|
+
details,
|
|
570
|
+
cause: error,
|
|
571
|
+
retryable,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private resolveMaxBackgroundWaitMs(ms?: number): number {
|
|
576
|
+
const n =
|
|
577
|
+
typeof ms === 'number' && Number.isFinite(ms) && ms > 0 ? Math.floor(ms) : this.defaultMaxBackgroundWaitMs;
|
|
578
|
+
// Ensure we never return a non-positive number even if misconfigured elsewhere.
|
|
579
|
+
return n > 0 ? n : DEFAULT_MAX_BACKGROUND_WAIT_MS;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private async cancelResponseBestEffort(
|
|
583
|
+
responseId: string
|
|
584
|
+
): Promise<
|
|
585
|
+
| { attempted: false }
|
|
586
|
+
| { attempted: true; ok: true }
|
|
587
|
+
| { attempted: true; ok: false; error?: Record<string, unknown> }
|
|
588
|
+
> {
|
|
589
|
+
if (!responseId) {
|
|
590
|
+
return { attempted: false };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
const resp = await this.client.responses.cancel(responseId);
|
|
595
|
+
|
|
596
|
+
// Docs show cancelled as the post-cancel status.
|
|
597
|
+
if (resp?.status === 'cancelled') {
|
|
598
|
+
return { attempted: true, ok: true };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
attempted: true,
|
|
603
|
+
ok: false,
|
|
604
|
+
error: {
|
|
605
|
+
message: 'Cancel did not return status=cancelled',
|
|
606
|
+
status: resp?.status,
|
|
607
|
+
},
|
|
608
|
+
};
|
|
609
|
+
} catch (e: unknown) {
|
|
610
|
+
return { attempted: true, ok: false, error: safeErrorSummary(e) };
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
290
614
|
private async createResponseAndMaybeWait(args: {
|
|
291
615
|
model: string;
|
|
292
616
|
instructions?: string;
|
|
@@ -302,14 +626,11 @@ export class OpenAiResponses {
|
|
|
302
626
|
textFormat?: unknown;
|
|
303
627
|
|
|
304
628
|
backgroundMode: boolean;
|
|
629
|
+
maxBackgroundWaitMs: number;
|
|
305
630
|
abortSignal?: AbortSignal;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
output_text?: string;
|
|
310
|
-
output?: unknown[];
|
|
311
|
-
usage?: unknown;
|
|
312
|
-
}> {
|
|
631
|
+
|
|
632
|
+
serviceTier?: OpenAiServiceTier;
|
|
633
|
+
}): Promise<OpenAIApi.Responses.Response> {
|
|
313
634
|
const body: Record<string, unknown> = {
|
|
314
635
|
model: args.model,
|
|
315
636
|
input: args.input,
|
|
@@ -343,77 +664,197 @@ export class OpenAiResponses {
|
|
|
343
664
|
body.text = { format: args.textFormat };
|
|
344
665
|
}
|
|
345
666
|
|
|
667
|
+
if (typeof args.serviceTier === 'string' && args.serviceTier.trim()) {
|
|
668
|
+
body.service_tier = args.serviceTier.trim();
|
|
669
|
+
}
|
|
670
|
+
|
|
346
671
|
if (args.backgroundMode) {
|
|
347
672
|
body.background = true;
|
|
348
673
|
body.store = true;
|
|
349
674
|
}
|
|
350
675
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
676
|
+
let created: OpenAIApi.Responses.Response;
|
|
677
|
+
try {
|
|
678
|
+
created = await this.client.responses.create(
|
|
679
|
+
body as never,
|
|
680
|
+
args.abortSignal ? { signal: args.abortSignal } : undefined
|
|
681
|
+
);
|
|
682
|
+
} catch (error: unknown) {
|
|
683
|
+
throw this.toOpenAiApiError(error, {
|
|
684
|
+
operation: 'responses.create',
|
|
685
|
+
model: args.model,
|
|
686
|
+
reasoningEffort: args.reasoningEffort,
|
|
687
|
+
backgroundMode: args.backgroundMode,
|
|
688
|
+
previousResponseId: args.previousResponseId,
|
|
689
|
+
aborted: args.abortSignal?.aborted ? true : undefined,
|
|
690
|
+
requestedServiceTier: args.serviceTier,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
355
693
|
|
|
356
694
|
if (!args.backgroundMode) {
|
|
357
|
-
return created
|
|
358
|
-
id?: string;
|
|
359
|
-
status?: string;
|
|
360
|
-
output_text?: string;
|
|
361
|
-
output?: unknown[];
|
|
362
|
-
usage?: unknown;
|
|
363
|
-
};
|
|
695
|
+
return created;
|
|
364
696
|
}
|
|
365
697
|
|
|
366
698
|
if (!created?.id) {
|
|
367
|
-
return created
|
|
368
|
-
id?: string;
|
|
369
|
-
status?: string;
|
|
370
|
-
output_text?: string;
|
|
371
|
-
output?: unknown[];
|
|
372
|
-
usage?: unknown;
|
|
373
|
-
};
|
|
699
|
+
return created;
|
|
374
700
|
}
|
|
375
701
|
|
|
376
|
-
return await this.waitForCompletion(created.id, args.abortSignal
|
|
702
|
+
return await this.waitForCompletion(created.id, args.abortSignal, {
|
|
703
|
+
model: args.model,
|
|
704
|
+
reasoningEffort: args.reasoningEffort,
|
|
705
|
+
maxWaitMs: this.resolveMaxBackgroundWaitMs(args.maxBackgroundWaitMs),
|
|
706
|
+
requestedServiceTier: args.serviceTier,
|
|
707
|
+
});
|
|
377
708
|
}
|
|
378
709
|
|
|
379
710
|
private async waitForCompletion(
|
|
380
711
|
responseId: string,
|
|
381
|
-
abortSignal?: AbortSignal
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
712
|
+
abortSignal?: AbortSignal,
|
|
713
|
+
ctx?: {
|
|
714
|
+
model?: string;
|
|
715
|
+
reasoningEffort?: OpenAIApi.Chat.Completions.ChatCompletionReasoningEffort;
|
|
716
|
+
maxWaitMs?: number;
|
|
717
|
+
requestedServiceTier?: OpenAiServiceTier;
|
|
718
|
+
}
|
|
719
|
+
): Promise<OpenAIApi.Responses.Response> {
|
|
720
|
+
this.logger.debug({ message: 'Waiting for completion', obj: { responseId } });
|
|
721
|
+
const maxWaitMs = this.resolveMaxBackgroundWaitMs(ctx?.maxWaitMs);
|
|
722
|
+
|
|
723
|
+
const startedAtMs = Date.now();
|
|
724
|
+
|
|
725
|
+
const delayMs = 1000;
|
|
726
|
+
let pollAttempt = 0;
|
|
727
|
+
|
|
728
|
+
let lastStatus = '';
|
|
729
|
+
let cancelAttempted = false;
|
|
730
|
+
|
|
731
|
+
const warnEveryMs = 10 * 60 * 1000;
|
|
732
|
+
let nextWarnAtMs = warnEveryMs;
|
|
733
|
+
|
|
734
|
+
const throwPollingStop = async (args: { kind: 'aborted' | 'timeout'; cause?: unknown }): Promise<never> => {
|
|
735
|
+
const waitedMs = Date.now() - startedAtMs;
|
|
736
|
+
|
|
737
|
+
// Best-effort cancellation to stop server-side work when we're done waiting.
|
|
738
|
+
let cancel: Awaited<ReturnType<OpenAiResponses['cancelResponseBestEffort']>> | undefined = undefined;
|
|
739
|
+
if (!cancelAttempted) {
|
|
740
|
+
cancelAttempted = true;
|
|
741
|
+
cancel = await this.cancelResponseBestEffort(responseId);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const baseDetails: Record<string, unknown> = {
|
|
745
|
+
operation: 'responses.retrieve',
|
|
746
|
+
response_id: responseId,
|
|
747
|
+
background: true,
|
|
748
|
+
poll_attempt: pollAttempt,
|
|
749
|
+
waited_ms: waitedMs,
|
|
750
|
+
max_wait_ms: maxWaitMs,
|
|
751
|
+
last_status: lastStatus || undefined,
|
|
752
|
+
model: typeof ctx?.model === 'string' && ctx.model.trim() ? ctx.model.trim() : undefined,
|
|
753
|
+
reasoning_effort: ctx?.reasoningEffort,
|
|
754
|
+
requested_service_tier:
|
|
755
|
+
typeof ctx?.requestedServiceTier === 'string' && ctx.requestedServiceTier.trim()
|
|
756
|
+
? ctx.requestedServiceTier.trim()
|
|
757
|
+
: undefined,
|
|
758
|
+
aborted: args.kind === 'aborted' ? true : undefined,
|
|
759
|
+
timeout: args.kind === 'timeout' ? true : undefined,
|
|
760
|
+
cancel_attempted: cancel?.attempted ? true : undefined,
|
|
761
|
+
cancel_ok: cancel && cancel.attempted && 'ok' in cancel ? (cancel as any).ok : undefined,
|
|
762
|
+
cancel_timed_out: cancel && cancel.attempted && (cancel as any).timedOut ? true : undefined,
|
|
763
|
+
cancel_error: cancel && cancel.attempted && (cancel as any).error ? (cancel as any).error : undefined,
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
if (args.cause) {
|
|
767
|
+
baseDetails.polling_cause = safeErrorSummary(args.cause);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const msg =
|
|
771
|
+
args.kind === 'timeout'
|
|
772
|
+
? `Background response exceeded max wait (maxWaitMs=${maxWaitMs}) while polling (id=${responseId}).`
|
|
773
|
+
: `Background polling aborted (id=${responseId}).`;
|
|
774
|
+
|
|
775
|
+
throw new OpenAiResponsesError({
|
|
776
|
+
code: 'OPENAI_API',
|
|
777
|
+
message: msg,
|
|
778
|
+
details: baseDetails,
|
|
779
|
+
cause: args.cause,
|
|
780
|
+
});
|
|
781
|
+
};
|
|
390
782
|
|
|
391
783
|
for (;;) {
|
|
784
|
+
const waitedMs = Date.now() - startedAtMs;
|
|
785
|
+
|
|
786
|
+
// Abort wins immediately.
|
|
392
787
|
if (abortSignal?.aborted) {
|
|
393
|
-
|
|
788
|
+
await throwPollingStop({ kind: 'aborted' });
|
|
394
789
|
}
|
|
395
790
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
);
|
|
791
|
+
// Max wait cap (1h default) to prevent runaway polling.
|
|
792
|
+
if (waitedMs >= maxWaitMs) {
|
|
793
|
+
await throwPollingStop({ kind: 'timeout' });
|
|
794
|
+
}
|
|
401
795
|
|
|
402
|
-
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
796
|
+
// Warn every 10 minutes elapsed (best-effort; may log slightly after the boundary).
|
|
797
|
+
if (waitedMs >= nextWarnAtMs) {
|
|
798
|
+
nextWarnAtMs += warnEveryMs;
|
|
799
|
+
|
|
800
|
+
this.logger.warn({
|
|
801
|
+
message: `Background polling still in progress`,
|
|
802
|
+
obj: {
|
|
803
|
+
responseId,
|
|
804
|
+
status: lastStatus || undefined,
|
|
805
|
+
waitedMs,
|
|
806
|
+
pollAttempt,
|
|
807
|
+
model: typeof ctx?.model === 'string' && ctx.model.trim() ? ctx.model.trim() : undefined,
|
|
808
|
+
reasoningEffort: ctx?.reasoningEffort,
|
|
809
|
+
serviceTier:
|
|
810
|
+
typeof ctx?.requestedServiceTier === 'string' && ctx.requestedServiceTier.trim()
|
|
811
|
+
? ctx.requestedServiceTier.trim()
|
|
812
|
+
: undefined,
|
|
813
|
+
},
|
|
814
|
+
});
|
|
411
815
|
}
|
|
412
816
|
|
|
413
|
-
|
|
817
|
+
pollAttempt += 1;
|
|
818
|
+
|
|
819
|
+
let resp: OpenAIApi.Responses.Response;
|
|
820
|
+
try {
|
|
821
|
+
resp = await this.client.responses.retrieve(
|
|
822
|
+
responseId,
|
|
823
|
+
undefined,
|
|
824
|
+
abortSignal ? { signal: abortSignal } : undefined
|
|
825
|
+
);
|
|
826
|
+
} catch (error: unknown) {
|
|
827
|
+
// If the request was aborted mid-flight, treat it as an abort and still attempt cancellation.
|
|
828
|
+
if (abortSignal?.aborted || isAbortError(error)) {
|
|
829
|
+
await throwPollingStop({ kind: 'aborted', cause: error });
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
throw this.toOpenAiApiError(error, {
|
|
833
|
+
operation: 'responses.retrieve',
|
|
834
|
+
model: ctx?.model,
|
|
835
|
+
reasoningEffort: ctx?.reasoningEffort,
|
|
836
|
+
backgroundMode: true,
|
|
837
|
+
responseId,
|
|
838
|
+
pollAttempt,
|
|
839
|
+
waitedMs,
|
|
840
|
+
maxWaitMs,
|
|
841
|
+
lastStatus,
|
|
842
|
+
requestedServiceTier: ctx?.requestedServiceTier,
|
|
843
|
+
});
|
|
844
|
+
}
|
|
414
845
|
|
|
415
|
-
|
|
416
|
-
|
|
846
|
+
const status = typeof resp?.status === 'string' ? resp.status : '';
|
|
847
|
+
lastStatus = status;
|
|
848
|
+
|
|
849
|
+
// Terminal states
|
|
850
|
+
if (status === 'completed' || status === 'failed' || status === 'incomplete' || status === 'cancelled') {
|
|
851
|
+
return resp;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
this.logger.debug({ message: `Polling response`, obj: { responseId, status, delayMs, pollAttempt, waitedMs } });
|
|
855
|
+
|
|
856
|
+
// Sleep but wake early if aborted, so abort latency is low.
|
|
857
|
+
await sleepWithAbort(delayMs, abortSignal);
|
|
417
858
|
}
|
|
418
859
|
}
|
|
419
860
|
|
|
@@ -641,48 +1082,32 @@ export class OpenAiResponses {
|
|
|
641
1082
|
// Usage + text extraction
|
|
642
1083
|
// -----------------------------------------
|
|
643
1084
|
|
|
644
|
-
private addUsageFromResponse(
|
|
645
|
-
|
|
646
|
-
|
|
1085
|
+
private addUsageFromResponse(
|
|
1086
|
+
response: OpenAIApi.Responses.Response,
|
|
1087
|
+
usage: UsageDataAccumulator,
|
|
1088
|
+
ctx?: { requestedServiceTier?: OpenAiServiceTier }
|
|
1089
|
+
): void {
|
|
1090
|
+
if (!response.usage) {
|
|
647
1091
|
return;
|
|
648
1092
|
}
|
|
649
1093
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
const id = inputDetails as Record<string, unknown>;
|
|
661
|
-
cached = typeof id.cached_tokens === 'number' ? id.cached_tokens : 0;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
const outputDetails = rec.output_tokens_details;
|
|
665
|
-
if (outputDetails && typeof outputDetails === 'object') {
|
|
666
|
-
const od = outputDetails as Record<string, unknown>;
|
|
667
|
-
reasoning = typeof od.reasoning_tokens === 'number' ? od.reasoning_tokens : 0;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
usage.addTokenUsage({
|
|
671
|
-
promptTokens: input,
|
|
672
|
-
cachedPromptTokens: cached,
|
|
673
|
-
completionTokens: output,
|
|
674
|
-
reasoningTokens: reasoning,
|
|
675
|
-
totalTokens: total,
|
|
676
|
-
});
|
|
1094
|
+
usage.addTokenUsage(
|
|
1095
|
+
{
|
|
1096
|
+
inputTokens: response.usage.input_tokens,
|
|
1097
|
+
cachedInputTokens: response.usage.input_tokens_details.cached_tokens,
|
|
1098
|
+
outputTokens: response.usage.output_tokens,
|
|
1099
|
+
reasoningTokens: response.usage.output_tokens_details.reasoning_tokens,
|
|
1100
|
+
totalTokens: response.usage.total_tokens,
|
|
1101
|
+
},
|
|
1102
|
+
{ serviceTier: response.service_tier ?? ctx?.requestedServiceTier }
|
|
1103
|
+
);
|
|
677
1104
|
}
|
|
678
1105
|
|
|
679
1106
|
private extractAssistantText(response: { output_text?: string; output?: unknown[] }): string {
|
|
680
|
-
const direct = typeof response.output_text === 'string' ? response.output_text.trim() : '';
|
|
681
|
-
if (direct) {
|
|
682
|
-
return direct;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
1107
|
const out = Array.isArray(response.output) ? response.output : [];
|
|
1108
|
+
|
|
1109
|
+
let lastJoined = '';
|
|
1110
|
+
|
|
686
1111
|
for (const item of out) {
|
|
687
1112
|
if (!item || typeof item !== 'object') {
|
|
688
1113
|
continue;
|
|
@@ -717,10 +1142,19 @@ export class OpenAiResponses {
|
|
|
717
1142
|
|
|
718
1143
|
const joined = pieces.join('\n').trim();
|
|
719
1144
|
if (joined) {
|
|
720
|
-
|
|
1145
|
+
lastJoined = joined;
|
|
721
1146
|
}
|
|
722
1147
|
}
|
|
723
1148
|
|
|
1149
|
+
if (lastJoined) {
|
|
1150
|
+
return lastJoined;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const direct = typeof response.output_text === 'string' ? response.output_text.trim() : '';
|
|
1154
|
+
if (direct) {
|
|
1155
|
+
return direct;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
724
1158
|
return '';
|
|
725
1159
|
}
|
|
726
1160
|
|
|
@@ -744,8 +1178,12 @@ export class OpenAiResponses {
|
|
|
744
1178
|
};
|
|
745
1179
|
}
|
|
746
1180
|
|
|
747
|
-
private parseAndValidateStructuredOutput<T>(
|
|
748
|
-
|
|
1181
|
+
private parseAndValidateStructuredOutput<T>(
|
|
1182
|
+
text: string,
|
|
1183
|
+
schema: unknown,
|
|
1184
|
+
ctx?: { model?: string; maxOutputTokens?: number; requestedServiceTier?: OpenAiServiceTier; serviceTier?: string }
|
|
1185
|
+
): T {
|
|
1186
|
+
const parsed = this.parseJson(text, ctx);
|
|
749
1187
|
|
|
750
1188
|
if (this.isZodSchema(schema)) {
|
|
751
1189
|
const res = schema.safeParse(parsed);
|
|
@@ -765,7 +1203,10 @@ export class OpenAiResponses {
|
|
|
765
1203
|
return typeof (schema as any).safeParse === 'function';
|
|
766
1204
|
}
|
|
767
1205
|
|
|
768
|
-
private parseJson(
|
|
1206
|
+
private parseJson(
|
|
1207
|
+
text: string,
|
|
1208
|
+
ctx?: { model?: string; maxOutputTokens?: number; requestedServiceTier?: OpenAiServiceTier; serviceTier?: string }
|
|
1209
|
+
): any {
|
|
769
1210
|
const cleaned = String(text ?? '')
|
|
770
1211
|
.trim()
|
|
771
1212
|
.replace(/^```(?:json)?/i, '')
|
|
@@ -774,7 +1215,9 @@ export class OpenAiResponses {
|
|
|
774
1215
|
|
|
775
1216
|
try {
|
|
776
1217
|
return JSON.parse(cleaned);
|
|
777
|
-
} catch {
|
|
1218
|
+
} catch (err1: unknown) {
|
|
1219
|
+
const firstErrMsg = err1 instanceof Error ? err1.message : String(err1);
|
|
1220
|
+
|
|
778
1221
|
const s = cleaned;
|
|
779
1222
|
const firstObj = s.indexOf('{');
|
|
780
1223
|
const firstArr = s.indexOf('[');
|
|
@@ -785,10 +1228,102 @@ export class OpenAiResponses {
|
|
|
785
1228
|
const end = Math.max(lastObj, lastArr);
|
|
786
1229
|
|
|
787
1230
|
if (start >= 0 && end > start) {
|
|
788
|
-
|
|
1231
|
+
const candidate = s.slice(start, end + 1);
|
|
1232
|
+
try {
|
|
1233
|
+
return JSON.parse(candidate);
|
|
1234
|
+
} catch (err2: unknown) {
|
|
1235
|
+
const secondErrMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
1236
|
+
|
|
1237
|
+
const pos2rel = extractJsonParsePosition(secondErrMsg);
|
|
1238
|
+
const pos2 = typeof pos2rel === 'number' ? start + pos2rel : undefined;
|
|
1239
|
+
|
|
1240
|
+
const pos1 = extractJsonParsePosition(firstErrMsg);
|
|
1241
|
+
const pos = typeof pos2 === 'number' ? pos2 : pos1;
|
|
1242
|
+
|
|
1243
|
+
const lc = extractJsonParseLineCol(secondErrMsg) ?? extractJsonParseLineCol(firstErrMsg);
|
|
1244
|
+
|
|
1245
|
+
const details: Record<string, unknown> = {
|
|
1246
|
+
model: typeof ctx?.model === 'string' && ctx.model.trim() ? ctx.model : undefined,
|
|
1247
|
+
max_output_tokens: typeof ctx?.maxOutputTokens === 'number' ? ctx.maxOutputTokens : undefined,
|
|
1248
|
+
|
|
1249
|
+
requested_service_tier:
|
|
1250
|
+
typeof ctx?.requestedServiceTier === 'string' && String(ctx.requestedServiceTier).trim()
|
|
1251
|
+
? String(ctx.requestedServiceTier).trim()
|
|
1252
|
+
: undefined,
|
|
1253
|
+
service_tier:
|
|
1254
|
+
typeof ctx?.serviceTier === 'string' && ctx.serviceTier.trim() ? ctx.serviceTier.trim() : undefined,
|
|
1255
|
+
|
|
1256
|
+
cleaned_len: s.length,
|
|
1257
|
+
cleaned_head: truncateHead(s, 250),
|
|
1258
|
+
cleaned_tail: truncateTail(s, 500),
|
|
1259
|
+
|
|
1260
|
+
json_start: start,
|
|
1261
|
+
json_end: end,
|
|
1262
|
+
json_candidate_len: candidate.length,
|
|
1263
|
+
|
|
1264
|
+
first_error: firstErrMsg,
|
|
1265
|
+
second_error: secondErrMsg,
|
|
1266
|
+
|
|
1267
|
+
error_pos: typeof pos === 'number' ? pos : undefined,
|
|
1268
|
+
error_line: lc?.line,
|
|
1269
|
+
error_column: lc?.column,
|
|
1270
|
+
error_context: typeof pos === 'number' ? snippetAround(s, pos, 160) : undefined,
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
const msg =
|
|
1274
|
+
`Failed to parse model output as JSON. ` +
|
|
1275
|
+
`cleaned_len=${s.length} json_start=${start} json_end=${end}. ` +
|
|
1276
|
+
`first_error=${JSON.stringify(firstErrMsg)} second_error=${JSON.stringify(secondErrMsg)}.`;
|
|
1277
|
+
|
|
1278
|
+
throw new OpenAiResponsesError({
|
|
1279
|
+
code: 'JSON_PARSE',
|
|
1280
|
+
message: msg,
|
|
1281
|
+
details,
|
|
1282
|
+
cause: err2,
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
789
1285
|
}
|
|
790
1286
|
|
|
791
|
-
|
|
1287
|
+
const pos = extractJsonParsePosition(firstErrMsg);
|
|
1288
|
+
const lc = extractJsonParseLineCol(firstErrMsg);
|
|
1289
|
+
|
|
1290
|
+
const details: Record<string, unknown> = {
|
|
1291
|
+
model: typeof ctx?.model === 'string' && ctx.model.trim() ? ctx.model : undefined,
|
|
1292
|
+
max_output_tokens: typeof ctx?.maxOutputTokens === 'number' ? ctx.maxOutputTokens : undefined,
|
|
1293
|
+
|
|
1294
|
+
requested_service_tier:
|
|
1295
|
+
typeof ctx?.requestedServiceTier === 'string' && String(ctx.requestedServiceTier).trim()
|
|
1296
|
+
? String(ctx.requestedServiceTier).trim()
|
|
1297
|
+
: undefined,
|
|
1298
|
+
service_tier:
|
|
1299
|
+
typeof ctx?.serviceTier === 'string' && ctx.serviceTier.trim() ? ctx.serviceTier.trim() : undefined,
|
|
1300
|
+
|
|
1301
|
+
cleaned_len: s.length,
|
|
1302
|
+
cleaned_head: truncateHead(s, 250),
|
|
1303
|
+
cleaned_tail: truncateTail(s, 500),
|
|
1304
|
+
|
|
1305
|
+
json_start: start >= 0 ? start : undefined,
|
|
1306
|
+
json_end: end >= 0 ? end : undefined,
|
|
1307
|
+
|
|
1308
|
+
first_error: firstErrMsg,
|
|
1309
|
+
|
|
1310
|
+
error_pos: typeof pos === 'number' ? pos : undefined,
|
|
1311
|
+
error_line: lc?.line,
|
|
1312
|
+
error_column: lc?.column,
|
|
1313
|
+
error_context: typeof pos === 'number' ? snippetAround(s, pos, 160) : undefined,
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
const msg =
|
|
1317
|
+
`Failed to parse model output as JSON. ` +
|
|
1318
|
+
`cleaned_len=${s.length}. ` +
|
|
1319
|
+
`error=${JSON.stringify(firstErrMsg)}.`;
|
|
1320
|
+
|
|
1321
|
+
throw new OpenAiResponsesError({
|
|
1322
|
+
code: 'JSON_PARSE',
|
|
1323
|
+
message: msg,
|
|
1324
|
+
details,
|
|
1325
|
+
cause: err1,
|
|
1326
|
+
});
|
|
792
1327
|
}
|
|
793
1328
|
}
|
|
794
1329
|
|
|
@@ -1038,9 +1573,8 @@ export class OpenAiResponses {
|
|
|
1038
1573
|
// Model/background defaults
|
|
1039
1574
|
// -----------------------------------------
|
|
1040
1575
|
|
|
1041
|
-
private resolveModel(model?:
|
|
1042
|
-
|
|
1043
|
-
return m.length > 0 ? m : DEFAULT_RESPONSES_MODEL;
|
|
1576
|
+
private resolveModel(model?: TiktokenModel): TiktokenModel {
|
|
1577
|
+
return model ?? this.defaultModel;
|
|
1044
1578
|
}
|
|
1045
1579
|
|
|
1046
1580
|
private resolveBackgroundMode(args: {
|
|
@@ -1071,6 +1605,265 @@ export class OpenAiResponses {
|
|
|
1071
1605
|
}
|
|
1072
1606
|
}
|
|
1073
1607
|
|
|
1608
|
+
export type OpenAiResponsesErrorCode = 'OPENAI_API' | 'RESPONSE_STATUS' | 'JSON_PARSE';
|
|
1609
|
+
|
|
1610
|
+
export class OpenAiResponsesError extends Error {
|
|
1611
|
+
public readonly code: OpenAiResponsesErrorCode;
|
|
1612
|
+
public readonly details: Record<string, unknown>;
|
|
1613
|
+
public readonly cause?: unknown;
|
|
1614
|
+
public readonly retryable: boolean;
|
|
1615
|
+
|
|
1616
|
+
constructor(args: {
|
|
1617
|
+
code: OpenAiResponsesErrorCode;
|
|
1618
|
+
message: string;
|
|
1619
|
+
details?: Record<string, unknown>;
|
|
1620
|
+
cause?: unknown;
|
|
1621
|
+
retryable?: boolean;
|
|
1622
|
+
}) {
|
|
1623
|
+
super(args.message);
|
|
1624
|
+
this.name = 'OpenAiResponsesError';
|
|
1625
|
+
this.code = args.code;
|
|
1626
|
+
this.details = args.details ?? {};
|
|
1627
|
+
this.cause = args.cause;
|
|
1628
|
+
this.retryable = typeof args.retryable === 'boolean' ? args.retryable : true;
|
|
1629
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function truncateHead(text: string, max: number): string {
|
|
1634
|
+
const s = String(text ?? '');
|
|
1635
|
+
if (max <= 0) {
|
|
1636
|
+
return '';
|
|
1637
|
+
}
|
|
1638
|
+
if (s.length <= max) {
|
|
1639
|
+
return s;
|
|
1640
|
+
}
|
|
1641
|
+
return s.slice(0, max) + '...';
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function truncateTail(text: string, max: number): string {
|
|
1645
|
+
const s = String(text ?? '');
|
|
1646
|
+
if (max <= 0) {
|
|
1647
|
+
return '';
|
|
1648
|
+
}
|
|
1649
|
+
if (s.length <= max) {
|
|
1650
|
+
return s;
|
|
1651
|
+
}
|
|
1652
|
+
return '...' + s.slice(s.length - max);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function extractJsonParsePosition(errMsg: string): number | undefined {
|
|
1656
|
+
const m = String(errMsg ?? '').match(/at position\s+(\d+)/i);
|
|
1657
|
+
if (!m) {
|
|
1658
|
+
return undefined;
|
|
1659
|
+
}
|
|
1660
|
+
const n = Number(m[1]);
|
|
1661
|
+
return Number.isFinite(n) ? n : undefined;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function extractJsonParseLineCol(errMsg: string): { line?: number; column?: number } | undefined {
|
|
1665
|
+
const m = String(errMsg ?? '').match(/line\s+(\d+)\s+column\s+(\d+)/i);
|
|
1666
|
+
if (!m) {
|
|
1667
|
+
return undefined;
|
|
1668
|
+
}
|
|
1669
|
+
const line = Number(m[1]);
|
|
1670
|
+
const column = Number(m[2]);
|
|
1671
|
+
return {
|
|
1672
|
+
line: Number.isFinite(line) ? line : undefined,
|
|
1673
|
+
column: Number.isFinite(column) ? column : undefined,
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function snippetAround(text: string, pos: number, radius: number): string {
|
|
1678
|
+
const s = String(text ?? '');
|
|
1679
|
+
const p = Math.max(0, Math.min(s.length, Number.isFinite(pos) ? pos : 0));
|
|
1680
|
+
const r = Math.max(0, radius);
|
|
1681
|
+
|
|
1682
|
+
const start = Math.max(0, p - r);
|
|
1683
|
+
const end = Math.min(s.length, p + r);
|
|
1684
|
+
|
|
1685
|
+
const before = s.slice(start, p);
|
|
1686
|
+
const after = s.slice(p, end);
|
|
1687
|
+
|
|
1688
|
+
const left = start > 0 ? '...' : '';
|
|
1689
|
+
const right = end < s.length ? '...' : '';
|
|
1690
|
+
|
|
1691
|
+
return `${left}${before}<<HERE>>${after}${right}`;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1074
1694
|
function sleep(ms: number): Promise<void> {
|
|
1075
1695
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1076
1696
|
}
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* Sleep, but wake early if the signal is aborted.
|
|
1700
|
+
* (We do not throw here; the caller should check `signal.aborted` and act.)
|
|
1701
|
+
*/
|
|
1702
|
+
function sleepWithAbort(ms: number, signal?: AbortSignal): Promise<void> {
|
|
1703
|
+
if (!signal) {
|
|
1704
|
+
return sleep(ms);
|
|
1705
|
+
}
|
|
1706
|
+
if (signal.aborted) {
|
|
1707
|
+
return Promise.resolve();
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
return new Promise((resolve) => {
|
|
1711
|
+
const t = setTimeout(() => {
|
|
1712
|
+
cleanup();
|
|
1713
|
+
resolve();
|
|
1714
|
+
}, ms);
|
|
1715
|
+
|
|
1716
|
+
const onAbort = () => {
|
|
1717
|
+
cleanup();
|
|
1718
|
+
resolve();
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
const cleanup = () => {
|
|
1722
|
+
try {
|
|
1723
|
+
clearTimeout(t);
|
|
1724
|
+
} catch {
|
|
1725
|
+
// ignore
|
|
1726
|
+
}
|
|
1727
|
+
try {
|
|
1728
|
+
signal.removeEventListener?.('abort', onAbort as any);
|
|
1729
|
+
} catch {
|
|
1730
|
+
// ignore
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
|
|
1734
|
+
try {
|
|
1735
|
+
signal.addEventListener?.('abort', onAbort as any, { once: true });
|
|
1736
|
+
} catch {
|
|
1737
|
+
// If addEventListener isn't available, fall back to plain sleep.
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function extractHttpStatus(error: unknown): number | undefined {
|
|
1743
|
+
if (!error || typeof error !== 'object') {
|
|
1744
|
+
return undefined;
|
|
1745
|
+
}
|
|
1746
|
+
const rec = error as Record<string, unknown>;
|
|
1747
|
+
const status = rec.status;
|
|
1748
|
+
if (typeof status === 'number' && Number.isFinite(status)) {
|
|
1749
|
+
return status;
|
|
1750
|
+
}
|
|
1751
|
+
const statusCode = rec.statusCode;
|
|
1752
|
+
if (typeof statusCode === 'number' && Number.isFinite(statusCode)) {
|
|
1753
|
+
return statusCode;
|
|
1754
|
+
}
|
|
1755
|
+
return undefined;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
function extractRequestId(error: unknown): string | undefined {
|
|
1759
|
+
if (!error || typeof error !== 'object') {
|
|
1760
|
+
return undefined;
|
|
1761
|
+
}
|
|
1762
|
+
const rec = error as Record<string, unknown>;
|
|
1763
|
+
|
|
1764
|
+
const direct = rec.request_id ?? rec.requestId;
|
|
1765
|
+
if (typeof direct === 'string' && direct.trim()) {
|
|
1766
|
+
return direct.trim();
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
const headers = rec.headers as any;
|
|
1770
|
+
if (!headers) {
|
|
1771
|
+
return undefined;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
if (typeof headers.get === 'function') {
|
|
1775
|
+
const v = headers.get('x-request-id');
|
|
1776
|
+
return typeof v === 'string' && v.trim() ? v.trim() : undefined;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
if (typeof headers === 'object' && !Array.isArray(headers)) {
|
|
1780
|
+
for (const k of Object.keys(headers)) {
|
|
1781
|
+
if (String(k).toLowerCase() !== 'x-request-id') {
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
const v = (headers as any)[k];
|
|
1785
|
+
return typeof v === 'string' && v.trim() ? v.trim() : undefined;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
return undefined;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function isRetryableHttpStatus(status: number | undefined): boolean {
|
|
1793
|
+
if (typeof status !== 'number') {
|
|
1794
|
+
return true;
|
|
1795
|
+
}
|
|
1796
|
+
if (status === 408 || status === 409 || status === 429) {
|
|
1797
|
+
return true;
|
|
1798
|
+
}
|
|
1799
|
+
if (status >= 500) {
|
|
1800
|
+
return true;
|
|
1801
|
+
}
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function isAbortError(error: unknown): boolean {
|
|
1806
|
+
if (!error) {
|
|
1807
|
+
return false;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// Most fetch implementations:
|
|
1811
|
+
// - error.name === 'AbortError'
|
|
1812
|
+
// - or error.code === 'ABORT_ERR'
|
|
1813
|
+
if (error instanceof Error) {
|
|
1814
|
+
const name = String(error.name ?? '').toLowerCase();
|
|
1815
|
+
if (name === 'aborterror') {
|
|
1816
|
+
return true;
|
|
1817
|
+
}
|
|
1818
|
+
const msg = String(error.message ?? '').toLowerCase();
|
|
1819
|
+
// Keep this conservative; don't treat every "abort" substring as abort.
|
|
1820
|
+
if (msg === 'aborted' || msg === 'request aborted') {
|
|
1821
|
+
return true;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
if (typeof error === 'object') {
|
|
1826
|
+
const rec = error as Record<string, unknown>;
|
|
1827
|
+
const code = rec.code;
|
|
1828
|
+
if (typeof code === 'string' && code.toUpperCase() === 'ABORT_ERR') {
|
|
1829
|
+
return true;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
return false;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function safeErrorSummary(error: unknown): Record<string, unknown> {
|
|
1837
|
+
if (!error) {
|
|
1838
|
+
return { message: 'Unknown error' };
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
const status = extractHttpStatus(error);
|
|
1842
|
+
const requestId = extractRequestId(error);
|
|
1843
|
+
|
|
1844
|
+
if (error instanceof OpenAiResponsesError) {
|
|
1845
|
+
return {
|
|
1846
|
+
name: error.name,
|
|
1847
|
+
message: error.message,
|
|
1848
|
+
code: error.code,
|
|
1849
|
+
details: error.details,
|
|
1850
|
+
status: typeof status === 'number' ? status : undefined,
|
|
1851
|
+
request_id: requestId,
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
if (error instanceof Error) {
|
|
1856
|
+
return {
|
|
1857
|
+
name: error.name,
|
|
1858
|
+
message: error.message,
|
|
1859
|
+
status: typeof status === 'number' ? status : undefined,
|
|
1860
|
+
request_id: requestId,
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
return {
|
|
1865
|
+
message: String(error),
|
|
1866
|
+
status: typeof status === 'number' ? status : undefined,
|
|
1867
|
+
request_id: requestId,
|
|
1868
|
+
};
|
|
1869
|
+
}
|