@opengeni/codex 0.2.15 → 0.2.17
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 +212 -127
- package/dist/index.js.map +1 -1
- package/dist/request-context.d.ts +9 -0
- package/package.json +1 -1
- package/src/fetch.ts +291 -162
- package/src/request-context.ts +13 -0
package/src/fetch.ts
CHANGED
|
@@ -17,11 +17,23 @@ import { opaqueProviderArtifactFingerprints } from "./opaque-artifact";
|
|
|
17
17
|
import {
|
|
18
18
|
codexRequestStorage,
|
|
19
19
|
type CodexModelRequestEvent,
|
|
20
|
+
type CodexRequestPreparationPhase,
|
|
20
21
|
type CodexRequestContext,
|
|
21
22
|
type CodexResponseTimeoutPolicy,
|
|
22
23
|
type CodexTokenSnapshot,
|
|
23
24
|
type CodexUsageHeaderSnapshot,
|
|
24
25
|
} from "./request-context";
|
|
26
|
+
|
|
27
|
+
function emitRequestPreparationDiagnostic(
|
|
28
|
+
ctx: CodexRequestContext,
|
|
29
|
+
phase: CodexRequestPreparationPhase,
|
|
30
|
+
): void {
|
|
31
|
+
try {
|
|
32
|
+
ctx.onRequestPreparationDiagnostic?.(phase);
|
|
33
|
+
} catch {
|
|
34
|
+
// Diagnostic observers are non-blocking and cannot affect transport.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
25
37
|
import {
|
|
26
38
|
CODEX_RESPONSE_TIMEOUT_ERROR_TYPE,
|
|
27
39
|
CodexResponseTimeoutError,
|
|
@@ -200,25 +212,157 @@ type RequestAudit = {
|
|
|
200
212
|
transportAttempt: number;
|
|
201
213
|
model?: string;
|
|
202
214
|
logicalStartedAt: number;
|
|
203
|
-
|
|
215
|
+
attemptStartedAtMonotonic: number;
|
|
204
216
|
policy: CodexResponseTimeoutPolicy;
|
|
217
|
+
terminalOutcome: RequestTerminalOutcome | null;
|
|
205
218
|
};
|
|
206
219
|
|
|
207
|
-
|
|
220
|
+
type RequestTerminalOutcome = "completed" | "failed" | "timed_out";
|
|
221
|
+
|
|
222
|
+
type SemanticTerminalState = {
|
|
223
|
+
phase: "completed" | "failed" | null;
|
|
224
|
+
/** Non-streaming callers must parse the complete SSE body before settling. */
|
|
225
|
+
deferTransportTerminal: boolean;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
type CodexSseEvent = {
|
|
229
|
+
type?: string;
|
|
230
|
+
response?: Record<string, unknown>;
|
|
231
|
+
error?: unknown;
|
|
232
|
+
code?: unknown;
|
|
233
|
+
message?: unknown;
|
|
234
|
+
param?: unknown;
|
|
235
|
+
item?: unknown;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
type CodexSseTerminalClassification =
|
|
239
|
+
| { phase: "completed" }
|
|
240
|
+
| {
|
|
241
|
+
phase: "failed";
|
|
242
|
+
rawError: unknown;
|
|
243
|
+
fallbackCode: string;
|
|
244
|
+
fallbackMessage: string;
|
|
245
|
+
}
|
|
246
|
+
| null;
|
|
247
|
+
|
|
248
|
+
function classifyCodexSseTerminal(ev: CodexSseEvent): CodexSseTerminalClassification {
|
|
249
|
+
if (ev.type === "response.failed") {
|
|
250
|
+
return {
|
|
251
|
+
phase: "failed",
|
|
252
|
+
rawError: ev.response?.error,
|
|
253
|
+
fallbackCode: "response_failed",
|
|
254
|
+
fallbackMessage: "The Codex response failed",
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (ev.type === "error" || ev.type === "response.error") {
|
|
258
|
+
return {
|
|
259
|
+
phase: "failed",
|
|
260
|
+
rawError: ev.error ?? ev.response?.error ?? ev,
|
|
261
|
+
fallbackCode: "response_error",
|
|
262
|
+
fallbackMessage: "The Codex response stream reported an error",
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
if (ev.type === "response.incomplete") {
|
|
266
|
+
const details = ev.response?.incomplete_details;
|
|
267
|
+
const reason =
|
|
268
|
+
details && typeof details === "object"
|
|
269
|
+
? (details as Record<string, unknown>).reason
|
|
270
|
+
: undefined;
|
|
271
|
+
return {
|
|
272
|
+
phase: "failed",
|
|
273
|
+
rawError: {
|
|
274
|
+
code: "response_incomplete",
|
|
275
|
+
message:
|
|
276
|
+
typeof reason === "string" && reason.length > 0
|
|
277
|
+
? `The Codex response was incomplete (${reason})`
|
|
278
|
+
: "The Codex response was incomplete",
|
|
279
|
+
},
|
|
280
|
+
fallbackCode: "response_incomplete",
|
|
281
|
+
fallbackMessage: "The Codex response was incomplete",
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (ev.type !== "response.completed" && ev.type !== "response.done") {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
if (!ev.response) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const responseStatus = ev.response.status;
|
|
292
|
+
if (
|
|
293
|
+
(responseStatus !== undefined && responseStatus !== "completed") ||
|
|
294
|
+
(ev.response.error !== null && ev.response.error !== undefined)
|
|
295
|
+
) {
|
|
296
|
+
const incomplete = responseStatus === "incomplete";
|
|
297
|
+
return {
|
|
298
|
+
phase: "failed",
|
|
299
|
+
rawError: ev.response.error,
|
|
300
|
+
fallbackCode: incomplete ? "response_incomplete" : "response_failed",
|
|
301
|
+
fallbackMessage: incomplete
|
|
302
|
+
? "The Codex response was incomplete"
|
|
303
|
+
: "The Codex response failed",
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
return { phase: "completed" };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function markSemanticTerminal(state: SemanticTerminalState, phase: "completed" | "failed"): void {
|
|
310
|
+
if (state.phase === null) {
|
|
311
|
+
state.phase = phase;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function terminalOutcomeForPhase(
|
|
316
|
+
phase: CodexModelRequestEvent["phase"],
|
|
317
|
+
): RequestTerminalOutcome | null {
|
|
318
|
+
if (phase === "completed" || phase === "failed" || phase === "timed_out") {
|
|
319
|
+
return phase;
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function requestEventFor(
|
|
208
325
|
audit: RequestAudit,
|
|
209
326
|
event: Omit<
|
|
210
327
|
CodexModelRequestEvent,
|
|
211
328
|
"requestId" | "transportAttempt" | "model" | "durationMs" | "timeoutPolicy"
|
|
212
329
|
>,
|
|
213
|
-
):
|
|
214
|
-
|
|
330
|
+
): CodexModelRequestEvent {
|
|
331
|
+
return {
|
|
215
332
|
requestId: audit.requestId,
|
|
216
333
|
transportAttempt: audit.transportAttempt,
|
|
217
334
|
...(audit.model ? { model: audit.model } : {}),
|
|
218
|
-
durationMs: Math.max(0,
|
|
335
|
+
durationMs: Math.max(0, performance.now() - audit.attemptStartedAtMonotonic),
|
|
219
336
|
timeoutPolicy: audit.policy,
|
|
220
337
|
...event,
|
|
221
|
-
}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function emitRequestEvent(
|
|
342
|
+
audit: RequestAudit,
|
|
343
|
+
event: Omit<
|
|
344
|
+
CodexModelRequestEvent,
|
|
345
|
+
"requestId" | "transportAttempt" | "model" | "durationMs" | "timeoutPolicy"
|
|
346
|
+
>,
|
|
347
|
+
): Promise<boolean> {
|
|
348
|
+
const terminalOutcome = terminalOutcomeForPhase(event.phase);
|
|
349
|
+
if (terminalOutcome !== null) {
|
|
350
|
+
if (audit.terminalOutcome !== null) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
// Fence before invoking either observer. The durable observer may reject,
|
|
354
|
+
// but a later transport callback must never turn that one terminal into a
|
|
355
|
+
// contradictory second terminal.
|
|
356
|
+
audit.terminalOutcome = terminalOutcome;
|
|
357
|
+
}
|
|
358
|
+
const observed = requestEventFor(audit, event);
|
|
359
|
+
try {
|
|
360
|
+
audit.ctx.onModelRequestDiagnostic?.(observed);
|
|
361
|
+
} catch {
|
|
362
|
+
// Diagnostic observers are strictly non-blocking and cannot affect transport.
|
|
363
|
+
}
|
|
364
|
+
await audit.ctx.onModelRequestEvent?.(observed);
|
|
365
|
+
return true;
|
|
222
366
|
}
|
|
223
367
|
|
|
224
368
|
function providerRequestId(headers: Headers): string | undefined {
|
|
@@ -275,11 +419,13 @@ async function observedResponse(
|
|
|
275
419
|
res: Response,
|
|
276
420
|
audit: RequestAudit,
|
|
277
421
|
externalSignal: AbortSignal | null | undefined,
|
|
422
|
+
semanticTerminal?: SemanticTerminalState,
|
|
278
423
|
): Promise<Response> {
|
|
279
424
|
const requestId = providerRequestId(res.headers);
|
|
280
425
|
if (!res.body) {
|
|
426
|
+
if (semanticTerminal) markSemanticTerminal(semanticTerminal, "failed");
|
|
281
427
|
await emitRequestEvent(audit, {
|
|
282
|
-
phase: res.ok ? "completed" : "failed",
|
|
428
|
+
phase: semanticTerminal?.phase ?? (res.ok ? "completed" : "failed"),
|
|
283
429
|
responseObserved: true,
|
|
284
430
|
status: res.status,
|
|
285
431
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
@@ -307,17 +453,19 @@ async function observedResponse(
|
|
|
307
453
|
if (terminal) return;
|
|
308
454
|
terminal = true;
|
|
309
455
|
clearTimers();
|
|
456
|
+
const semanticPhase = semanticTerminal?.phase;
|
|
457
|
+
const phase = semanticPhase ?? "timed_out";
|
|
310
458
|
const error = new CodexResponseTimeoutError(klass, audit.requestId, true);
|
|
311
459
|
void reader.cancel(error).catch(() => undefined);
|
|
312
460
|
void emitRequestEvent(audit, {
|
|
313
|
-
phase
|
|
461
|
+
phase,
|
|
314
462
|
responseObserved: true,
|
|
315
|
-
timeoutClass: klass,
|
|
463
|
+
...(phase === "timed_out" ? { timeoutClass: klass } : {}),
|
|
316
464
|
status: res.status,
|
|
317
465
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
318
466
|
}).then(
|
|
319
|
-
() => controller.error(error),
|
|
320
|
-
() => controller.error(error),
|
|
467
|
+
() => (phase === "completed" ? controller.close() : controller.error(error)),
|
|
468
|
+
() => (phase === "completed" ? controller.close() : controller.error(error)),
|
|
321
469
|
);
|
|
322
470
|
};
|
|
323
471
|
armIdle = () => {
|
|
@@ -337,13 +485,15 @@ async function observedResponse(
|
|
|
337
485
|
const reason = externalSignal?.reason ?? new DOMException("Aborted", "AbortError");
|
|
338
486
|
void reader.cancel(reason).catch(() => undefined);
|
|
339
487
|
void emitRequestEvent(audit, {
|
|
340
|
-
phase: "failed",
|
|
488
|
+
phase: semanticTerminal?.phase ?? "failed",
|
|
341
489
|
responseObserved: true,
|
|
342
490
|
status: res.status,
|
|
343
491
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
344
492
|
}).then(
|
|
345
|
-
() =>
|
|
346
|
-
|
|
493
|
+
() =>
|
|
494
|
+
semanticTerminal?.phase === "completed" ? controller.close() : controller.error(reason),
|
|
495
|
+
() =>
|
|
496
|
+
semanticTerminal?.phase === "completed" ? controller.close() : controller.error(reason),
|
|
347
497
|
);
|
|
348
498
|
};
|
|
349
499
|
if (externalSignal?.aborted) {
|
|
@@ -362,12 +512,19 @@ async function observedResponse(
|
|
|
362
512
|
if (chunk.done) {
|
|
363
513
|
terminal = true;
|
|
364
514
|
clearTimers();
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
515
|
+
if (semanticTerminal && semanticTerminal.phase === null) {
|
|
516
|
+
if (!semanticTerminal.deferTransportTerminal) {
|
|
517
|
+
markSemanticTerminal(semanticTerminal, "failed");
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (!semanticTerminal?.deferTransportTerminal || semanticTerminal.phase !== null) {
|
|
521
|
+
await emitRequestEvent(audit, {
|
|
522
|
+
phase: semanticTerminal?.phase ?? (res.ok ? "completed" : "failed"),
|
|
523
|
+
responseObserved: true,
|
|
524
|
+
status: res.status,
|
|
525
|
+
...(requestId ? { providerRequestId: requestId } : {}),
|
|
526
|
+
});
|
|
527
|
+
}
|
|
371
528
|
controller.close();
|
|
372
529
|
return;
|
|
373
530
|
}
|
|
@@ -392,21 +549,32 @@ async function observedResponse(
|
|
|
392
549
|
if (terminal) return;
|
|
393
550
|
terminal = true;
|
|
394
551
|
clearTimers();
|
|
552
|
+
const semanticPhase = semanticTerminal?.phase;
|
|
553
|
+
if (semanticTerminal && semanticPhase === null) {
|
|
554
|
+
markSemanticTerminal(semanticTerminal, "failed");
|
|
555
|
+
}
|
|
395
556
|
await emitRequestEvent(audit, {
|
|
396
|
-
phase: "failed",
|
|
557
|
+
phase: semanticPhase ?? "failed",
|
|
397
558
|
responseObserved: true,
|
|
398
559
|
status: res.status,
|
|
399
560
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
400
561
|
});
|
|
401
|
-
|
|
562
|
+
if (semanticPhase === "completed") {
|
|
563
|
+
controller.close();
|
|
564
|
+
} else {
|
|
565
|
+
controller.error(error);
|
|
566
|
+
}
|
|
402
567
|
}
|
|
403
568
|
},
|
|
404
569
|
async cancel(reason) {
|
|
405
570
|
if (!terminal) {
|
|
406
571
|
terminal = true;
|
|
407
572
|
clearTimers();
|
|
573
|
+
if (semanticTerminal && semanticTerminal.phase === null) {
|
|
574
|
+
markSemanticTerminal(semanticTerminal, "failed");
|
|
575
|
+
}
|
|
408
576
|
await emitRequestEvent(audit, {
|
|
409
|
-
phase: "failed",
|
|
577
|
+
phase: semanticTerminal?.phase ?? "failed",
|
|
410
578
|
responseObserved: true,
|
|
411
579
|
status: res.status,
|
|
412
580
|
...(requestId ? { providerRequestId: requestId } : {}),
|
|
@@ -458,6 +626,7 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
458
626
|
if (!ctx) {
|
|
459
627
|
return base(input, init); // not a codex turn — passthrough, untouched
|
|
460
628
|
}
|
|
629
|
+
emitRequestPreparationDiagnostic(ctx, "transport_entry");
|
|
461
630
|
|
|
462
631
|
const rawUrl =
|
|
463
632
|
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
@@ -544,7 +713,10 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
544
713
|
throw new Error("Model request could not be prepared");
|
|
545
714
|
}
|
|
546
715
|
if (!bodyAlreadyNormalized) {
|
|
547
|
-
ctx.onRequestOpaqueArtifacts?.({
|
|
716
|
+
ctx.onRequestOpaqueArtifacts?.({
|
|
717
|
+
requestId,
|
|
718
|
+
fingerprints: requestOpaqueArtifacts,
|
|
719
|
+
});
|
|
548
720
|
}
|
|
549
721
|
headers.set(
|
|
550
722
|
"Idempotency-Key",
|
|
@@ -566,13 +738,19 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
566
738
|
transportAttempt,
|
|
567
739
|
...(model ? { model } : {}),
|
|
568
740
|
logicalStartedAt,
|
|
569
|
-
|
|
741
|
+
attemptStartedAtMonotonic: performance.now(),
|
|
570
742
|
policy,
|
|
743
|
+
terminalOutcome: null,
|
|
571
744
|
};
|
|
745
|
+
emitRequestPreparationDiagnostic(ctx, "wire_request_ready");
|
|
572
746
|
await emitRequestEvent(audit, {
|
|
573
747
|
phase: "started",
|
|
574
748
|
responseObserved: false,
|
|
575
749
|
});
|
|
750
|
+
const semanticTerminal: SemanticTerminalState = {
|
|
751
|
+
phase: null,
|
|
752
|
+
deferTransportTerminal: !callerWantsStream,
|
|
753
|
+
};
|
|
576
754
|
try {
|
|
577
755
|
res = await fetchBeforeHeaders(base, rewritten, nextInit, audit);
|
|
578
756
|
const upstreamRequestId = providerRequestId(res.headers);
|
|
@@ -582,8 +760,7 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
582
760
|
status: res.status,
|
|
583
761
|
...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
|
|
584
762
|
});
|
|
585
|
-
|
|
586
|
-
res = observed;
|
|
763
|
+
res = await observedResponse(res, audit, nextInit.signal, semanticTerminal);
|
|
587
764
|
} catch (error) {
|
|
588
765
|
if (nextInit.signal?.aborted) {
|
|
589
766
|
await emitRequestEvent(audit, {
|
|
@@ -645,13 +822,31 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
645
822
|
// Response lets the SDK reconstruct error.error for EVERY codex error
|
|
646
823
|
// (401/400/5xx too). For a hard usage cap we also pin x-should-retry:false
|
|
647
824
|
// so the SDK does not burn its retry budget on a limit that won't lift.
|
|
648
|
-
|
|
825
|
+
const buffered = await bufferCodexErrorResponse(res);
|
|
826
|
+
const upstreamRequestId = providerRequestId(res.headers);
|
|
827
|
+
markSemanticTerminal(semanticTerminal, "failed");
|
|
828
|
+
await emitRequestEvent(audit, {
|
|
829
|
+
phase: "failed",
|
|
830
|
+
responseObserved: true,
|
|
831
|
+
status: res.status,
|
|
832
|
+
...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
|
|
833
|
+
}).catch(() => undefined);
|
|
834
|
+
return buffered;
|
|
649
835
|
}
|
|
650
|
-
|
|
836
|
+
if (callerWantsStream) {
|
|
837
|
+
res = validateCodexStream(res, (phase) => {
|
|
838
|
+
markSemanticTerminal(semanticTerminal, phase);
|
|
839
|
+
});
|
|
840
|
+
} else {
|
|
841
|
+
res = await sseToJsonResponse(res, audit, semanticTerminal);
|
|
842
|
+
}
|
|
843
|
+
return res;
|
|
651
844
|
};
|
|
652
845
|
|
|
653
846
|
try {
|
|
654
|
-
|
|
847
|
+
const token = await ctx.getToken();
|
|
848
|
+
emitRequestPreparationDiagnostic(ctx, "credential_ready");
|
|
849
|
+
let res = await attempt(token, 0);
|
|
655
850
|
if (res.status === 401) {
|
|
656
851
|
res = await attempt(await ctx.refresh(), 1); // single refresh-on-401 retry (spec §1.9)
|
|
657
852
|
}
|
|
@@ -808,7 +1003,12 @@ async function readBoundedResponseText(
|
|
|
808
1003
|
* non-streaming `responses.create` caller expects: the terminal response.*
|
|
809
1004
|
* event carries the full `response` payload.
|
|
810
1005
|
*/
|
|
811
|
-
async function sseToJsonResponse(
|
|
1006
|
+
async function sseToJsonResponse(
|
|
1007
|
+
res: Response,
|
|
1008
|
+
audit: RequestAudit,
|
|
1009
|
+
semanticTerminal: SemanticTerminalState,
|
|
1010
|
+
): Promise<Response> {
|
|
1011
|
+
const upstreamRequestId = providerRequestId(res.headers);
|
|
812
1012
|
const text = await res.text();
|
|
813
1013
|
let final: Record<string, unknown> | null = null;
|
|
814
1014
|
let terminalError: Response | null = null;
|
|
@@ -819,75 +1019,49 @@ async function sseToJsonResponse(res: Response): Promise<Response> {
|
|
|
819
1019
|
continue;
|
|
820
1020
|
}
|
|
821
1021
|
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
|
-
};
|
|
1022
|
+
const ev = JSON.parse(data) as CodexSseEvent;
|
|
831
1023
|
if (ev.type === "response.output_item.done" && ev.item !== undefined) {
|
|
832
1024
|
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;
|
|
1025
|
+
} else {
|
|
1026
|
+
const terminal = classifyCodexSseTerminal(ev);
|
|
1027
|
+
if (terminal?.phase === "failed") {
|
|
1028
|
+
terminalError = codexSseFailureResponse(
|
|
1029
|
+
res,
|
|
1030
|
+
terminal.rawError,
|
|
1031
|
+
terminal.fallbackCode,
|
|
1032
|
+
terminal.fallbackMessage,
|
|
1033
|
+
{
|
|
1034
|
+
eventType: ev.type,
|
|
1035
|
+
responseId: ev.response?.id,
|
|
1036
|
+
responseStatus: ev.response?.status,
|
|
1037
|
+
},
|
|
1038
|
+
);
|
|
1039
|
+
} else if (terminal?.phase === "completed") {
|
|
1040
|
+
final = ev.response ?? null;
|
|
1041
|
+
}
|
|
882
1042
|
}
|
|
883
1043
|
} catch {
|
|
884
1044
|
/* ignore non-JSON keepalive lines */
|
|
885
1045
|
}
|
|
886
1046
|
}
|
|
887
1047
|
if (terminalError) {
|
|
1048
|
+
markSemanticTerminal(semanticTerminal, "failed");
|
|
1049
|
+
await emitRequestEvent(audit, {
|
|
1050
|
+
phase: "failed",
|
|
1051
|
+
responseObserved: true,
|
|
1052
|
+
status: res.status,
|
|
1053
|
+
...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
|
|
1054
|
+
});
|
|
888
1055
|
return terminalError;
|
|
889
1056
|
}
|
|
890
1057
|
if (!final) {
|
|
1058
|
+
markSemanticTerminal(semanticTerminal, "failed");
|
|
1059
|
+
await emitRequestEvent(audit, {
|
|
1060
|
+
phase: "failed",
|
|
1061
|
+
responseObserved: true,
|
|
1062
|
+
status: res.status,
|
|
1063
|
+
...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
|
|
1064
|
+
});
|
|
891
1065
|
return codexSseFailureResponse(
|
|
892
1066
|
res,
|
|
893
1067
|
null,
|
|
@@ -903,6 +1077,13 @@ async function sseToJsonResponse(res: Response): Promise<Response> {
|
|
|
903
1077
|
`[codex-debug] sse->json items=${items.length} outputLen=${Array.isArray(final?.output) ? (final.output as unknown[]).length : "?"}`,
|
|
904
1078
|
);
|
|
905
1079
|
}
|
|
1080
|
+
markSemanticTerminal(semanticTerminal, "completed");
|
|
1081
|
+
await emitRequestEvent(audit, {
|
|
1082
|
+
phase: "completed",
|
|
1083
|
+
responseObserved: true,
|
|
1084
|
+
status: res.status,
|
|
1085
|
+
...(upstreamRequestId ? { providerRequestId: upstreamRequestId } : {}),
|
|
1086
|
+
});
|
|
906
1087
|
const headers = new Headers(res.headers);
|
|
907
1088
|
headers.set("content-type", "application/json");
|
|
908
1089
|
headers.delete("content-length");
|
|
@@ -1178,8 +1359,12 @@ function codexSseFailureError(
|
|
|
1178
1359
|
* output reconstruction belongs to the model reducer, so this layer retains no
|
|
1179
1360
|
* duplicate output-item graph.
|
|
1180
1361
|
*/
|
|
1181
|
-
function validateCodexStream(
|
|
1362
|
+
function validateCodexStream(
|
|
1363
|
+
res: Response,
|
|
1364
|
+
onSemanticTerminal?: (phase: "completed" | "failed") => void,
|
|
1365
|
+
): Response {
|
|
1182
1366
|
if (!res.body) {
|
|
1367
|
+
onSemanticTerminal?.("failed");
|
|
1183
1368
|
const error = codexSseFailureError(
|
|
1184
1369
|
res,
|
|
1185
1370
|
null,
|
|
@@ -1212,7 +1397,7 @@ function validateCodexStream(res: Response): Response {
|
|
|
1212
1397
|
const block = buffer.slice(0, boundary.start);
|
|
1213
1398
|
const separator = buffer.slice(boundary.start, boundary.end);
|
|
1214
1399
|
buffer = buffer.slice(boundary.end);
|
|
1215
|
-
successfulTerminalSeen ||= inspectCodexSseBlock(block, res);
|
|
1400
|
+
successfulTerminalSeen ||= inspectCodexSseBlock(block, res, onSemanticTerminal);
|
|
1216
1401
|
controller.enqueue(encoder.encode(`${block}${separator}`));
|
|
1217
1402
|
boundary = findSseBlockBoundary(buffer, final);
|
|
1218
1403
|
}
|
|
@@ -1226,7 +1411,7 @@ function validateCodexStream(res: Response): Response {
|
|
|
1226
1411
|
buffer += decoder.decode();
|
|
1227
1412
|
emitCompleteBlocks(controller, true);
|
|
1228
1413
|
if (buffer.length > 0) {
|
|
1229
|
-
successfulTerminalSeen ||= inspectCodexSseBlock(buffer, res);
|
|
1414
|
+
successfulTerminalSeen ||= inspectCodexSseBlock(buffer, res, onSemanticTerminal);
|
|
1230
1415
|
controller.enqueue(encoder.encode(buffer));
|
|
1231
1416
|
buffer = "";
|
|
1232
1417
|
}
|
|
@@ -1293,7 +1478,11 @@ const CODEX_TERMINAL_TYPE_HINTS = [
|
|
|
1293
1478
|
* without object allocation; failed/error/incomplete terminals throw before the
|
|
1294
1479
|
* model can mistake them for an ordinary response_done event.
|
|
1295
1480
|
*/
|
|
1296
|
-
function inspectCodexSseBlock(
|
|
1481
|
+
function inspectCodexSseBlock(
|
|
1482
|
+
block: string,
|
|
1483
|
+
source: Response,
|
|
1484
|
+
onSemanticTerminal?: (phase: "completed" | "failed") => void,
|
|
1485
|
+
): boolean {
|
|
1297
1486
|
const lines = block.split(/\r\n|\r|\n/);
|
|
1298
1487
|
const dataStr = lines
|
|
1299
1488
|
.filter((l) => l.startsWith("data:"))
|
|
@@ -1305,39 +1494,20 @@ function inspectCodexSseBlock(block: string, source: Response): boolean {
|
|
|
1305
1494
|
if (!CODEX_TERMINAL_TYPE_HINTS.some((terminalType) => dataStr.includes(terminalType))) {
|
|
1306
1495
|
return false;
|
|
1307
1496
|
}
|
|
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
|
-
};
|
|
1497
|
+
let ev: CodexSseEvent;
|
|
1317
1498
|
try {
|
|
1318
1499
|
ev = JSON.parse(dataStr);
|
|
1319
1500
|
} catch {
|
|
1320
1501
|
return false;
|
|
1321
1502
|
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
ev.response?.error,
|
|
1326
|
-
"response_failed",
|
|
1327
|
-
"The Codex response failed",
|
|
1328
|
-
{
|
|
1329
|
-
eventType: ev.type,
|
|
1330
|
-
responseId: ev.response?.id,
|
|
1331
|
-
responseStatus: ev.response?.status,
|
|
1332
|
-
},
|
|
1333
|
-
);
|
|
1334
|
-
}
|
|
1335
|
-
if (ev.type === "error" || ev.type === "response.error") {
|
|
1503
|
+
const terminal = classifyCodexSseTerminal(ev);
|
|
1504
|
+
if (terminal?.phase === "failed") {
|
|
1505
|
+
onSemanticTerminal?.("failed");
|
|
1336
1506
|
throw codexSseFailureError(
|
|
1337
1507
|
source,
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1508
|
+
terminal.rawError,
|
|
1509
|
+
terminal.fallbackCode,
|
|
1510
|
+
terminal.fallbackMessage,
|
|
1341
1511
|
{
|
|
1342
1512
|
eventType: ev.type,
|
|
1343
1513
|
responseId: ev.response?.id,
|
|
@@ -1345,49 +1515,8 @@ function inspectCodexSseBlock(block: string, source: Response): boolean {
|
|
|
1345
1515
|
},
|
|
1346
1516
|
);
|
|
1347
1517
|
}
|
|
1348
|
-
if (
|
|
1349
|
-
|
|
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
|
-
}
|
|
1518
|
+
if (terminal?.phase === "completed") {
|
|
1519
|
+
onSemanticTerminal?.("completed");
|
|
1391
1520
|
return true;
|
|
1392
1521
|
}
|
|
1393
1522
|
return false;
|
package/src/request-context.ts
CHANGED
|
@@ -67,6 +67,11 @@ export type CodexRequestOpaqueArtifacts = {
|
|
|
67
67
|
fingerprints: readonly string[];
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
export type CodexRequestPreparationPhase =
|
|
71
|
+
| "transport_entry"
|
|
72
|
+
| "credential_ready"
|
|
73
|
+
| "wire_request_ready";
|
|
74
|
+
|
|
70
75
|
export type CodexRequestContext = {
|
|
71
76
|
clientVersion: string;
|
|
72
77
|
/**
|
|
@@ -97,6 +102,14 @@ export type CodexRequestContext = {
|
|
|
97
102
|
onUsageHeaders?: (snapshot: CodexUsageHeaderSnapshot) => void;
|
|
98
103
|
/** Optional per-run override, primarily for deterministic transport tests. */
|
|
99
104
|
responseTimeoutPolicy?: Partial<CodexResponseTimeoutPolicy>;
|
|
105
|
+
/**
|
|
106
|
+
* Synchronous, best-effort diagnostics for the request lifecycle. This hook
|
|
107
|
+
* runs before the durable audit sink and MUST remain non-blocking: a throw is
|
|
108
|
+
* swallowed by the transport and it must never receive request bodies/auth.
|
|
109
|
+
*/
|
|
110
|
+
onModelRequestDiagnostic?: (event: CodexModelRequestEvent) => void;
|
|
111
|
+
/** Bounded synchronous checkpoints for pre-network request preparation. */
|
|
112
|
+
onRequestPreparationDiagnostic?: (phase: CodexRequestPreparationPhase) => void;
|
|
100
113
|
/** Worker-owned durable audit sink; payloads never contain request bodies or auth. */
|
|
101
114
|
onModelRequestEvent?: (event: CodexModelRequestEvent) => Promise<void> | void;
|
|
102
115
|
/** Exact opaque artifacts on the normalized wire request, never their ciphertext. */
|