@opengeni/codex 0.2.15 → 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/index.js +197 -125
- package/dist/index.js.map +1 -1
- package/dist/request-context.d.ts +6 -0
- package/package.json +1 -1
- package/src/fetch.ts +270 -160
- package/src/request-context.ts +6 -0
|
@@ -83,6 +83,12 @@ export type CodexRequestContext = {
|
|
|
83
83
|
onUsageHeaders?: (snapshot: CodexUsageHeaderSnapshot) => void;
|
|
84
84
|
/** Optional per-run override, primarily for deterministic transport tests. */
|
|
85
85
|
responseTimeoutPolicy?: Partial<CodexResponseTimeoutPolicy>;
|
|
86
|
+
/**
|
|
87
|
+
* Synchronous, best-effort diagnostics for the request lifecycle. This hook
|
|
88
|
+
* runs before the durable audit sink and MUST remain non-blocking: a throw is
|
|
89
|
+
* swallowed by the transport and it must never receive request bodies/auth.
|
|
90
|
+
*/
|
|
91
|
+
onModelRequestDiagnostic?: (event: CodexModelRequestEvent) => void;
|
|
86
92
|
/** Worker-owned durable audit sink; payloads never contain request bodies or auth. */
|
|
87
93
|
onModelRequestEvent?: (event: CodexModelRequestEvent) => Promise<void> | void;
|
|
88
94
|
/** Exact opaque artifacts on the normalized wire request, never their ciphertext. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/codex",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"description": "ChatGPT/Codex subscription auth + transport: device-code login, token refresh, and the Responses-backend fetch. Pure HTTP + transforms; no database dependency.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
package/src/fetch.ts
CHANGED
|
@@ -200,25 +200,157 @@ type RequestAudit = {
|
|
|
200
200
|
transportAttempt: number;
|
|
201
201
|
model?: string;
|
|
202
202
|
logicalStartedAt: number;
|
|
203
|
-
|
|
203
|
+
attemptStartedAtMonotonic: number;
|
|
204
204
|
policy: CodexResponseTimeoutPolicy;
|
|
205
|
+
terminalOutcome: RequestTerminalOutcome | null;
|
|
205
206
|
};
|
|
206
207
|
|
|
207
|
-
|
|
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(
|
|
208
313
|
audit: RequestAudit,
|
|
209
314
|
event: Omit<
|
|
210
315
|
CodexModelRequestEvent,
|
|
211
316
|
"requestId" | "transportAttempt" | "model" | "durationMs" | "timeoutPolicy"
|
|
212
317
|
>,
|
|
213
|
-
):
|
|
214
|
-
|
|
318
|
+
): CodexModelRequestEvent {
|
|
319
|
+
return {
|
|
215
320
|
requestId: audit.requestId,
|
|
216
321
|
transportAttempt: audit.transportAttempt,
|
|
217
322
|
...(audit.model ? { model: audit.model } : {}),
|
|
218
|
-
durationMs: Math.max(0,
|
|
323
|
+
durationMs: Math.max(0, performance.now() - audit.attemptStartedAtMonotonic),
|
|
219
324
|
timeoutPolicy: audit.policy,
|
|
220
325
|
...event,
|
|
221
|
-
}
|
|
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;
|
|
222
354
|
}
|
|
223
355
|
|
|
224
356
|
function providerRequestId(headers: Headers): string | undefined {
|
|
@@ -275,11 +407,13 @@ async function observedResponse(
|
|
|
275
407
|
res: Response,
|
|
276
408
|
audit: RequestAudit,
|
|
277
409
|
externalSignal: AbortSignal | null | undefined,
|
|
410
|
+
semanticTerminal?: SemanticTerminalState,
|
|
278
411
|
): Promise<Response> {
|
|
279
412
|
const requestId = providerRequestId(res.headers);
|
|
280
413
|
if (!res.body) {
|
|
414
|
+
if (semanticTerminal) markSemanticTerminal(semanticTerminal, "failed");
|
|
281
415
|
await emitRequestEvent(audit, {
|
|
282
|
-
phase: res.ok ? "completed" : "failed",
|
|
416
|
+
phase: semanticTerminal?.phase ?? (res.ok ? "completed" : "failed"),
|
|
283
417
|
responseObserved: true,
|
|
284
418
|
status: res.status,
|
|
285
419
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
@@ -307,17 +441,19 @@ async function observedResponse(
|
|
|
307
441
|
if (terminal) return;
|
|
308
442
|
terminal = true;
|
|
309
443
|
clearTimers();
|
|
444
|
+
const semanticPhase = semanticTerminal?.phase;
|
|
445
|
+
const phase = semanticPhase ?? "timed_out";
|
|
310
446
|
const error = new CodexResponseTimeoutError(klass, audit.requestId, true);
|
|
311
447
|
void reader.cancel(error).catch(() => undefined);
|
|
312
448
|
void emitRequestEvent(audit, {
|
|
313
|
-
phase
|
|
449
|
+
phase,
|
|
314
450
|
responseObserved: true,
|
|
315
|
-
timeoutClass: klass,
|
|
451
|
+
...(phase === "timed_out" ? { timeoutClass: klass } : {}),
|
|
316
452
|
status: res.status,
|
|
317
453
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
318
454
|
}).then(
|
|
319
|
-
() => controller.error(error),
|
|
320
|
-
() => controller.error(error),
|
|
455
|
+
() => (phase === "completed" ? controller.close() : controller.error(error)),
|
|
456
|
+
() => (phase === "completed" ? controller.close() : controller.error(error)),
|
|
321
457
|
);
|
|
322
458
|
};
|
|
323
459
|
armIdle = () => {
|
|
@@ -337,13 +473,15 @@ async function observedResponse(
|
|
|
337
473
|
const reason = externalSignal?.reason ?? new DOMException("Aborted", "AbortError");
|
|
338
474
|
void reader.cancel(reason).catch(() => undefined);
|
|
339
475
|
void emitRequestEvent(audit, {
|
|
340
|
-
phase: "failed",
|
|
476
|
+
phase: semanticTerminal?.phase ?? "failed",
|
|
341
477
|
responseObserved: true,
|
|
342
478
|
status: res.status,
|
|
343
479
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
344
480
|
}).then(
|
|
345
|
-
() =>
|
|
346
|
-
|
|
481
|
+
() =>
|
|
482
|
+
semanticTerminal?.phase === "completed" ? controller.close() : controller.error(reason),
|
|
483
|
+
() =>
|
|
484
|
+
semanticTerminal?.phase === "completed" ? controller.close() : controller.error(reason),
|
|
347
485
|
);
|
|
348
486
|
};
|
|
349
487
|
if (externalSignal?.aborted) {
|
|
@@ -362,12 +500,19 @@ async function observedResponse(
|
|
|
362
500
|
if (chunk.done) {
|
|
363
501
|
terminal = true;
|
|
364
502
|
clearTimers();
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
+
}
|
|
371
516
|
controller.close();
|
|
372
517
|
return;
|
|
373
518
|
}
|
|
@@ -392,21 +537,32 @@ async function observedResponse(
|
|
|
392
537
|
if (terminal) return;
|
|
393
538
|
terminal = true;
|
|
394
539
|
clearTimers();
|
|
540
|
+
const semanticPhase = semanticTerminal?.phase;
|
|
541
|
+
if (semanticTerminal && semanticPhase === null) {
|
|
542
|
+
markSemanticTerminal(semanticTerminal, "failed");
|
|
543
|
+
}
|
|
395
544
|
await emitRequestEvent(audit, {
|
|
396
|
-
phase: "failed",
|
|
545
|
+
phase: semanticPhase ?? "failed",
|
|
397
546
|
responseObserved: true,
|
|
398
547
|
status: res.status,
|
|
399
548
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
400
549
|
});
|
|
401
|
-
|
|
550
|
+
if (semanticPhase === "completed") {
|
|
551
|
+
controller.close();
|
|
552
|
+
} else {
|
|
553
|
+
controller.error(error);
|
|
554
|
+
}
|
|
402
555
|
}
|
|
403
556
|
},
|
|
404
557
|
async cancel(reason) {
|
|
405
558
|
if (!terminal) {
|
|
406
559
|
terminal = true;
|
|
407
560
|
clearTimers();
|
|
561
|
+
if (semanticTerminal && semanticTerminal.phase === null) {
|
|
562
|
+
markSemanticTerminal(semanticTerminal, "failed");
|
|
563
|
+
}
|
|
408
564
|
await emitRequestEvent(audit, {
|
|
409
|
-
phase: "failed",
|
|
565
|
+
phase: semanticTerminal?.phase ?? "failed",
|
|
410
566
|
responseObserved: true,
|
|
411
567
|
status: res.status,
|
|
412
568
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
@@ -566,13 +722,18 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
566
722
|
transportAttempt,
|
|
567
723
|
...(model ? { model } : {}),
|
|
568
724
|
logicalStartedAt,
|
|
569
|
-
|
|
725
|
+
attemptStartedAtMonotonic: performance.now(),
|
|
570
726
|
policy,
|
|
727
|
+
terminalOutcome: null,
|
|
571
728
|
};
|
|
572
729
|
await emitRequestEvent(audit, {
|
|
573
730
|
phase: "started",
|
|
574
731
|
responseObserved: false,
|
|
575
732
|
});
|
|
733
|
+
const semanticTerminal: SemanticTerminalState = {
|
|
734
|
+
phase: null,
|
|
735
|
+
deferTransportTerminal: !callerWantsStream,
|
|
736
|
+
};
|
|
576
737
|
try {
|
|
577
738
|
res = await fetchBeforeHeaders(base, rewritten, nextInit, audit);
|
|
578
739
|
const upstreamRequestId = providerRequestId(res.headers);
|
|
@@ -582,8 +743,7 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
582
743
|
status: res.status,
|
|
583
744
|
...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
|
|
584
745
|
});
|
|
585
|
-
|
|
586
|
-
res = observed;
|
|
746
|
+
res = await observedResponse(res, audit, nextInit.signal, semanticTerminal);
|
|
587
747
|
} catch (error) {
|
|
588
748
|
if (nextInit.signal?.aborted) {
|
|
589
749
|
await emitRequestEvent(audit, {
|
|
@@ -645,9 +805,25 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
645
805
|
// Response lets the SDK reconstruct error.error for EVERY codex error
|
|
646
806
|
// (401/400/5xx too). For a hard usage cap we also pin x-should-retry:false
|
|
647
807
|
// so the SDK does not burn its retry budget on a limit that won't lift.
|
|
648
|
-
|
|
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;
|
|
649
818
|
}
|
|
650
|
-
|
|
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;
|
|
651
827
|
};
|
|
652
828
|
|
|
653
829
|
try {
|
|
@@ -808,7 +984,12 @@ async function readBoundedResponseText(
|
|
|
808
984
|
* non-streaming `responses.create` caller expects: the terminal response.*
|
|
809
985
|
* event carries the full `response` payload.
|
|
810
986
|
*/
|
|
811
|
-
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);
|
|
812
993
|
const text = await res.text();
|
|
813
994
|
let final: Record<string, unknown> | null = null;
|
|
814
995
|
let terminalError: Response | null = null;
|
|
@@ -819,75 +1000,49 @@ async function sseToJsonResponse(res: Response): Promise<Response> {
|
|
|
819
1000
|
continue;
|
|
820
1001
|
}
|
|
821
1002
|
try {
|
|
822
|
-
const ev = JSON.parse(data) as
|
|
823
|
-
type?: string;
|
|
824
|
-
response?: Record<string, unknown>;
|
|
825
|
-
error?: unknown;
|
|
826
|
-
code?: unknown;
|
|
827
|
-
message?: unknown;
|
|
828
|
-
param?: unknown;
|
|
829
|
-
item?: unknown;
|
|
830
|
-
};
|
|
1003
|
+
const ev = JSON.parse(data) as CodexSseEvent;
|
|
831
1004
|
if (ev.type === "response.output_item.done" && ev.item !== undefined) {
|
|
832
1005
|
items.push(ev.item);
|
|
833
|
-
} else
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
"The Codex response stream reported an error",
|
|
851
|
-
{
|
|
852
|
-
eventType: ev.type,
|
|
853
|
-
responseId: ev.response?.id,
|
|
854
|
-
responseStatus: ev.response?.status,
|
|
855
|
-
},
|
|
856
|
-
);
|
|
857
|
-
} else if (ev.type === "response.incomplete") {
|
|
858
|
-
const details = ev.response?.incomplete_details;
|
|
859
|
-
const reason =
|
|
860
|
-
details && typeof details === "object"
|
|
861
|
-
? (details as Record<string, unknown>).reason
|
|
862
|
-
: undefined;
|
|
863
|
-
terminalError = codexSseFailureResponse(
|
|
864
|
-
res,
|
|
865
|
-
{
|
|
866
|
-
code: "response_incomplete",
|
|
867
|
-
message:
|
|
868
|
-
typeof reason === "string" && reason.length > 0
|
|
869
|
-
? `The Codex response was incomplete (${reason})`
|
|
870
|
-
: "The Codex response was incomplete",
|
|
871
|
-
},
|
|
872
|
-
"response_incomplete",
|
|
873
|
-
"The Codex response was incomplete",
|
|
874
|
-
{
|
|
875
|
-
eventType: ev.type,
|
|
876
|
-
responseId: ev.response?.id,
|
|
877
|
-
responseStatus: ev.response?.status,
|
|
878
|
-
},
|
|
879
|
-
);
|
|
880
|
-
} else if (ev.type === "response.completed" || ev.type === "response.done") {
|
|
881
|
-
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
|
+
}
|
|
882
1023
|
}
|
|
883
1024
|
} catch {
|
|
884
1025
|
/* ignore non-JSON keepalive lines */
|
|
885
1026
|
}
|
|
886
1027
|
}
|
|
887
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
|
+
});
|
|
888
1036
|
return terminalError;
|
|
889
1037
|
}
|
|
890
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
|
+
});
|
|
891
1046
|
return codexSseFailureResponse(
|
|
892
1047
|
res,
|
|
893
1048
|
null,
|
|
@@ -903,6 +1058,13 @@ async function sseToJsonResponse(res: Response): Promise<Response> {
|
|
|
903
1058
|
`[codex-debug] sse->json items=${items.length} outputLen=${Array.isArray(final?.output) ? (final.output as unknown[]).length : "?"}`,
|
|
904
1059
|
);
|
|
905
1060
|
}
|
|
1061
|
+
markSemanticTerminal(semanticTerminal, "completed");
|
|
1062
|
+
await emitRequestEvent(audit, {
|
|
1063
|
+
phase: "completed",
|
|
1064
|
+
responseObserved: true,
|
|
1065
|
+
status: res.status,
|
|
1066
|
+
...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
|
|
1067
|
+
});
|
|
906
1068
|
const headers = new Headers(res.headers);
|
|
907
1069
|
headers.set("content-type", "application/json");
|
|
908
1070
|
headers.delete("content-length");
|
|
@@ -1178,8 +1340,12 @@ function codexSseFailureError(
|
|
|
1178
1340
|
* output reconstruction belongs to the model reducer, so this layer retains no
|
|
1179
1341
|
* duplicate output-item graph.
|
|
1180
1342
|
*/
|
|
1181
|
-
function validateCodexStream(
|
|
1343
|
+
function validateCodexStream(
|
|
1344
|
+
res: Response,
|
|
1345
|
+
onSemanticTerminal?: (phase: "completed" | "failed") => void,
|
|
1346
|
+
): Response {
|
|
1182
1347
|
if (!res.body) {
|
|
1348
|
+
onSemanticTerminal?.("failed");
|
|
1183
1349
|
const error = codexSseFailureError(
|
|
1184
1350
|
res,
|
|
1185
1351
|
null,
|
|
@@ -1212,7 +1378,7 @@ function validateCodexStream(res: Response): Response {
|
|
|
1212
1378
|
const block = buffer.slice(0, boundary.start);
|
|
1213
1379
|
const separator = buffer.slice(boundary.start, boundary.end);
|
|
1214
1380
|
buffer = buffer.slice(boundary.end);
|
|
1215
|
-
successfulTerminalSeen ||= inspectCodexSseBlock(block, res);
|
|
1381
|
+
successfulTerminalSeen ||= inspectCodexSseBlock(block, res, onSemanticTerminal);
|
|
1216
1382
|
controller.enqueue(encoder.encode(`${block}${separator}`));
|
|
1217
1383
|
boundary = findSseBlockBoundary(buffer, final);
|
|
1218
1384
|
}
|
|
@@ -1226,7 +1392,7 @@ function validateCodexStream(res: Response): Response {
|
|
|
1226
1392
|
buffer += decoder.decode();
|
|
1227
1393
|
emitCompleteBlocks(controller, true);
|
|
1228
1394
|
if (buffer.length > 0) {
|
|
1229
|
-
successfulTerminalSeen ||= inspectCodexSseBlock(buffer, res);
|
|
1395
|
+
successfulTerminalSeen ||= inspectCodexSseBlock(buffer, res, onSemanticTerminal);
|
|
1230
1396
|
controller.enqueue(encoder.encode(buffer));
|
|
1231
1397
|
buffer = "";
|
|
1232
1398
|
}
|
|
@@ -1293,7 +1459,11 @@ const CODEX_TERMINAL_TYPE_HINTS = [
|
|
|
1293
1459
|
* without object allocation; failed/error/incomplete terminals throw before the
|
|
1294
1460
|
* model can mistake them for an ordinary response_done event.
|
|
1295
1461
|
*/
|
|
1296
|
-
function inspectCodexSseBlock(
|
|
1462
|
+
function inspectCodexSseBlock(
|
|
1463
|
+
block: string,
|
|
1464
|
+
source: Response,
|
|
1465
|
+
onSemanticTerminal?: (phase: "completed" | "failed") => void,
|
|
1466
|
+
): boolean {
|
|
1297
1467
|
const lines = block.split(/\r\n|\r|\n/);
|
|
1298
1468
|
const dataStr = lines
|
|
1299
1469
|
.filter((l) => l.startsWith("data:"))
|
|
@@ -1305,26 +1475,20 @@ function inspectCodexSseBlock(block: string, source: Response): boolean {
|
|
|
1305
1475
|
if (!CODEX_TERMINAL_TYPE_HINTS.some((terminalType) => dataStr.includes(terminalType))) {
|
|
1306
1476
|
return false;
|
|
1307
1477
|
}
|
|
1308
|
-
let ev:
|
|
1309
|
-
type?: string;
|
|
1310
|
-
item?: unknown;
|
|
1311
|
-
response?: Record<string, unknown>;
|
|
1312
|
-
error?: unknown;
|
|
1313
|
-
code?: unknown;
|
|
1314
|
-
message?: unknown;
|
|
1315
|
-
param?: unknown;
|
|
1316
|
-
};
|
|
1478
|
+
let ev: CodexSseEvent;
|
|
1317
1479
|
try {
|
|
1318
1480
|
ev = JSON.parse(dataStr);
|
|
1319
1481
|
} catch {
|
|
1320
1482
|
return false;
|
|
1321
1483
|
}
|
|
1322
|
-
|
|
1484
|
+
const terminal = classifyCodexSseTerminal(ev);
|
|
1485
|
+
if (terminal?.phase === "failed") {
|
|
1486
|
+
onSemanticTerminal?.("failed");
|
|
1323
1487
|
throw codexSseFailureError(
|
|
1324
1488
|
source,
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1489
|
+
terminal.rawError,
|
|
1490
|
+
terminal.fallbackCode,
|
|
1491
|
+
terminal.fallbackMessage,
|
|
1328
1492
|
{
|
|
1329
1493
|
eventType: ev.type,
|
|
1330
1494
|
responseId: ev.response?.id,
|
|
@@ -1332,62 +1496,8 @@ function inspectCodexSseBlock(block: string, source: Response): boolean {
|
|
|
1332
1496
|
},
|
|
1333
1497
|
);
|
|
1334
1498
|
}
|
|
1335
|
-
if (
|
|
1336
|
-
|
|
1337
|
-
source,
|
|
1338
|
-
ev.error ?? ev.response?.error ?? ev,
|
|
1339
|
-
"response_error",
|
|
1340
|
-
"The Codex response stream reported an error",
|
|
1341
|
-
{
|
|
1342
|
-
eventType: ev.type,
|
|
1343
|
-
responseId: ev.response?.id,
|
|
1344
|
-
responseStatus: ev.response?.status,
|
|
1345
|
-
},
|
|
1346
|
-
);
|
|
1347
|
-
}
|
|
1348
|
-
if (ev.type === "response.incomplete") {
|
|
1349
|
-
const details = ev.response?.incomplete_details;
|
|
1350
|
-
const reason =
|
|
1351
|
-
details && typeof details === "object"
|
|
1352
|
-
? (details as Record<string, unknown>).reason
|
|
1353
|
-
: undefined;
|
|
1354
|
-
throw codexSseFailureError(
|
|
1355
|
-
source,
|
|
1356
|
-
{
|
|
1357
|
-
code: "response_incomplete",
|
|
1358
|
-
message:
|
|
1359
|
-
typeof reason === "string" && reason.length > 0
|
|
1360
|
-
? `The Codex response was incomplete (${reason})`
|
|
1361
|
-
: "The Codex response was incomplete",
|
|
1362
|
-
},
|
|
1363
|
-
"response_incomplete",
|
|
1364
|
-
"The Codex response was incomplete",
|
|
1365
|
-
{
|
|
1366
|
-
eventType: ev.type,
|
|
1367
|
-
responseId: ev.response?.id,
|
|
1368
|
-
responseStatus: ev.response?.status,
|
|
1369
|
-
},
|
|
1370
|
-
);
|
|
1371
|
-
}
|
|
1372
|
-
if ((ev.type === "response.completed" || ev.type === "response.done") && ev.response) {
|
|
1373
|
-
if (
|
|
1374
|
-
(ev.response.status !== undefined && ev.response.status !== "completed") ||
|
|
1375
|
-
(ev.response.error !== null && ev.response.error !== undefined)
|
|
1376
|
-
) {
|
|
1377
|
-
throw codexSseFailureError(
|
|
1378
|
-
source,
|
|
1379
|
-
ev.response.error,
|
|
1380
|
-
ev.response.status === "incomplete" ? "response_incomplete" : "response_failed",
|
|
1381
|
-
ev.response.status === "incomplete"
|
|
1382
|
-
? "The Codex response was incomplete"
|
|
1383
|
-
: "The Codex response failed",
|
|
1384
|
-
{
|
|
1385
|
-
eventType: ev.type,
|
|
1386
|
-
responseId: ev.response.id,
|
|
1387
|
-
responseStatus: ev.response.status,
|
|
1388
|
-
},
|
|
1389
|
-
);
|
|
1390
|
-
}
|
|
1499
|
+
if (terminal?.phase === "completed") {
|
|
1500
|
+
onSemanticTerminal?.("completed");
|
|
1391
1501
|
return true;
|
|
1392
1502
|
}
|
|
1393
1503
|
return false;
|
package/src/request-context.ts
CHANGED
|
@@ -97,6 +97,12 @@ export type CodexRequestContext = {
|
|
|
97
97
|
onUsageHeaders?: (snapshot: CodexUsageHeaderSnapshot) => void;
|
|
98
98
|
/** Optional per-run override, primarily for deterministic transport tests. */
|
|
99
99
|
responseTimeoutPolicy?: Partial<CodexResponseTimeoutPolicy>;
|
|
100
|
+
/**
|
|
101
|
+
* Synchronous, best-effort diagnostics for the request lifecycle. This hook
|
|
102
|
+
* runs before the durable audit sink and MUST remain non-blocking: a throw is
|
|
103
|
+
* swallowed by the transport and it must never receive request bodies/auth.
|
|
104
|
+
*/
|
|
105
|
+
onModelRequestDiagnostic?: (event: CodexModelRequestEvent) => void;
|
|
100
106
|
/** Worker-owned durable audit sink; payloads never contain request bodies or auth. */
|
|
101
107
|
onModelRequestEvent?: (event: CodexModelRequestEvent) => Promise<void> | void;
|
|
102
108
|
/** Exact opaque artifacts on the normalized wire request, never their ciphertext. */
|