@opengeni/codex 0.2.13 → 0.2.16
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/api-client.d.ts +2 -2
- package/dist/fetch.d.ts +9 -1
- package/dist/images.d.ts +34 -0
- package/dist/index.d.ts +17 -16
- package/dist/index.js +483 -158
- package/dist/index.js.map +1 -1
- package/dist/mcp-sanitize.d.ts +1 -1
- package/dist/model-output-truncation.d.ts +1 -1
- package/dist/normalize.d.ts +6 -0
- package/dist/realtime.d.ts +4 -4
- package/dist/refresh.d.ts +1 -1
- package/dist/request-context.d.ts +6 -0
- package/dist/response-timeout.d.ts +1 -1
- package/dist/usage-normalize.d.ts +1 -1
- package/package.json +2 -1
- package/src/fetch.ts +410 -208
- package/src/images.ts +209 -0
- package/src/index.ts +1 -0
- package/src/model-output-truncation.ts +7 -1
- package/src/normalize.ts +26 -0
- package/src/request-context.ts +6 -0
package/src/fetch.ts
CHANGED
|
@@ -39,6 +39,20 @@ export type FetchLike = (input: string | URL | Request, init?: RequestInit) => P
|
|
|
39
39
|
* error that happened during the same Codex turn.
|
|
40
40
|
*/
|
|
41
41
|
export const CODEX_TRANSPORT_ERROR_HEADER = "x-opengeni-codex-transport-error";
|
|
42
|
+
/** Internal transport handoff; always removed before network I/O. */
|
|
43
|
+
export const CODEX_REQUEST_BODY_NORMALIZED_HEADER = "x-opengeni-request-body-normalized";
|
|
44
|
+
const REPLAYABLE_REQUEST_BODY_FACTORY = Symbol.for("opengeni.replayable-request-body-factory");
|
|
45
|
+
|
|
46
|
+
type ReplayableRequestInit = RequestInit & {
|
|
47
|
+
[REPLAYABLE_REQUEST_BODY_FACTORY]?: () => ReadableStream<Uint8Array>;
|
|
48
|
+
};
|
|
49
|
+
/** Internal resolved-model handoff; always removed before network I/O. */
|
|
50
|
+
export const CODEX_REQUEST_MODEL_HEADER = "x-opengeni-request-model";
|
|
51
|
+
/** Internal durable request-identity handoff; always removed before network I/O. */
|
|
52
|
+
export const CODEX_REQUEST_ID_HEADER = "x-opengeni-request-id";
|
|
53
|
+
/** Internal original response-mode handoff; always removed before network I/O. */
|
|
54
|
+
export const CODEX_REQUEST_CALLER_STREAM_HEADER = "x-opengeni-request-caller-stream";
|
|
55
|
+
const MAX_CODEX_ERROR_BODY_BYTES = 64 * 1024;
|
|
42
56
|
|
|
43
57
|
function headersCarryCodexTransportMarker(headers: unknown): boolean {
|
|
44
58
|
if (!headers || typeof headers !== "object") return false;
|
|
@@ -186,25 +200,157 @@ type RequestAudit = {
|
|
|
186
200
|
transportAttempt: number;
|
|
187
201
|
model?: string;
|
|
188
202
|
logicalStartedAt: number;
|
|
189
|
-
|
|
203
|
+
attemptStartedAtMonotonic: number;
|
|
190
204
|
policy: CodexResponseTimeoutPolicy;
|
|
205
|
+
terminalOutcome: RequestTerminalOutcome | null;
|
|
191
206
|
};
|
|
192
207
|
|
|
193
|
-
|
|
208
|
+
type RequestTerminalOutcome = "completed" | "failed" | "timed_out";
|
|
209
|
+
|
|
210
|
+
type SemanticTerminalState = {
|
|
211
|
+
phase: "completed" | "failed" | null;
|
|
212
|
+
/** Non-streaming callers must parse the complete SSE body before settling. */
|
|
213
|
+
deferTransportTerminal: boolean;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
type CodexSseEvent = {
|
|
217
|
+
type?: string;
|
|
218
|
+
response?: Record<string, unknown>;
|
|
219
|
+
error?: unknown;
|
|
220
|
+
code?: unknown;
|
|
221
|
+
message?: unknown;
|
|
222
|
+
param?: unknown;
|
|
223
|
+
item?: unknown;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
type CodexSseTerminalClassification =
|
|
227
|
+
| { phase: "completed" }
|
|
228
|
+
| {
|
|
229
|
+
phase: "failed";
|
|
230
|
+
rawError: unknown;
|
|
231
|
+
fallbackCode: string;
|
|
232
|
+
fallbackMessage: string;
|
|
233
|
+
}
|
|
234
|
+
| null;
|
|
235
|
+
|
|
236
|
+
function classifyCodexSseTerminal(ev: CodexSseEvent): CodexSseTerminalClassification {
|
|
237
|
+
if (ev.type === "response.failed") {
|
|
238
|
+
return {
|
|
239
|
+
phase: "failed",
|
|
240
|
+
rawError: ev.response?.error,
|
|
241
|
+
fallbackCode: "response_failed",
|
|
242
|
+
fallbackMessage: "The Codex response failed",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (ev.type === "error" || ev.type === "response.error") {
|
|
246
|
+
return {
|
|
247
|
+
phase: "failed",
|
|
248
|
+
rawError: ev.error ?? ev.response?.error ?? ev,
|
|
249
|
+
fallbackCode: "response_error",
|
|
250
|
+
fallbackMessage: "The Codex response stream reported an error",
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (ev.type === "response.incomplete") {
|
|
254
|
+
const details = ev.response?.incomplete_details;
|
|
255
|
+
const reason =
|
|
256
|
+
details && typeof details === "object"
|
|
257
|
+
? (details as Record<string, unknown>).reason
|
|
258
|
+
: undefined;
|
|
259
|
+
return {
|
|
260
|
+
phase: "failed",
|
|
261
|
+
rawError: {
|
|
262
|
+
code: "response_incomplete",
|
|
263
|
+
message:
|
|
264
|
+
typeof reason === "string" && reason.length > 0
|
|
265
|
+
? `The Codex response was incomplete (${reason})`
|
|
266
|
+
: "The Codex response was incomplete",
|
|
267
|
+
},
|
|
268
|
+
fallbackCode: "response_incomplete",
|
|
269
|
+
fallbackMessage: "The Codex response was incomplete",
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (ev.type !== "response.completed" && ev.type !== "response.done") {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
if (!ev.response) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const responseStatus = ev.response.status;
|
|
280
|
+
if (
|
|
281
|
+
(responseStatus !== undefined && responseStatus !== "completed") ||
|
|
282
|
+
(ev.response.error !== null && ev.response.error !== undefined)
|
|
283
|
+
) {
|
|
284
|
+
const incomplete = responseStatus === "incomplete";
|
|
285
|
+
return {
|
|
286
|
+
phase: "failed",
|
|
287
|
+
rawError: ev.response.error,
|
|
288
|
+
fallbackCode: incomplete ? "response_incomplete" : "response_failed",
|
|
289
|
+
fallbackMessage: incomplete
|
|
290
|
+
? "The Codex response was incomplete"
|
|
291
|
+
: "The Codex response failed",
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return { phase: "completed" };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function markSemanticTerminal(state: SemanticTerminalState, phase: "completed" | "failed"): void {
|
|
298
|
+
if (state.phase === null) {
|
|
299
|
+
state.phase = phase;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function terminalOutcomeForPhase(
|
|
304
|
+
phase: CodexModelRequestEvent["phase"],
|
|
305
|
+
): RequestTerminalOutcome | null {
|
|
306
|
+
if (phase === "completed" || phase === "failed" || phase === "timed_out") {
|
|
307
|
+
return phase;
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function requestEventFor(
|
|
194
313
|
audit: RequestAudit,
|
|
195
314
|
event: Omit<
|
|
196
315
|
CodexModelRequestEvent,
|
|
197
316
|
"requestId" | "transportAttempt" | "model" | "durationMs" | "timeoutPolicy"
|
|
198
317
|
>,
|
|
199
|
-
):
|
|
200
|
-
|
|
318
|
+
): CodexModelRequestEvent {
|
|
319
|
+
return {
|
|
201
320
|
requestId: audit.requestId,
|
|
202
321
|
transportAttempt: audit.transportAttempt,
|
|
203
322
|
...(audit.model ? { model: audit.model } : {}),
|
|
204
|
-
durationMs: Math.max(0,
|
|
323
|
+
durationMs: Math.max(0, performance.now() - audit.attemptStartedAtMonotonic),
|
|
205
324
|
timeoutPolicy: audit.policy,
|
|
206
325
|
...event,
|
|
207
|
-
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function emitRequestEvent(
|
|
330
|
+
audit: RequestAudit,
|
|
331
|
+
event: Omit<
|
|
332
|
+
CodexModelRequestEvent,
|
|
333
|
+
"requestId" | "transportAttempt" | "model" | "durationMs" | "timeoutPolicy"
|
|
334
|
+
>,
|
|
335
|
+
): Promise<boolean> {
|
|
336
|
+
const terminalOutcome = terminalOutcomeForPhase(event.phase);
|
|
337
|
+
if (terminalOutcome !== null) {
|
|
338
|
+
if (audit.terminalOutcome !== null) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
// Fence before invoking either observer. The durable observer may reject,
|
|
342
|
+
// but a later transport callback must never turn that one terminal into a
|
|
343
|
+
// contradictory second terminal.
|
|
344
|
+
audit.terminalOutcome = terminalOutcome;
|
|
345
|
+
}
|
|
346
|
+
const observed = requestEventFor(audit, event);
|
|
347
|
+
try {
|
|
348
|
+
audit.ctx.onModelRequestDiagnostic?.(observed);
|
|
349
|
+
} catch {
|
|
350
|
+
// Diagnostic observers are strictly non-blocking and cannot affect transport.
|
|
351
|
+
}
|
|
352
|
+
await audit.ctx.onModelRequestEvent?.(observed);
|
|
353
|
+
return true;
|
|
208
354
|
}
|
|
209
355
|
|
|
210
356
|
function providerRequestId(headers: Headers): string | undefined {
|
|
@@ -261,11 +407,13 @@ async function observedResponse(
|
|
|
261
407
|
res: Response,
|
|
262
408
|
audit: RequestAudit,
|
|
263
409
|
externalSignal: AbortSignal | null | undefined,
|
|
410
|
+
semanticTerminal?: SemanticTerminalState,
|
|
264
411
|
): Promise<Response> {
|
|
265
412
|
const requestId = providerRequestId(res.headers);
|
|
266
413
|
if (!res.body) {
|
|
414
|
+
if (semanticTerminal) markSemanticTerminal(semanticTerminal, "failed");
|
|
267
415
|
await emitRequestEvent(audit, {
|
|
268
|
-
phase: res.ok ? "completed" : "failed",
|
|
416
|
+
phase: semanticTerminal?.phase ?? (res.ok ? "completed" : "failed"),
|
|
269
417
|
responseObserved: true,
|
|
270
418
|
status: res.status,
|
|
271
419
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
@@ -293,17 +441,19 @@ async function observedResponse(
|
|
|
293
441
|
if (terminal) return;
|
|
294
442
|
terminal = true;
|
|
295
443
|
clearTimers();
|
|
444
|
+
const semanticPhase = semanticTerminal?.phase;
|
|
445
|
+
const phase = semanticPhase ?? "timed_out";
|
|
296
446
|
const error = new CodexResponseTimeoutError(klass, audit.requestId, true);
|
|
297
447
|
void reader.cancel(error).catch(() => undefined);
|
|
298
448
|
void emitRequestEvent(audit, {
|
|
299
|
-
phase
|
|
449
|
+
phase,
|
|
300
450
|
responseObserved: true,
|
|
301
|
-
timeoutClass: klass,
|
|
451
|
+
...(phase === "timed_out" ? { timeoutClass: klass } : {}),
|
|
302
452
|
status: res.status,
|
|
303
453
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
304
454
|
}).then(
|
|
305
|
-
() => controller.error(error),
|
|
306
|
-
() => controller.error(error),
|
|
455
|
+
() => (phase === "completed" ? controller.close() : controller.error(error)),
|
|
456
|
+
() => (phase === "completed" ? controller.close() : controller.error(error)),
|
|
307
457
|
);
|
|
308
458
|
};
|
|
309
459
|
armIdle = () => {
|
|
@@ -323,13 +473,15 @@ async function observedResponse(
|
|
|
323
473
|
const reason = externalSignal?.reason ?? new DOMException("Aborted", "AbortError");
|
|
324
474
|
void reader.cancel(reason).catch(() => undefined);
|
|
325
475
|
void emitRequestEvent(audit, {
|
|
326
|
-
phase: "failed",
|
|
476
|
+
phase: semanticTerminal?.phase ?? "failed",
|
|
327
477
|
responseObserved: true,
|
|
328
478
|
status: res.status,
|
|
329
479
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
330
480
|
}).then(
|
|
331
|
-
() =>
|
|
332
|
-
|
|
481
|
+
() =>
|
|
482
|
+
semanticTerminal?.phase === "completed" ? controller.close() : controller.error(reason),
|
|
483
|
+
() =>
|
|
484
|
+
semanticTerminal?.phase === "completed" ? controller.close() : controller.error(reason),
|
|
333
485
|
);
|
|
334
486
|
};
|
|
335
487
|
if (externalSignal?.aborted) {
|
|
@@ -348,45 +500,69 @@ async function observedResponse(
|
|
|
348
500
|
if (chunk.done) {
|
|
349
501
|
terminal = true;
|
|
350
502
|
clearTimers();
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
503
|
+
if (semanticTerminal && semanticTerminal.phase === null) {
|
|
504
|
+
if (!semanticTerminal.deferTransportTerminal) {
|
|
505
|
+
markSemanticTerminal(semanticTerminal, "failed");
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (!semanticTerminal?.deferTransportTerminal || semanticTerminal.phase !== null) {
|
|
509
|
+
await emitRequestEvent(audit, {
|
|
510
|
+
phase: semanticTerminal?.phase ?? (res.ok ? "completed" : "failed"),
|
|
511
|
+
responseObserved: true,
|
|
512
|
+
status: res.status,
|
|
513
|
+
...(requestId ? { providerRequestId: requestId } : {}),
|
|
514
|
+
});
|
|
515
|
+
}
|
|
357
516
|
controller.close();
|
|
358
517
|
return;
|
|
359
518
|
}
|
|
360
|
-
armIdle();
|
|
361
519
|
if (!firstByte) {
|
|
362
520
|
firstByte = true;
|
|
521
|
+
// Deliver the provider byte before durable audit I/O. Audit latency
|
|
522
|
+
// is not provider silence and must not manufacture an idle timeout.
|
|
523
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
524
|
+
controller.enqueue(chunk.value);
|
|
363
525
|
await emitRequestEvent(audit, {
|
|
364
526
|
phase: "first_byte",
|
|
365
527
|
responseObserved: true,
|
|
366
528
|
status: res.status,
|
|
367
529
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
368
530
|
});
|
|
531
|
+
if (!terminal) armIdle();
|
|
532
|
+
return;
|
|
369
533
|
}
|
|
534
|
+
armIdle();
|
|
370
535
|
controller.enqueue(chunk.value);
|
|
371
536
|
} catch (error) {
|
|
372
537
|
if (terminal) return;
|
|
373
538
|
terminal = true;
|
|
374
539
|
clearTimers();
|
|
540
|
+
const semanticPhase = semanticTerminal?.phase;
|
|
541
|
+
if (semanticTerminal && semanticPhase === null) {
|
|
542
|
+
markSemanticTerminal(semanticTerminal, "failed");
|
|
543
|
+
}
|
|
375
544
|
await emitRequestEvent(audit, {
|
|
376
|
-
phase: "failed",
|
|
545
|
+
phase: semanticPhase ?? "failed",
|
|
377
546
|
responseObserved: true,
|
|
378
547
|
status: res.status,
|
|
379
548
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
380
549
|
});
|
|
381
|
-
|
|
550
|
+
if (semanticPhase === "completed") {
|
|
551
|
+
controller.close();
|
|
552
|
+
} else {
|
|
553
|
+
controller.error(error);
|
|
554
|
+
}
|
|
382
555
|
}
|
|
383
556
|
},
|
|
384
557
|
async cancel(reason) {
|
|
385
558
|
if (!terminal) {
|
|
386
559
|
terminal = true;
|
|
387
560
|
clearTimers();
|
|
561
|
+
if (semanticTerminal && semanticTerminal.phase === null) {
|
|
562
|
+
markSemanticTerminal(semanticTerminal, "failed");
|
|
563
|
+
}
|
|
388
564
|
await emitRequestEvent(audit, {
|
|
389
|
-
phase: "failed",
|
|
565
|
+
phase: semanticTerminal?.phase ?? "failed",
|
|
390
566
|
responseObserved: true,
|
|
391
567
|
status: res.status,
|
|
392
568
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
@@ -446,7 +622,8 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
446
622
|
const rewritten = rawUrl.replace(/(?<!\/codex)\/responses(\b|$)/, "/codex/responses$1");
|
|
447
623
|
|
|
448
624
|
const policy = resolveCodexResponseTimeoutPolicy(ctx.responseTimeoutPolicy);
|
|
449
|
-
const
|
|
625
|
+
const handedRequestId = new Headers(init?.headers).get(CODEX_REQUEST_ID_HEADER);
|
|
626
|
+
const requestId = handedRequestId ?? ctx.nextRequestId?.() ?? randomUUID();
|
|
450
627
|
const logicalStartedAt = Date.now();
|
|
451
628
|
let transportAttempt = 0;
|
|
452
629
|
|
|
@@ -455,6 +632,13 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
455
632
|
authenticationAttempt: number,
|
|
456
633
|
): Promise<Response> => {
|
|
457
634
|
const headers = new Headers(init?.headers);
|
|
635
|
+
const bodyAlreadyNormalized = headers.get(CODEX_REQUEST_BODY_NORMALIZED_HEADER) === "1";
|
|
636
|
+
const normalizedModel = headers.get(CODEX_REQUEST_MODEL_HEADER) ?? undefined;
|
|
637
|
+
const normalizedCallerStream = headers.get(CODEX_REQUEST_CALLER_STREAM_HEADER);
|
|
638
|
+
headers.delete(CODEX_REQUEST_BODY_NORMALIZED_HEADER);
|
|
639
|
+
headers.delete(CODEX_REQUEST_MODEL_HEADER);
|
|
640
|
+
headers.delete(CODEX_REQUEST_ID_HEADER);
|
|
641
|
+
headers.delete(CODEX_REQUEST_CALLER_STREAM_HEADER);
|
|
458
642
|
headers.set("Authorization", `Bearer ${auth.accessToken}`);
|
|
459
643
|
if (auth.chatgptAccountId) {
|
|
460
644
|
headers.set("ChatGPT-Account-ID", auth.chatgptAccountId);
|
|
@@ -486,13 +670,20 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
486
670
|
}
|
|
487
671
|
|
|
488
672
|
// The backend is streaming-only; force stream=true on the wire but remember
|
|
489
|
-
// the caller's intent
|
|
490
|
-
//
|
|
491
|
-
let callerWantsStream = true;
|
|
492
|
-
let model: string | undefined;
|
|
673
|
+
// the caller's intent for legacy/unowned non-streaming consumers. The owned
|
|
674
|
+
// compaction path consumes the same streaming model boundary as normal turns.
|
|
675
|
+
let callerWantsStream = bodyAlreadyNormalized ? normalizedCallerStream !== "0" : true;
|
|
676
|
+
let model: string | undefined = normalizedModel;
|
|
493
677
|
let requestOpaqueArtifacts: string[] = [];
|
|
494
|
-
const
|
|
495
|
-
|
|
678
|
+
const replayableBodyFactory = (init as ReplayableRequestInit | undefined)?.[
|
|
679
|
+
REPLAYABLE_REQUEST_BODY_FACTORY
|
|
680
|
+
];
|
|
681
|
+
const nextInit: RequestInit = {
|
|
682
|
+
...init,
|
|
683
|
+
headers,
|
|
684
|
+
...(replayableBodyFactory ? { body: replayableBodyFactory() } : {}),
|
|
685
|
+
};
|
|
686
|
+
if (!bodyAlreadyNormalized && typeof init?.body === "string") {
|
|
496
687
|
try {
|
|
497
688
|
const parsed = JSON.parse(init.body) as Record<string, unknown>;
|
|
498
689
|
callerWantsStream = parsed.stream === true;
|
|
@@ -501,10 +692,16 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
501
692
|
nextInit.body = JSON.stringify(normalized);
|
|
502
693
|
requestOpaqueArtifacts = opaqueProviderArtifactFingerprints(normalized.input);
|
|
503
694
|
} catch {
|
|
504
|
-
|
|
695
|
+
// This is the final request-policy boundary for the strict Responses
|
|
696
|
+
// endpoint. Never let malformed bytes bypass the reviewed policy.
|
|
697
|
+
throw new Error("Model request could not be prepared");
|
|
505
698
|
}
|
|
699
|
+
} else if (!bodyAlreadyNormalized) {
|
|
700
|
+
throw new Error("Model request could not be prepared");
|
|
701
|
+
}
|
|
702
|
+
if (!bodyAlreadyNormalized) {
|
|
703
|
+
ctx.onRequestOpaqueArtifacts?.({ requestId, fingerprints: requestOpaqueArtifacts });
|
|
506
704
|
}
|
|
507
|
-
ctx.onRequestOpaqueArtifacts?.({ requestId, fingerprints: requestOpaqueArtifacts });
|
|
508
705
|
headers.set(
|
|
509
706
|
"Idempotency-Key",
|
|
510
707
|
authenticationAttempt === 0 ? requestId : `${requestId}:auth-${authenticationAttempt}`,
|
|
@@ -525,13 +722,18 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
525
722
|
transportAttempt,
|
|
526
723
|
...(model ? { model } : {}),
|
|
527
724
|
logicalStartedAt,
|
|
528
|
-
|
|
725
|
+
attemptStartedAtMonotonic: performance.now(),
|
|
529
726
|
policy,
|
|
727
|
+
terminalOutcome: null,
|
|
530
728
|
};
|
|
531
729
|
await emitRequestEvent(audit, {
|
|
532
730
|
phase: "started",
|
|
533
731
|
responseObserved: false,
|
|
534
732
|
});
|
|
733
|
+
const semanticTerminal: SemanticTerminalState = {
|
|
734
|
+
phase: null,
|
|
735
|
+
deferTransportTerminal: !callerWantsStream,
|
|
736
|
+
};
|
|
535
737
|
try {
|
|
536
738
|
res = await fetchBeforeHeaders(base, rewritten, nextInit, audit);
|
|
537
739
|
const upstreamRequestId = providerRequestId(res.headers);
|
|
@@ -541,8 +743,7 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
541
743
|
status: res.status,
|
|
542
744
|
...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
|
|
543
745
|
});
|
|
544
|
-
|
|
545
|
-
res = observed;
|
|
746
|
+
res = await observedResponse(res, audit, nextInit.signal, semanticTerminal);
|
|
546
747
|
} catch (error) {
|
|
547
748
|
if (nextInit.signal?.aborted) {
|
|
548
749
|
await emitRequestEvent(audit, {
|
|
@@ -590,11 +791,10 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
590
791
|
status: res.status,
|
|
591
792
|
});
|
|
592
793
|
}
|
|
593
|
-
// The
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
// caller, or repair the live stream's terminal event for a streaming caller.
|
|
794
|
+
// The backend leaves terminal response.output empty and delivers assistant
|
|
795
|
+
// items through output_item.done. The typed model reducer reconstructs normal
|
|
796
|
+
// streaming calls; only the legacy non-streaming transport fallback collapses
|
|
797
|
+
// SSE into one JSON response here.
|
|
598
798
|
if (!res.ok) {
|
|
599
799
|
// Buffer the error body once and re-emit it as a concrete JSON Response.
|
|
600
800
|
// A streaming responses request whose error body is left as the raw
|
|
@@ -605,9 +805,25 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
605
805
|
// Response lets the SDK reconstruct error.error for EVERY codex error
|
|
606
806
|
// (401/400/5xx too). For a hard usage cap we also pin x-should-retry:false
|
|
607
807
|
// so the SDK does not burn its retry budget on a limit that won't lift.
|
|
608
|
-
|
|
808
|
+
const buffered = await bufferCodexErrorResponse(res);
|
|
809
|
+
const upstreamRequestId = providerRequestId(res.headers);
|
|
810
|
+
markSemanticTerminal(semanticTerminal, "failed");
|
|
811
|
+
await emitRequestEvent(audit, {
|
|
812
|
+
phase: "failed",
|
|
813
|
+
responseObserved: true,
|
|
814
|
+
status: res.status,
|
|
815
|
+
...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
|
|
816
|
+
}).catch(() => undefined);
|
|
817
|
+
return buffered;
|
|
609
818
|
}
|
|
610
|
-
|
|
819
|
+
if (callerWantsStream) {
|
|
820
|
+
res = validateCodexStream(res, (phase) => {
|
|
821
|
+
markSemanticTerminal(semanticTerminal, phase);
|
|
822
|
+
});
|
|
823
|
+
} else {
|
|
824
|
+
res = await sseToJsonResponse(res, audit, semanticTerminal);
|
|
825
|
+
}
|
|
826
|
+
return res;
|
|
611
827
|
};
|
|
612
828
|
|
|
613
829
|
try {
|
|
@@ -681,35 +897,99 @@ export function classifyCodexUsageLimitError(error: unknown): CodexUsageLimitInf
|
|
|
681
897
|
* Reading the body here also drains the socket of a discarded 401 (no leak).
|
|
682
898
|
*/
|
|
683
899
|
async function bufferCodexErrorResponse(res: Response): Promise<Response> {
|
|
684
|
-
const bodyText = await
|
|
900
|
+
const { text: bodyText, truncated } = await readBoundedResponseText(
|
|
901
|
+
res,
|
|
902
|
+
MAX_CODEX_ERROR_BODY_BYTES,
|
|
903
|
+
);
|
|
685
904
|
const headers = new Headers(res.headers);
|
|
686
905
|
headers.set("content-type", "application/json");
|
|
687
906
|
headers.set(CODEX_TRANSPORT_ERROR_HEADER, "1");
|
|
688
907
|
headers.delete("content-length"); // body re-serialized
|
|
689
908
|
headers.delete("content-encoding"); // text() already decoded any gzip
|
|
690
909
|
let errorType: string | undefined;
|
|
910
|
+
let responseBody = bodyText;
|
|
691
911
|
try {
|
|
692
912
|
const parsed = JSON.parse(bodyText) as { error?: { type?: unknown } };
|
|
693
913
|
errorType = typeof parsed.error?.type === "string" ? parsed.error.type : undefined;
|
|
694
914
|
} catch {
|
|
695
915
|
/* non-JSON error body — leave as-is, no retry-header override */
|
|
696
916
|
}
|
|
917
|
+
if (truncated) {
|
|
918
|
+
responseBody = JSON.stringify({
|
|
919
|
+
error: {
|
|
920
|
+
type: "provider_error_body_too_large",
|
|
921
|
+
code: "provider_error_body_too_large",
|
|
922
|
+
message: `The provider returned an error body larger than ${MAX_CODEX_ERROR_BODY_BYTES} bytes`,
|
|
923
|
+
},
|
|
924
|
+
});
|
|
925
|
+
headers.set("x-opengeni-provider-error-truncated", "1");
|
|
926
|
+
}
|
|
697
927
|
if (errorType === CODEX_USAGE_LIMIT_ERROR_TYPE) {
|
|
698
928
|
headers.set("x-should-retry", "false");
|
|
699
929
|
}
|
|
700
|
-
return new Response(
|
|
930
|
+
return new Response(responseBody, {
|
|
701
931
|
status: res.status,
|
|
702
932
|
statusText: res.statusText,
|
|
703
933
|
headers,
|
|
704
934
|
});
|
|
705
935
|
}
|
|
706
936
|
|
|
937
|
+
async function readBoundedResponseText(
|
|
938
|
+
response: Response,
|
|
939
|
+
maxBytes: number,
|
|
940
|
+
): Promise<{ text: string; truncated: boolean }> {
|
|
941
|
+
if (!response.body) return { text: "", truncated: false };
|
|
942
|
+
const reader = response.body.getReader();
|
|
943
|
+
const decoder = new TextDecoder();
|
|
944
|
+
const parts: string[] = [];
|
|
945
|
+
let bytes = 0;
|
|
946
|
+
let truncated = false;
|
|
947
|
+
try {
|
|
948
|
+
while (bytes < maxBytes) {
|
|
949
|
+
const next = await reader.read();
|
|
950
|
+
if (next.done) {
|
|
951
|
+
parts.push(decoder.decode());
|
|
952
|
+
return { text: parts.join(""), truncated };
|
|
953
|
+
}
|
|
954
|
+
const remaining = maxBytes - bytes;
|
|
955
|
+
const accepted =
|
|
956
|
+
next.value.byteLength > remaining ? next.value.subarray(0, remaining) : next.value;
|
|
957
|
+
bytes += accepted.byteLength;
|
|
958
|
+
parts.push(decoder.decode(accepted, { stream: true }));
|
|
959
|
+
if (accepted.byteLength !== next.value.byteLength) {
|
|
960
|
+
truncated = true;
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
if (bytes >= maxBytes) {
|
|
964
|
+
// Reaching the hard cap is sufficient to classify the body as
|
|
965
|
+
// oversized. Probing for one more chunk can wait forever when an
|
|
966
|
+
// upstream producer stops emitting without closing its stream.
|
|
967
|
+
truncated = true;
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
} catch {
|
|
972
|
+
truncated = true;
|
|
973
|
+
} finally {
|
|
974
|
+
// Cancellation is advisory cleanup. Some Fetch/Streams implementations do
|
|
975
|
+
// not settle cancel() until the producer exits; never let an oversized
|
|
976
|
+
// provider error hold the request open behind that implementation detail.
|
|
977
|
+
if (truncated) void reader.cancel().catch(() => undefined);
|
|
978
|
+
}
|
|
979
|
+
return { text: parts.join(""), truncated };
|
|
980
|
+
}
|
|
981
|
+
|
|
707
982
|
/**
|
|
708
983
|
* Collapse a Responses SSE stream into the single JSON Response object a
|
|
709
984
|
* non-streaming `responses.create` caller expects: the terminal response.*
|
|
710
985
|
* event carries the full `response` payload.
|
|
711
986
|
*/
|
|
712
|
-
async function sseToJsonResponse(
|
|
987
|
+
async function sseToJsonResponse(
|
|
988
|
+
res: Response,
|
|
989
|
+
audit: RequestAudit,
|
|
990
|
+
semanticTerminal: SemanticTerminalState,
|
|
991
|
+
): Promise<Response> {
|
|
992
|
+
const upstreamRequestId = providerRequestId(res.headers);
|
|
713
993
|
const text = await res.text();
|
|
714
994
|
let final: Record<string, unknown> | null = null;
|
|
715
995
|
let terminalError: Response | null = null;
|
|
@@ -720,75 +1000,49 @@ async function sseToJsonResponse(res: Response): Promise<Response> {
|
|
|
720
1000
|
continue;
|
|
721
1001
|
}
|
|
722
1002
|
try {
|
|
723
|
-
const ev = JSON.parse(data) as
|
|
724
|
-
type?: string;
|
|
725
|
-
response?: Record<string, unknown>;
|
|
726
|
-
error?: unknown;
|
|
727
|
-
code?: unknown;
|
|
728
|
-
message?: unknown;
|
|
729
|
-
param?: unknown;
|
|
730
|
-
item?: unknown;
|
|
731
|
-
};
|
|
1003
|
+
const ev = JSON.parse(data) as CodexSseEvent;
|
|
732
1004
|
if (ev.type === "response.output_item.done" && ev.item !== undefined) {
|
|
733
1005
|
items.push(ev.item);
|
|
734
|
-
} else
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
"The Codex response stream reported an error",
|
|
752
|
-
{
|
|
753
|
-
eventType: ev.type,
|
|
754
|
-
responseId: ev.response?.id,
|
|
755
|
-
responseStatus: ev.response?.status,
|
|
756
|
-
},
|
|
757
|
-
);
|
|
758
|
-
} else if (ev.type === "response.incomplete") {
|
|
759
|
-
const details = ev.response?.incomplete_details;
|
|
760
|
-
const reason =
|
|
761
|
-
details && typeof details === "object"
|
|
762
|
-
? (details as Record<string, unknown>).reason
|
|
763
|
-
: undefined;
|
|
764
|
-
terminalError = codexSseFailureResponse(
|
|
765
|
-
res,
|
|
766
|
-
{
|
|
767
|
-
code: "response_incomplete",
|
|
768
|
-
message:
|
|
769
|
-
typeof reason === "string" && reason.length > 0
|
|
770
|
-
? `The Codex response was incomplete (${reason})`
|
|
771
|
-
: "The Codex response was incomplete",
|
|
772
|
-
},
|
|
773
|
-
"response_incomplete",
|
|
774
|
-
"The Codex response was incomplete",
|
|
775
|
-
{
|
|
776
|
-
eventType: ev.type,
|
|
777
|
-
responseId: ev.response?.id,
|
|
778
|
-
responseStatus: ev.response?.status,
|
|
779
|
-
},
|
|
780
|
-
);
|
|
781
|
-
} else if (ev.type === "response.completed" || ev.type === "response.done") {
|
|
782
|
-
final = ev.response ?? null;
|
|
1006
|
+
} else {
|
|
1007
|
+
const terminal = classifyCodexSseTerminal(ev);
|
|
1008
|
+
if (terminal?.phase === "failed") {
|
|
1009
|
+
terminalError = codexSseFailureResponse(
|
|
1010
|
+
res,
|
|
1011
|
+
terminal.rawError,
|
|
1012
|
+
terminal.fallbackCode,
|
|
1013
|
+
terminal.fallbackMessage,
|
|
1014
|
+
{
|
|
1015
|
+
eventType: ev.type,
|
|
1016
|
+
responseId: ev.response?.id,
|
|
1017
|
+
responseStatus: ev.response?.status,
|
|
1018
|
+
},
|
|
1019
|
+
);
|
|
1020
|
+
} else if (terminal?.phase === "completed") {
|
|
1021
|
+
final = ev.response ?? null;
|
|
1022
|
+
}
|
|
783
1023
|
}
|
|
784
1024
|
} catch {
|
|
785
1025
|
/* ignore non-JSON keepalive lines */
|
|
786
1026
|
}
|
|
787
1027
|
}
|
|
788
1028
|
if (terminalError) {
|
|
1029
|
+
markSemanticTerminal(semanticTerminal, "failed");
|
|
1030
|
+
await emitRequestEvent(audit, {
|
|
1031
|
+
phase: "failed",
|
|
1032
|
+
responseObserved: true,
|
|
1033
|
+
status: res.status,
|
|
1034
|
+
...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
|
|
1035
|
+
});
|
|
789
1036
|
return terminalError;
|
|
790
1037
|
}
|
|
791
1038
|
if (!final) {
|
|
1039
|
+
markSemanticTerminal(semanticTerminal, "failed");
|
|
1040
|
+
await emitRequestEvent(audit, {
|
|
1041
|
+
phase: "failed",
|
|
1042
|
+
responseObserved: true,
|
|
1043
|
+
status: res.status,
|
|
1044
|
+
...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
|
|
1045
|
+
});
|
|
792
1046
|
return codexSseFailureResponse(
|
|
793
1047
|
res,
|
|
794
1048
|
null,
|
|
@@ -804,6 +1058,13 @@ async function sseToJsonResponse(res: Response): Promise<Response> {
|
|
|
804
1058
|
`[codex-debug] sse->json items=${items.length} outputLen=${Array.isArray(final?.output) ? (final.output as unknown[]).length : "?"}`,
|
|
805
1059
|
);
|
|
806
1060
|
}
|
|
1061
|
+
markSemanticTerminal(semanticTerminal, "completed");
|
|
1062
|
+
await emitRequestEvent(audit, {
|
|
1063
|
+
phase: "completed",
|
|
1064
|
+
responseObserved: true,
|
|
1065
|
+
status: res.status,
|
|
1066
|
+
...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
|
|
1067
|
+
});
|
|
807
1068
|
const headers = new Headers(res.headers);
|
|
808
1069
|
headers.set("content-type", "application/json");
|
|
809
1070
|
headers.delete("content-length");
|
|
@@ -1074,12 +1335,17 @@ function codexSseFailureError(
|
|
|
1074
1335
|
}
|
|
1075
1336
|
|
|
1076
1337
|
/**
|
|
1077
|
-
*
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1338
|
+
* Preserve a live Responses SSE stream byte-for-byte while translating only
|
|
1339
|
+
* provider-specific terminal failures into typed transport errors. Successful
|
|
1340
|
+
* output reconstruction belongs to the model reducer, so this layer retains no
|
|
1341
|
+
* duplicate output-item graph.
|
|
1080
1342
|
*/
|
|
1081
|
-
function
|
|
1343
|
+
function validateCodexStream(
|
|
1344
|
+
res: Response,
|
|
1345
|
+
onSemanticTerminal?: (phase: "completed" | "failed") => void,
|
|
1346
|
+
): Response {
|
|
1082
1347
|
if (!res.body) {
|
|
1348
|
+
onSemanticTerminal?.("failed");
|
|
1083
1349
|
const error = codexSseFailureError(
|
|
1084
1350
|
res,
|
|
1085
1351
|
null,
|
|
@@ -1099,7 +1365,6 @@ function repairCodexStream(res: Response): Response {
|
|
|
1099
1365
|
headers,
|
|
1100
1366
|
});
|
|
1101
1367
|
}
|
|
1102
|
-
const items: unknown[] = [];
|
|
1103
1368
|
const decoder = new TextDecoder();
|
|
1104
1369
|
const encoder = new TextEncoder();
|
|
1105
1370
|
let buffer = "";
|
|
@@ -1113,9 +1378,8 @@ function repairCodexStream(res: Response): Response {
|
|
|
1113
1378
|
const block = buffer.slice(0, boundary.start);
|
|
1114
1379
|
const separator = buffer.slice(boundary.start, boundary.end);
|
|
1115
1380
|
buffer = buffer.slice(boundary.end);
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
controller.enqueue(encoder.encode(`${patched.block}${separator}`));
|
|
1381
|
+
successfulTerminalSeen ||= inspectCodexSseBlock(block, res, onSemanticTerminal);
|
|
1382
|
+
controller.enqueue(encoder.encode(`${block}${separator}`));
|
|
1119
1383
|
boundary = findSseBlockBoundary(buffer, final);
|
|
1120
1384
|
}
|
|
1121
1385
|
};
|
|
@@ -1128,9 +1392,8 @@ function repairCodexStream(res: Response): Response {
|
|
|
1128
1392
|
buffer += decoder.decode();
|
|
1129
1393
|
emitCompleteBlocks(controller, true);
|
|
1130
1394
|
if (buffer.length > 0) {
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
controller.enqueue(encoder.encode(patched.block));
|
|
1395
|
+
successfulTerminalSeen ||= inspectCodexSseBlock(buffer, res, onSemanticTerminal);
|
|
1396
|
+
controller.enqueue(encoder.encode(buffer));
|
|
1134
1397
|
buffer = "";
|
|
1135
1398
|
}
|
|
1136
1399
|
if (!successfulTerminalSeen) {
|
|
@@ -1182,83 +1445,50 @@ function sseLineEndingEnd(value: string, index: number, final: boolean): number
|
|
|
1182
1445
|
return final ? index + 1 : null;
|
|
1183
1446
|
}
|
|
1184
1447
|
|
|
1185
|
-
|
|
1448
|
+
const CODEX_TERMINAL_TYPE_HINTS = [
|
|
1449
|
+
'"response.completed"',
|
|
1450
|
+
'"response.done"',
|
|
1451
|
+
'"response.failed"',
|
|
1452
|
+
'"response.incomplete"',
|
|
1453
|
+
'"response.error"',
|
|
1454
|
+
'"error"',
|
|
1455
|
+
] as const;
|
|
1186
1456
|
|
|
1187
1457
|
/**
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1458
|
+
* Parse only blocks that can be terminal. Ordinary deltas and output items pass
|
|
1459
|
+
* without object allocation; failed/error/incomplete terminals throw before the
|
|
1460
|
+
* model can mistake them for an ordinary response_done event.
|
|
1191
1461
|
*/
|
|
1192
|
-
function
|
|
1462
|
+
function inspectCodexSseBlock(
|
|
1463
|
+
block: string,
|
|
1464
|
+
source: Response,
|
|
1465
|
+
onSemanticTerminal?: (phase: "completed" | "failed") => void,
|
|
1466
|
+
): boolean {
|
|
1193
1467
|
const lines = block.split(/\r\n|\r|\n/);
|
|
1194
1468
|
const dataStr = lines
|
|
1195
1469
|
.filter((l) => l.startsWith("data:"))
|
|
1196
1470
|
.map((l) => l.slice(5).trim())
|
|
1197
1471
|
.join("\n");
|
|
1198
1472
|
if (!dataStr || dataStr === "[DONE]") {
|
|
1199
|
-
return
|
|
1473
|
+
return false;
|
|
1200
1474
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
error?: unknown;
|
|
1206
|
-
code?: unknown;
|
|
1207
|
-
message?: unknown;
|
|
1208
|
-
param?: unknown;
|
|
1209
|
-
};
|
|
1475
|
+
if (!CODEX_TERMINAL_TYPE_HINTS.some((terminalType) => dataStr.includes(terminalType))) {
|
|
1476
|
+
return false;
|
|
1477
|
+
}
|
|
1478
|
+
let ev: CodexSseEvent;
|
|
1210
1479
|
try {
|
|
1211
1480
|
ev = JSON.parse(dataStr);
|
|
1212
1481
|
} catch {
|
|
1213
|
-
return
|
|
1214
|
-
}
|
|
1215
|
-
if (ev.type === "response.output_item.done" && ev.item !== undefined) {
|
|
1216
|
-
items.push(ev.item);
|
|
1217
|
-
return { block, successfulTerminal: false };
|
|
1218
|
-
}
|
|
1219
|
-
if (ev.type === "response.failed") {
|
|
1220
|
-
throw codexSseFailureError(
|
|
1221
|
-
source,
|
|
1222
|
-
ev.response?.error,
|
|
1223
|
-
"response_failed",
|
|
1224
|
-
"The Codex response failed",
|
|
1225
|
-
{
|
|
1226
|
-
eventType: ev.type,
|
|
1227
|
-
responseId: ev.response?.id,
|
|
1228
|
-
responseStatus: ev.response?.status,
|
|
1229
|
-
},
|
|
1230
|
-
);
|
|
1231
|
-
}
|
|
1232
|
-
if (ev.type === "error" || ev.type === "response.error") {
|
|
1233
|
-
throw codexSseFailureError(
|
|
1234
|
-
source,
|
|
1235
|
-
ev.error ?? ev.response?.error ?? ev,
|
|
1236
|
-
"response_error",
|
|
1237
|
-
"The Codex response stream reported an error",
|
|
1238
|
-
{
|
|
1239
|
-
eventType: ev.type,
|
|
1240
|
-
responseId: ev.response?.id,
|
|
1241
|
-
responseStatus: ev.response?.status,
|
|
1242
|
-
},
|
|
1243
|
-
);
|
|
1482
|
+
return false;
|
|
1244
1483
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
details && typeof details === "object"
|
|
1249
|
-
? (details as Record<string, unknown>).reason
|
|
1250
|
-
: undefined;
|
|
1484
|
+
const terminal = classifyCodexSseTerminal(ev);
|
|
1485
|
+
if (terminal?.phase === "failed") {
|
|
1486
|
+
onSemanticTerminal?.("failed");
|
|
1251
1487
|
throw codexSseFailureError(
|
|
1252
1488
|
source,
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
typeof reason === "string" && reason.length > 0
|
|
1257
|
-
? `The Codex response was incomplete (${reason})`
|
|
1258
|
-
: "The Codex response was incomplete",
|
|
1259
|
-
},
|
|
1260
|
-
"response_incomplete",
|
|
1261
|
-
"The Codex response was incomplete",
|
|
1489
|
+
terminal.rawError,
|
|
1490
|
+
terminal.fallbackCode,
|
|
1491
|
+
terminal.fallbackMessage,
|
|
1262
1492
|
{
|
|
1263
1493
|
eventType: ev.type,
|
|
1264
1494
|
responseId: ev.response?.id,
|
|
@@ -1266,37 +1496,9 @@ function patchSseBlock(block: string, items: unknown[], source: Response): Patch
|
|
|
1266
1496
|
},
|
|
1267
1497
|
);
|
|
1268
1498
|
}
|
|
1269
|
-
if (
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
ev.response.status === "incomplete" ||
|
|
1273
|
-
(ev.response.error !== null && ev.response.error !== undefined)
|
|
1274
|
-
) {
|
|
1275
|
-
throw codexSseFailureError(
|
|
1276
|
-
source,
|
|
1277
|
-
ev.response.error,
|
|
1278
|
-
ev.response.status === "incomplete" ? "response_incomplete" : "response_failed",
|
|
1279
|
-
ev.response.status === "incomplete"
|
|
1280
|
-
? "The Codex response was incomplete"
|
|
1281
|
-
: "The Codex response failed",
|
|
1282
|
-
{
|
|
1283
|
-
eventType: ev.type,
|
|
1284
|
-
responseId: ev.response.id,
|
|
1285
|
-
responseStatus: ev.response.status,
|
|
1286
|
-
},
|
|
1287
|
-
);
|
|
1288
|
-
}
|
|
1289
|
-
const out = ev.response.output;
|
|
1290
|
-
if ((!Array.isArray(out) || out.length === 0) && items.length > 0) {
|
|
1291
|
-
ev.response = { ...ev.response, output: items };
|
|
1292
|
-
const nonData = lines.filter((l) => !l.startsWith("data:"));
|
|
1293
|
-
const lineEnding = block.match(/\r\n|\r|\n/)?.[0] ?? "\n";
|
|
1294
|
-
return {
|
|
1295
|
-
block: [...nonData, `data: ${JSON.stringify(ev)}`].join(lineEnding),
|
|
1296
|
-
successfulTerminal: true,
|
|
1297
|
-
};
|
|
1298
|
-
}
|
|
1299
|
-
return { block, successfulTerminal: true };
|
|
1499
|
+
if (terminal?.phase === "completed") {
|
|
1500
|
+
onSemanticTerminal?.("completed");
|
|
1501
|
+
return true;
|
|
1300
1502
|
}
|
|
1301
|
-
return
|
|
1503
|
+
return false;
|
|
1302
1504
|
}
|