pi-openai-codex-compat 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +71 -37
- package/extensions/openai-codex-compat/apply-patch-diff-render.ts +6 -2
- package/extensions/openai-codex-compat/apply-patch-engine.ts +89 -5
- package/extensions/openai-codex-compat/codex-cache-diagnostics.ts +97 -0
- package/extensions/openai-codex-compat/codex-installation.ts +51 -0
- package/extensions/openai-codex-compat/codex-metadata.ts +139 -0
- package/extensions/openai-codex-compat/codex-protocol.ts +1 -0
- package/extensions/openai-codex-compat/codex-provider.ts +524 -58
- package/extensions/openai-codex-compat/codex-stream.ts +52 -26
- package/extensions/openai-codex-compat/codex-thread-lineage.ts +156 -0
- package/extensions/openai-codex-compat/codex-transport.ts +1129 -86
- package/extensions/openai-codex-compat/compaction-checkpoint.ts +2 -2
- package/extensions/openai-codex-compat/config.ts +13 -0
- package/extensions/openai-codex-compat/index.ts +6 -0
- package/extensions/openai-codex-compat/namespaced-tools.ts +2 -0
- package/extensions/openai-codex-compat/output-limit-continuation.ts +151 -0
- package/extensions/openai-codex-compat/remote-compaction.ts +13 -0
- package/extensions/openai-codex-compat/request-options.ts +2 -2
- package/extensions/openai-codex-compat/responses-lite.ts +147 -0
- package/extensions/openai-codex-compat/settings-pane.ts +11 -0
- package/package.json +2 -2
|
@@ -9,8 +9,16 @@ import {
|
|
|
9
9
|
uuidv7,
|
|
10
10
|
} from "@earendil-works/pi-ai";
|
|
11
11
|
import { codexCacheKey } from "./codex-cache-key.ts";
|
|
12
|
+
import type { CodexCacheDiagnosticContext } from "./codex-cache-diagnostics.ts";
|
|
13
|
+
import {
|
|
14
|
+
applyCodexMetadataHeaders,
|
|
15
|
+
CODEX_INSTALLATION_ID_METADATA_KEY,
|
|
16
|
+
CODEX_WINDOW_ID_HEADER,
|
|
17
|
+
type CodexRequestKind,
|
|
18
|
+
} from "./codex-metadata.ts";
|
|
12
19
|
import { isObject, type JsonRecord } from "./codex-protocol.ts";
|
|
13
|
-
import { normalizeReplayItem } from "./responses-replay.ts";
|
|
20
|
+
import { normalizeReplayItem, stableResponsesJson } from "./responses-replay.ts";
|
|
21
|
+
import { applyResponsesLiteHeaders, responsesLiteSsePayload } from "./responses-lite.ts";
|
|
14
22
|
|
|
15
23
|
/**
|
|
16
24
|
* Focused adaptation of @earendil-works/pi-ai@0.83.0
|
|
@@ -19,15 +27,20 @@ import { normalizeReplayItem } from "./responses-replay.ts";
|
|
|
19
27
|
|
|
20
28
|
const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
21
29
|
const OPENAI_BETA_RESPONSES_WEBSOCKETS = "responses_websockets=2026-02-06";
|
|
30
|
+
export const CODEX_WS_REQUEST_START_METADATA_KEY = "x-codex-ws-stream-request-start-ms";
|
|
22
31
|
const DEFAULT_MAX_RETRIES = 0;
|
|
23
|
-
const BASE_DELAY_MS =
|
|
32
|
+
const BASE_DELAY_MS = 200;
|
|
33
|
+
const DEFAULT_WEBSOCKET_MAX_RETRIES = 5;
|
|
34
|
+
const DEFAULT_WEBSOCKET_RETRY_BASE_DELAY_MS = 200;
|
|
35
|
+
const DEFAULT_SSE_STREAM_MAX_RETRIES = 5;
|
|
36
|
+
const DEFAULT_SSE_STREAM_RETRY_BASE_DELAY_MS = 200;
|
|
24
37
|
const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;
|
|
25
38
|
const REQUEST_COMPRESSION_ZSTD_LEVEL = 3;
|
|
26
39
|
const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS = 15_000;
|
|
27
|
-
const SESSION_WEBSOCKET_CACHE_TTL_MS = 5 * 60 * 1_000;
|
|
28
|
-
const SESSION_WEBSOCKET_MAX_AGE_MS = 55 * 60 * 1_000;
|
|
29
40
|
const WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE = "websocket_connection_limit_reached";
|
|
30
41
|
const PREVIOUS_RESPONSE_NOT_FOUND_CODE = "previous_response_not_found";
|
|
42
|
+
const CODEX_TURN_STATE_HEADER = "x-codex-turn-state";
|
|
43
|
+
const CODEX_ROUTING_HINT_HEADER = "x-codex-routing-hint";
|
|
31
44
|
|
|
32
45
|
type ProcessWithBuiltinModules = typeof process & {
|
|
33
46
|
getBuiltinModule?: {
|
|
@@ -44,7 +57,7 @@ export type CodexJsonRequestOptions = {
|
|
|
44
57
|
fetch?: typeof fetch;
|
|
45
58
|
};
|
|
46
59
|
|
|
47
|
-
export type
|
|
60
|
+
export type CodexTransportFailureDiagnostic = {
|
|
48
61
|
type: "provider_transport_failure";
|
|
49
62
|
timestamp: number;
|
|
50
63
|
error: {
|
|
@@ -62,6 +75,154 @@ export type CodexTransportDiagnostic = {
|
|
|
62
75
|
};
|
|
63
76
|
};
|
|
64
77
|
|
|
78
|
+
export type CodexContinuationBypassReason =
|
|
79
|
+
| "request_template_changed"
|
|
80
|
+
| "non_array_input"
|
|
81
|
+
| "history_prefix_changed";
|
|
82
|
+
|
|
83
|
+
type CodexContinuationHistoryMismatch = {
|
|
84
|
+
index: number;
|
|
85
|
+
baselineInputItems: number;
|
|
86
|
+
currentInputItems: number;
|
|
87
|
+
baselineItem?: unknown;
|
|
88
|
+
currentItem?: unknown;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
type CodexTransportRecoveryAttempt = {
|
|
92
|
+
transport: "websocket" | "sse";
|
|
93
|
+
connection?: "new" | "reused";
|
|
94
|
+
contextMode: "full" | "delta";
|
|
95
|
+
inputItems: number;
|
|
96
|
+
fullInputItems: number;
|
|
97
|
+
fullRequestBytes: number;
|
|
98
|
+
wireRequestBytes: number;
|
|
99
|
+
outcome: "selected" | "previous_response_not_found" | "retry_scheduled";
|
|
100
|
+
turnStateReplayed?: boolean;
|
|
101
|
+
turnStateReplayedValue?: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export type CodexTransportRecoveryDiagnostic = {
|
|
105
|
+
type: "codex_transport_recovery";
|
|
106
|
+
timestamp: number;
|
|
107
|
+
details: {
|
|
108
|
+
trigger:
|
|
109
|
+
| "previous_response_not_found"
|
|
110
|
+
| "local_continuation_bypass"
|
|
111
|
+
| "websocket_retry"
|
|
112
|
+
| "sse_stream_retry"
|
|
113
|
+
| "sse_after_websocket_failure"
|
|
114
|
+
| "sticky_sse_after_websocket_failure";
|
|
115
|
+
configuredTransport: string;
|
|
116
|
+
previousResponseId?: string;
|
|
117
|
+
continuationBypassReason?: CodexContinuationBypassReason;
|
|
118
|
+
historyMismatch?: CodexContinuationHistoryMismatch;
|
|
119
|
+
error?: CodexTransportFailureDiagnostic["error"];
|
|
120
|
+
attempts: CodexTransportRecoveryAttempt[];
|
|
121
|
+
cacheIdentity: CacheIdentitySnapshot;
|
|
122
|
+
previousCacheIdentity?: CacheIdentitySnapshot;
|
|
123
|
+
cacheAffinityEnabled: boolean;
|
|
124
|
+
cacheIdentityPreserved?: boolean;
|
|
125
|
+
promptKeyAndHeaderAligned: boolean;
|
|
126
|
+
accountIdentityPreserved?: boolean;
|
|
127
|
+
retryNumber?: number;
|
|
128
|
+
maxRetries?: number;
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export type CodexTransportPrewarmDiagnostic = {
|
|
133
|
+
type: "codex_transport_prewarm";
|
|
134
|
+
timestamp: number;
|
|
135
|
+
details: {
|
|
136
|
+
outcome: "completed" | "failed" | "skipped";
|
|
137
|
+
continuationReady: boolean;
|
|
138
|
+
reason?: "sse_configured" | "sticky_sse_fallback";
|
|
139
|
+
cache?: CodexCacheDiagnosticContext;
|
|
140
|
+
turnStateAvailableAtStart?: boolean;
|
|
141
|
+
turnStateReceived?: boolean;
|
|
142
|
+
turnStateAtStart?: string;
|
|
143
|
+
turnStateReceivedValue?: string;
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export type CodexCacheUsageDiagnostic = {
|
|
148
|
+
inputTokens: number;
|
|
149
|
+
cachedTokens: number;
|
|
150
|
+
cacheWriteTokens: number;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export type CodexTransportRequestDiagnostic = {
|
|
154
|
+
type: "codex_transport_request";
|
|
155
|
+
timestamp: number;
|
|
156
|
+
details: {
|
|
157
|
+
requestKind: CodexRequestKind;
|
|
158
|
+
configuredTransport: string;
|
|
159
|
+
selectedTransport: "websocket" | "sse";
|
|
160
|
+
connection?: "new" | "reused";
|
|
161
|
+
contextMode: "full" | "delta";
|
|
162
|
+
inputItems: number;
|
|
163
|
+
fullInputItems: number;
|
|
164
|
+
fullRequestBytes: number;
|
|
165
|
+
wireRequestBytes: number;
|
|
166
|
+
cacheAffinityEnabled: boolean;
|
|
167
|
+
promptKeyAndHeaderAligned: boolean;
|
|
168
|
+
cacheIdentity: CacheIdentitySnapshot;
|
|
169
|
+
sessionId?: string;
|
|
170
|
+
promptCacheKey?: string;
|
|
171
|
+
accountId: string;
|
|
172
|
+
clientSessionId?: string;
|
|
173
|
+
threadId?: string;
|
|
174
|
+
turnId?: string;
|
|
175
|
+
installationId?: string;
|
|
176
|
+
windowId?: string;
|
|
177
|
+
routingHint?: string;
|
|
178
|
+
turnMetadata?: string;
|
|
179
|
+
responseId?: string;
|
|
180
|
+
previousResponseId?: string;
|
|
181
|
+
turnStateAvailableAtStart: boolean;
|
|
182
|
+
turnStateReplayed: boolean;
|
|
183
|
+
turnStateReceived: boolean;
|
|
184
|
+
turnStateAtStart?: string;
|
|
185
|
+
turnStateReplayedValue?: string;
|
|
186
|
+
turnStateReceivedValue?: string;
|
|
187
|
+
usage?: CodexCacheUsageDiagnostic;
|
|
188
|
+
cache?: CodexCacheDiagnosticContext;
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export type CodexTransportDiagnostic =
|
|
193
|
+
| CodexTransportFailureDiagnostic
|
|
194
|
+
| CodexTransportRecoveryDiagnostic
|
|
195
|
+
| CodexTransportPrewarmDiagnostic
|
|
196
|
+
| CodexTransportRequestDiagnostic;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* In-memory, turn-scoped server routing state. Diagnostics read the value
|
|
200
|
+
* explicitly so it is visible only where the transport records it deliberately.
|
|
201
|
+
*/
|
|
202
|
+
export class CodexTurnState {
|
|
203
|
+
#value: string | undefined;
|
|
204
|
+
#revision = 0;
|
|
205
|
+
|
|
206
|
+
get available(): boolean {
|
|
207
|
+
return this.#value !== undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
get revision(): number {
|
|
211
|
+
return this.#revision;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
replayValue(): string | undefined {
|
|
215
|
+
return this.#value;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
capture(value: string | undefined): boolean {
|
|
219
|
+
if (this.#value !== undefined || !value) return false;
|
|
220
|
+
this.#value = value;
|
|
221
|
+
this.#revision += 1;
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
65
226
|
export type CodexContinuationHandle = {
|
|
66
227
|
readonly responseId: string;
|
|
67
228
|
replaceResponseItems(items: readonly JsonRecord[]): boolean;
|
|
@@ -85,6 +246,7 @@ export interface OpenAICodexWebSocketDebugStats {
|
|
|
85
246
|
lastPreviousResponseId?: string;
|
|
86
247
|
websocketFailures: number;
|
|
87
248
|
sseFallbacks: number;
|
|
249
|
+
prewarmRequests: number;
|
|
88
250
|
websocketFallbackActive?: boolean;
|
|
89
251
|
lastWebSocketError?: string;
|
|
90
252
|
}
|
|
@@ -96,6 +258,14 @@ type CodexTransportOptions = OpenAICodexResponsesOptions & {
|
|
|
96
258
|
onWebSocketResponseHandle?(handle: CodexWebSocketResponseHandle): void;
|
|
97
259
|
onTransportStart?(): void;
|
|
98
260
|
onTransportDiagnostic?(diagnostic: CodexTransportDiagnostic): void;
|
|
261
|
+
warmup?: boolean;
|
|
262
|
+
requestKind?: CodexRequestKind;
|
|
263
|
+
turnState?: CodexTurnState;
|
|
264
|
+
cacheDiagnostics?: CodexCacheDiagnosticContext;
|
|
265
|
+
websocketMaxRetries?: number;
|
|
266
|
+
websocketRetryBaseDelayMs?: number;
|
|
267
|
+
sseStreamMaxRetries?: number;
|
|
268
|
+
sseStreamRetryBaseDelayMs?: number;
|
|
99
269
|
};
|
|
100
270
|
|
|
101
271
|
type WebSocketEventType = "open" | "message" | "error" | "close";
|
|
@@ -117,8 +287,6 @@ type WebSocketConstructor = new (
|
|
|
117
287
|
type CachedWebSocket = {
|
|
118
288
|
socket: WebSocketLike;
|
|
119
289
|
busy: boolean;
|
|
120
|
-
createdAt: number;
|
|
121
|
-
idleTimer?: ReturnType<typeof setTimeout>;
|
|
122
290
|
continuation?: {
|
|
123
291
|
lastRequestBody: JsonRecord;
|
|
124
292
|
lastResponseId: string;
|
|
@@ -126,8 +294,23 @@ type CachedWebSocket = {
|
|
|
126
294
|
};
|
|
127
295
|
};
|
|
128
296
|
|
|
297
|
+
export type CacheIdentitySnapshot = {
|
|
298
|
+
promptCacheKey: string | undefined;
|
|
299
|
+
sessionHeader: string | null;
|
|
300
|
+
threadHeader: string | null;
|
|
301
|
+
clientRequestHeader: string | null;
|
|
302
|
+
installationId?: string;
|
|
303
|
+
windowId?: string;
|
|
304
|
+
routingHint: string | null;
|
|
305
|
+
accountId: string;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
type WebSocketFallbackSession = {
|
|
309
|
+
cacheIdentity: CacheIdentitySnapshot;
|
|
310
|
+
};
|
|
311
|
+
|
|
129
312
|
const websocketSessions = new Map<string, Map<string, CachedWebSocket>>();
|
|
130
|
-
const websocketFallbackSessions = new
|
|
313
|
+
const websocketFallbackSessions = new Map<string, WebSocketFallbackSession>();
|
|
131
314
|
const websocketDebugStats = new Map<string, OpenAICodexWebSocketDebugStats>();
|
|
132
315
|
|
|
133
316
|
function getOrCreateWebSocketDebugStats(sessionId: string): OpenAICodexWebSocketDebugStats {
|
|
@@ -144,6 +327,7 @@ function getOrCreateWebSocketDebugStats(sessionId: string): OpenAICodexWebSocket
|
|
|
144
327
|
lastInputItems: 0,
|
|
145
328
|
websocketFailures: 0,
|
|
146
329
|
sseFallbacks: 0,
|
|
330
|
+
prewarmRequests: 0,
|
|
147
331
|
};
|
|
148
332
|
websocketDebugStats.set(sessionId, stats);
|
|
149
333
|
}
|
|
@@ -169,7 +353,6 @@ export function resetOpenAICodexWebSocketDebugStats(sessionId?: string): void {
|
|
|
169
353
|
|
|
170
354
|
export function closeOpenAICodexWebSocketSessions(sessionId?: string): void {
|
|
171
355
|
const closeEntry = (entry: CachedWebSocket): void => {
|
|
172
|
-
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
173
356
|
closeSocket(entry.socket, "debug_close");
|
|
174
357
|
};
|
|
175
358
|
if (sessionId) {
|
|
@@ -194,6 +377,12 @@ function isWebSocketSseFallbackActive(sessionId: string | undefined): boolean {
|
|
|
194
377
|
return sessionId ? websocketFallbackSessions.has(sessionId) : false;
|
|
195
378
|
}
|
|
196
379
|
|
|
380
|
+
function webSocketFallbackSession(
|
|
381
|
+
sessionId: string | undefined,
|
|
382
|
+
): WebSocketFallbackSession | undefined {
|
|
383
|
+
return sessionId ? websocketFallbackSessions.get(sessionId) : undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
197
386
|
function recordWebSocketSseFallback(sessionId: string | undefined): void {
|
|
198
387
|
if (!sessionId) return;
|
|
199
388
|
const stats = getOrCreateWebSocketDebugStats(sessionId);
|
|
@@ -201,9 +390,13 @@ function recordWebSocketSseFallback(sessionId: string | undefined): void {
|
|
|
201
390
|
stats.websocketFallbackActive = isWebSocketSseFallbackActive(sessionId);
|
|
202
391
|
}
|
|
203
392
|
|
|
204
|
-
function recordWebSocketFailure(
|
|
393
|
+
function recordWebSocketFailure(
|
|
394
|
+
sessionId: string | undefined,
|
|
395
|
+
error: unknown,
|
|
396
|
+
cacheIdentity: CacheIdentitySnapshot,
|
|
397
|
+
): void {
|
|
205
398
|
if (!sessionId) return;
|
|
206
|
-
websocketFallbackSessions.
|
|
399
|
+
websocketFallbackSessions.set(sessionId, { cacheIdentity });
|
|
207
400
|
const stats = getOrCreateWebSocketDebugStats(sessionId);
|
|
208
401
|
stats.websocketFailures += 1;
|
|
209
402
|
stats.lastWebSocketError = thrownMessage(error);
|
|
@@ -232,6 +425,16 @@ class CodexProtocolError extends Error {
|
|
|
232
425
|
}
|
|
233
426
|
}
|
|
234
427
|
|
|
428
|
+
class CodexHttpError extends Error {
|
|
429
|
+
readonly retryable: boolean;
|
|
430
|
+
|
|
431
|
+
constructor(message: string, retryable: boolean) {
|
|
432
|
+
super(message);
|
|
433
|
+
this.name = "CodexHttpError";
|
|
434
|
+
this.retryable = retryable;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
235
438
|
class WebSocketCloseError extends Error {
|
|
236
439
|
readonly code: number | undefined;
|
|
237
440
|
readonly reason: string | undefined;
|
|
@@ -309,7 +512,11 @@ function thrownMessage(error: unknown): string {
|
|
|
309
512
|
}
|
|
310
513
|
|
|
311
514
|
function isCodexNonTransportError(error: unknown): boolean {
|
|
312
|
-
return
|
|
515
|
+
return (
|
|
516
|
+
error instanceof CodexApiError ||
|
|
517
|
+
error instanceof CodexHttpError ||
|
|
518
|
+
error instanceof CodexProtocolError
|
|
519
|
+
);
|
|
313
520
|
}
|
|
314
521
|
|
|
315
522
|
function isWebSocketConnectionLimitReachedError(error: unknown): boolean {
|
|
@@ -320,7 +527,7 @@ function isPreviousResponseNotFoundError(error: unknown): boolean {
|
|
|
320
527
|
return error instanceof CodexApiError && error.code === PREVIOUS_RESPONSE_NOT_FOUND_CODE;
|
|
321
528
|
}
|
|
322
529
|
|
|
323
|
-
function diagnosticError(error: unknown):
|
|
530
|
+
function diagnosticError(error: unknown): CodexTransportFailureDiagnostic["error"] {
|
|
324
531
|
if (!(error instanceof Error)) {
|
|
325
532
|
return { name: "ThrownValue", message: thrownMessage(error) };
|
|
326
533
|
}
|
|
@@ -338,14 +545,15 @@ function transportDiagnostic(
|
|
|
338
545
|
transport: string,
|
|
339
546
|
emitted: boolean,
|
|
340
547
|
requestBytes: number,
|
|
341
|
-
|
|
548
|
+
fallbackSelected = !emitted,
|
|
549
|
+
): CodexTransportFailureDiagnostic {
|
|
342
550
|
return {
|
|
343
551
|
type: "provider_transport_failure",
|
|
344
552
|
timestamp: Date.now(),
|
|
345
553
|
error: diagnosticError(error),
|
|
346
554
|
details: {
|
|
347
555
|
configuredTransport: transport,
|
|
348
|
-
...(
|
|
556
|
+
...(fallbackSelected ? { fallbackTransport: "sse" as const } : {}),
|
|
349
557
|
eventsEmitted: emitted,
|
|
350
558
|
phase: emitted ? "after_message_stream_start" : "before_message_stream_start",
|
|
351
559
|
requestBytes,
|
|
@@ -371,6 +579,13 @@ function sleep(milliseconds: number, signal?: AbortSignal): Promise<void> {
|
|
|
371
579
|
});
|
|
372
580
|
}
|
|
373
581
|
|
|
582
|
+
function retryBackoffMs(baseDelayMs: number, attempt: number): number {
|
|
583
|
+
if (baseDelayMs <= 0) return 0;
|
|
584
|
+
const exponential = baseDelayMs * 2 ** Math.max(0, attempt - 1);
|
|
585
|
+
const jitter = 0.9 + Math.random() * 0.2;
|
|
586
|
+
return Math.floor(exponential * jitter);
|
|
587
|
+
}
|
|
588
|
+
|
|
374
589
|
function normalizeTimeoutMs(value: number | undefined): number | undefined {
|
|
375
590
|
if (value === undefined) return undefined;
|
|
376
591
|
if (!Number.isFinite(value) || value < 0) {
|
|
@@ -379,8 +594,16 @@ function normalizeTimeoutMs(value: number | undefined): number | undefined {
|
|
|
379
594
|
return Math.floor(value);
|
|
380
595
|
}
|
|
381
596
|
|
|
597
|
+
function normalizeRetryCount(value: number | undefined, fallback: number, name: string): number {
|
|
598
|
+
const resolved = value ?? fallback;
|
|
599
|
+
if (!Number.isFinite(resolved) || resolved < 0) {
|
|
600
|
+
throw new Error(`Invalid ${name}: ${String(resolved)}`);
|
|
601
|
+
}
|
|
602
|
+
return Math.floor(resolved);
|
|
603
|
+
}
|
|
604
|
+
|
|
382
605
|
function isTerminalRateLimitError(errorText: string): boolean {
|
|
383
|
-
return /GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(
|
|
606
|
+
return /GoUsageLimitError|FreeUsageLimitError|usage_limit_reached|usage_not_included|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(
|
|
384
607
|
errorText,
|
|
385
608
|
);
|
|
386
609
|
}
|
|
@@ -401,6 +624,7 @@ function retryDelayMs(headers: Headers): number | undefined {
|
|
|
401
624
|
}
|
|
402
625
|
|
|
403
626
|
class RetryDelayExceededError extends Error {}
|
|
627
|
+
class SseStreamIncompleteError extends Error {}
|
|
404
628
|
|
|
405
629
|
function validateRetryDelay(delayMs: number, options: CodexTransportOptions): number {
|
|
406
630
|
const maximum = options.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
|
|
@@ -412,9 +636,10 @@ function validateRetryDelay(delayMs: number, options: CodexTransportOptions): nu
|
|
|
412
636
|
return delayMs;
|
|
413
637
|
}
|
|
414
638
|
|
|
415
|
-
function codexHttpError(status: number, statusText: string, raw: string):
|
|
639
|
+
function codexHttpError(status: number, statusText: string, raw: string): CodexHttpError {
|
|
416
640
|
let message = raw || statusText || "Request failed";
|
|
417
641
|
let friendlyMessage: string | undefined;
|
|
642
|
+
const retryable = isRetryable(status, raw);
|
|
418
643
|
try {
|
|
419
644
|
const parsed = JSON.parse(raw) as {
|
|
420
645
|
error?: {
|
|
@@ -426,7 +651,7 @@ function codexHttpError(status: number, statusText: string, raw: string): Error
|
|
|
426
651
|
};
|
|
427
652
|
};
|
|
428
653
|
const error = parsed?.error;
|
|
429
|
-
if (!error) return new
|
|
654
|
+
if (!error) return new CodexHttpError(message, retryable);
|
|
430
655
|
const code = error.code || error.type || "";
|
|
431
656
|
if (
|
|
432
657
|
status === 429 ||
|
|
@@ -441,7 +666,7 @@ function codexHttpError(status: number, statusText: string, raw: string): Error
|
|
|
441
666
|
}
|
|
442
667
|
message = error.message || friendlyMessage || message;
|
|
443
668
|
} catch {}
|
|
444
|
-
return new
|
|
669
|
+
return new CodexHttpError(friendlyMessage || message, retryable);
|
|
445
670
|
}
|
|
446
671
|
|
|
447
672
|
function combineAbortSignals(signals: readonly (AbortSignal | undefined)[]): {
|
|
@@ -476,10 +701,280 @@ function combineAbortSignals(signals: readonly (AbortSignal | undefined)[]): {
|
|
|
476
701
|
};
|
|
477
702
|
}
|
|
478
703
|
|
|
704
|
+
function webSocketRecoveryAttempt(
|
|
705
|
+
attempt: CodexWebSocketAttempt,
|
|
706
|
+
outcome: CodexTransportRecoveryAttempt["outcome"],
|
|
707
|
+
): CodexTransportRecoveryAttempt {
|
|
708
|
+
return {
|
|
709
|
+
transport: "websocket",
|
|
710
|
+
connection: attempt.connection,
|
|
711
|
+
contextMode: attempt.contextMode,
|
|
712
|
+
inputItems: attempt.inputItems,
|
|
713
|
+
fullInputItems: attempt.fullInputItems,
|
|
714
|
+
fullRequestBytes: attempt.fullRequestBytes,
|
|
715
|
+
wireRequestBytes: attempt.wireRequestBytes,
|
|
716
|
+
outcome,
|
|
717
|
+
...(attempt.turnStateReplayed === undefined
|
|
718
|
+
? {}
|
|
719
|
+
: { turnStateReplayed: attempt.turnStateReplayed }),
|
|
720
|
+
...(attempt.turnStateReplayedValue
|
|
721
|
+
? { turnStateReplayedValue: attempt.turnStateReplayedValue }
|
|
722
|
+
: {}),
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function sseRecoveryAttempt(
|
|
727
|
+
body: JsonRecord,
|
|
728
|
+
requestBytes: number,
|
|
729
|
+
turnStateReplayedValue: string | undefined,
|
|
730
|
+
): CodexTransportRecoveryAttempt {
|
|
731
|
+
const inputItems = requestInputLength(body);
|
|
732
|
+
return {
|
|
733
|
+
transport: "sse",
|
|
734
|
+
contextMode: "full",
|
|
735
|
+
inputItems,
|
|
736
|
+
fullInputItems: inputItems,
|
|
737
|
+
fullRequestBytes: requestBytes,
|
|
738
|
+
wireRequestBytes: requestBytes,
|
|
739
|
+
outcome: "selected",
|
|
740
|
+
turnStateReplayed: turnStateReplayedValue !== undefined,
|
|
741
|
+
...(turnStateReplayedValue ? { turnStateReplayedValue } : {}),
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function transportRecoveryDiagnostic(options: {
|
|
746
|
+
trigger: CodexTransportRecoveryDiagnostic["details"]["trigger"];
|
|
747
|
+
configuredTransport: string;
|
|
748
|
+
attempts: CodexTransportRecoveryAttempt[];
|
|
749
|
+
cacheIdentity: CacheIdentitySnapshot;
|
|
750
|
+
previousResponseId?: string;
|
|
751
|
+
continuationBypassReason?: CodexContinuationBypassReason;
|
|
752
|
+
historyMismatch?: CodexContinuationHistoryMismatch;
|
|
753
|
+
error?: unknown;
|
|
754
|
+
previousCacheIdentity?: CacheIdentitySnapshot;
|
|
755
|
+
cacheIdentityPreserved?: boolean;
|
|
756
|
+
accountIdentityPreserved?: boolean;
|
|
757
|
+
retryNumber?: number;
|
|
758
|
+
maxRetries?: number;
|
|
759
|
+
}): CodexTransportRecoveryDiagnostic {
|
|
760
|
+
return {
|
|
761
|
+
type: "codex_transport_recovery",
|
|
762
|
+
timestamp: Date.now(),
|
|
763
|
+
details: {
|
|
764
|
+
trigger: options.trigger,
|
|
765
|
+
configuredTransport: options.configuredTransport,
|
|
766
|
+
...(options.previousResponseId ? { previousResponseId: options.previousResponseId } : {}),
|
|
767
|
+
...(options.continuationBypassReason
|
|
768
|
+
? { continuationBypassReason: options.continuationBypassReason }
|
|
769
|
+
: {}),
|
|
770
|
+
...(options.historyMismatch ? { historyMismatch: options.historyMismatch } : {}),
|
|
771
|
+
...(options.error === undefined ? {} : { error: diagnosticError(options.error) }),
|
|
772
|
+
attempts: options.attempts,
|
|
773
|
+
cacheIdentity: options.cacheIdentity,
|
|
774
|
+
...(options.previousCacheIdentity
|
|
775
|
+
? { previousCacheIdentity: options.previousCacheIdentity }
|
|
776
|
+
: {}),
|
|
777
|
+
cacheAffinityEnabled: cacheAffinityEnabled(options.cacheIdentity),
|
|
778
|
+
...(options.cacheIdentityPreserved !== undefined
|
|
779
|
+
? { cacheIdentityPreserved: options.cacheIdentityPreserved }
|
|
780
|
+
: options.previousCacheIdentity
|
|
781
|
+
? {
|
|
782
|
+
cacheIdentityPreserved: sameCacheIdentity(
|
|
783
|
+
options.previousCacheIdentity,
|
|
784
|
+
options.cacheIdentity,
|
|
785
|
+
),
|
|
786
|
+
}
|
|
787
|
+
: {}),
|
|
788
|
+
...(options.accountIdentityPreserved !== undefined
|
|
789
|
+
? { accountIdentityPreserved: options.accountIdentityPreserved }
|
|
790
|
+
: options.previousCacheIdentity
|
|
791
|
+
? {
|
|
792
|
+
accountIdentityPreserved:
|
|
793
|
+
options.previousCacheIdentity.accountId === options.cacheIdentity.accountId,
|
|
794
|
+
}
|
|
795
|
+
: {}),
|
|
796
|
+
promptKeyAndHeaderAligned: promptKeyAndHeaderAligned(options.cacheIdentity),
|
|
797
|
+
...(options.retryNumber === undefined ? {} : { retryNumber: options.retryNumber }),
|
|
798
|
+
...(options.maxRetries === undefined ? {} : { maxRetries: options.maxRetries }),
|
|
799
|
+
},
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function cacheUsageDiagnostic(event: JsonRecord): CodexCacheUsageDiagnostic | undefined {
|
|
804
|
+
if (!isTerminalEvent(event) || !isObject(event.response) || !isObject(event.response.usage)) {
|
|
805
|
+
return undefined;
|
|
806
|
+
}
|
|
807
|
+
const usage = event.response.usage;
|
|
808
|
+
const details = isObject(usage.input_tokens_details) ? usage.input_tokens_details : undefined;
|
|
809
|
+
return {
|
|
810
|
+
inputTokens: typeof usage.input_tokens === "number" ? usage.input_tokens : 0,
|
|
811
|
+
cachedTokens: typeof details?.cached_tokens === "number" ? details.cached_tokens : 0,
|
|
812
|
+
cacheWriteTokens:
|
|
813
|
+
typeof details?.cache_write_tokens === "number" ? details.cache_write_tokens : 0,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function transportRequestDiagnostic(options: {
|
|
818
|
+
requestOptions: CodexTransportOptions;
|
|
819
|
+
body: JsonRecord;
|
|
820
|
+
configuredTransport: string;
|
|
821
|
+
selectedTransport: "websocket" | "sse";
|
|
822
|
+
attempt:
|
|
823
|
+
| CodexWebSocketAttempt
|
|
824
|
+
| {
|
|
825
|
+
contextMode: "full";
|
|
826
|
+
inputItems: number;
|
|
827
|
+
fullInputItems: number;
|
|
828
|
+
fullRequestBytes: number;
|
|
829
|
+
wireRequestBytes: number;
|
|
830
|
+
turnStateReplayed: boolean;
|
|
831
|
+
turnStateReplayedValue?: string;
|
|
832
|
+
};
|
|
833
|
+
cacheIdentity: CacheIdentitySnapshot;
|
|
834
|
+
turnStateAvailableAtStart: boolean;
|
|
835
|
+
turnStateRevisionAtStart: number;
|
|
836
|
+
turnStateValueAtStart?: string;
|
|
837
|
+
responseId?: string;
|
|
838
|
+
usage?: CodexCacheUsageDiagnostic;
|
|
839
|
+
}): CodexTransportRequestDiagnostic {
|
|
840
|
+
const metadata = isObject(options.body.client_metadata)
|
|
841
|
+
? options.body.client_metadata
|
|
842
|
+
: undefined;
|
|
843
|
+
const turnStateReceived =
|
|
844
|
+
(options.requestOptions.turnState?.revision ?? options.turnStateRevisionAtStart) >
|
|
845
|
+
options.turnStateRevisionAtStart;
|
|
846
|
+
const currentTurnStateValue = options.requestOptions.turnState?.replayValue();
|
|
847
|
+
const routingHint = codexRoutingHint(options.body);
|
|
848
|
+
return {
|
|
849
|
+
type: "codex_transport_request",
|
|
850
|
+
timestamp: Date.now(),
|
|
851
|
+
details: {
|
|
852
|
+
requestKind: options.requestOptions.requestKind ?? "turn",
|
|
853
|
+
configuredTransport: options.configuredTransport,
|
|
854
|
+
selectedTransport: options.selectedTransport,
|
|
855
|
+
...("connection" in options.attempt ? { connection: options.attempt.connection } : {}),
|
|
856
|
+
contextMode: options.attempt.contextMode,
|
|
857
|
+
inputItems: options.attempt.inputItems,
|
|
858
|
+
fullInputItems: options.attempt.fullInputItems,
|
|
859
|
+
fullRequestBytes: options.attempt.fullRequestBytes,
|
|
860
|
+
wireRequestBytes: options.attempt.wireRequestBytes,
|
|
861
|
+
cacheAffinityEnabled: cacheAffinityEnabled(options.cacheIdentity),
|
|
862
|
+
promptKeyAndHeaderAligned: promptKeyAndHeaderAligned(options.cacheIdentity),
|
|
863
|
+
cacheIdentity: options.cacheIdentity,
|
|
864
|
+
...(options.requestOptions.sessionId ? { sessionId: options.requestOptions.sessionId } : {}),
|
|
865
|
+
...(typeof options.body.prompt_cache_key === "string"
|
|
866
|
+
? { promptCacheKey: options.body.prompt_cache_key }
|
|
867
|
+
: {}),
|
|
868
|
+
accountId: options.cacheIdentity.accountId,
|
|
869
|
+
...(typeof metadata?.["session_id"] === "string"
|
|
870
|
+
? { clientSessionId: metadata["session_id"] }
|
|
871
|
+
: {}),
|
|
872
|
+
...(typeof metadata?.["thread_id"] === "string" ? { threadId: metadata["thread_id"] } : {}),
|
|
873
|
+
...(typeof metadata?.["turn_id"] === "string" ? { turnId: metadata["turn_id"] } : {}),
|
|
874
|
+
...(typeof metadata?.[CODEX_INSTALLATION_ID_METADATA_KEY] === "string"
|
|
875
|
+
? { installationId: metadata[CODEX_INSTALLATION_ID_METADATA_KEY] }
|
|
876
|
+
: {}),
|
|
877
|
+
...(typeof metadata?.[CODEX_WINDOW_ID_HEADER] === "string"
|
|
878
|
+
? { windowId: metadata[CODEX_WINDOW_ID_HEADER] }
|
|
879
|
+
: {}),
|
|
880
|
+
...(routingHint ? { routingHint } : {}),
|
|
881
|
+
...(typeof metadata?.["x-codex-turn-metadata"] === "string"
|
|
882
|
+
? { turnMetadata: metadata["x-codex-turn-metadata"] }
|
|
883
|
+
: {}),
|
|
884
|
+
...(options.responseId ? { responseId: options.responseId } : {}),
|
|
885
|
+
...("previousResponseId" in options.attempt && options.attempt.previousResponseId
|
|
886
|
+
? { previousResponseId: options.attempt.previousResponseId }
|
|
887
|
+
: {}),
|
|
888
|
+
turnStateAvailableAtStart: options.turnStateAvailableAtStart,
|
|
889
|
+
turnStateReplayed: options.attempt.turnStateReplayed ?? false,
|
|
890
|
+
turnStateReceived,
|
|
891
|
+
...(options.turnStateValueAtStart ? { turnStateAtStart: options.turnStateValueAtStart } : {}),
|
|
892
|
+
...(options.attempt.turnStateReplayedValue
|
|
893
|
+
? { turnStateReplayedValue: options.attempt.turnStateReplayedValue }
|
|
894
|
+
: {}),
|
|
895
|
+
...(turnStateReceived && currentTurnStateValue
|
|
896
|
+
? { turnStateReceivedValue: currentTurnStateValue }
|
|
897
|
+
: {}),
|
|
898
|
+
...(options.usage ? { usage: options.usage } : {}),
|
|
899
|
+
...(options.requestOptions.cacheDiagnostics
|
|
900
|
+
? { cache: options.requestOptions.cacheDiagnostics }
|
|
901
|
+
: {}),
|
|
902
|
+
},
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function shouldReportRequestDiagnostic(options: CodexTransportOptions): boolean {
|
|
907
|
+
return options.cacheDiagnostics !== undefined || options.turnState !== undefined;
|
|
908
|
+
}
|
|
909
|
+
|
|
479
910
|
function headersToRecord(headers: Headers): Record<string, string> {
|
|
480
911
|
return Object.fromEntries(headers.entries());
|
|
481
912
|
}
|
|
482
913
|
|
|
914
|
+
function applyTurnStateHeader(
|
|
915
|
+
headers: Headers,
|
|
916
|
+
turnState: CodexTurnState | undefined,
|
|
917
|
+
): string | undefined {
|
|
918
|
+
if (!turnState) return undefined;
|
|
919
|
+
const value = turnState?.replayValue();
|
|
920
|
+
if (!value) {
|
|
921
|
+
headers.delete(CODEX_TURN_STATE_HEADER);
|
|
922
|
+
return undefined;
|
|
923
|
+
}
|
|
924
|
+
headers.set(CODEX_TURN_STATE_HEADER, value);
|
|
925
|
+
return value;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function codexRoutingHint(body: JsonRecord): string | undefined {
|
|
929
|
+
if (typeof body.model !== "string" || body.model.length === 0) return undefined;
|
|
930
|
+
const tier =
|
|
931
|
+
typeof body.service_tier === "string" && body.service_tier.length > 0
|
|
932
|
+
? `;tier=${body.service_tier}`
|
|
933
|
+
: "";
|
|
934
|
+
return `model=${body.model}${tier}`;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function applyCodexRoutingHint(headers: Headers, body: JsonRecord): void {
|
|
938
|
+
const hint = codexRoutingHint(body);
|
|
939
|
+
if (!hint) {
|
|
940
|
+
headers.delete(CODEX_ROUTING_HINT_HEADER);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
headers.set(CODEX_ROUTING_HINT_HEADER, hint);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function captureTurnStateHeader(headers: Headers, turnState: CodexTurnState | undefined): boolean {
|
|
947
|
+
return turnState?.capture(headers.get(CODEX_TURN_STATE_HEADER) ?? undefined) ?? false;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function captureTurnStateEvent(event: JsonRecord, turnState: CodexTurnState | undefined): boolean {
|
|
951
|
+
if (event.type !== "response.metadata" || !isObject(event["headers"])) return false;
|
|
952
|
+
for (const [name, value] of Object.entries(event["headers"])) {
|
|
953
|
+
if (name.toLowerCase() === CODEX_TURN_STATE_HEADER && typeof value === "string") {
|
|
954
|
+
return turnState?.capture(value) ?? false;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function withTurnStateMetadata(
|
|
961
|
+
body: JsonRecord,
|
|
962
|
+
turnState: CodexTurnState | undefined,
|
|
963
|
+
): { body: JsonRecord; replayedValue?: string } {
|
|
964
|
+
const value = turnState?.replayValue();
|
|
965
|
+
if (!value) return { body };
|
|
966
|
+
return {
|
|
967
|
+
body: {
|
|
968
|
+
...body,
|
|
969
|
+
client_metadata: {
|
|
970
|
+
...(isObject(body.client_metadata) ? body.client_metadata : {}),
|
|
971
|
+
[CODEX_TURN_STATE_HEADER]: value,
|
|
972
|
+
},
|
|
973
|
+
},
|
|
974
|
+
replayedValue: value,
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
483
978
|
function extractAccountId(token: string): string {
|
|
484
979
|
try {
|
|
485
980
|
const parts = token.split(".");
|
|
@@ -553,15 +1048,25 @@ function sseHeaders(
|
|
|
553
1048
|
accountId: string,
|
|
554
1049
|
token: string,
|
|
555
1050
|
sessionId: string | undefined,
|
|
1051
|
+
body: JsonRecord,
|
|
556
1052
|
): Headers {
|
|
557
1053
|
const headers = baseHeaders(modelHeaders, additionalHeaders, accountId, token);
|
|
558
|
-
headers.
|
|
1054
|
+
headers.delete("OpenAI-Beta");
|
|
1055
|
+
headers.delete("openai-beta");
|
|
559
1056
|
headers.set("accept", "text/event-stream");
|
|
560
1057
|
headers.set("content-type", "application/json");
|
|
561
1058
|
if (sessionId) {
|
|
562
1059
|
headers.set("session-id", sessionId);
|
|
563
|
-
headers.set("x-client-request-id", sessionId);
|
|
564
1060
|
}
|
|
1061
|
+
applyCodexMetadataHeaders(headers, body);
|
|
1062
|
+
if (sessionId && !headers.has("thread-id")) headers.set("thread-id", sessionId);
|
|
1063
|
+
const threadId = headers.get("thread-id");
|
|
1064
|
+
if (threadId) {
|
|
1065
|
+
// Official Responses HTTP uses the thread identity as x-client-request-id.
|
|
1066
|
+
headers.set("x-client-request-id", threadId);
|
|
1067
|
+
}
|
|
1068
|
+
applyResponsesLiteHeaders(headers, body);
|
|
1069
|
+
applyCodexRoutingHint(headers, body);
|
|
565
1070
|
return headers;
|
|
566
1071
|
}
|
|
567
1072
|
|
|
@@ -587,6 +1092,7 @@ function websocketHeaders(
|
|
|
587
1092
|
accountId: string,
|
|
588
1093
|
token: string,
|
|
589
1094
|
requestId: string,
|
|
1095
|
+
body: JsonRecord,
|
|
590
1096
|
): Headers {
|
|
591
1097
|
const headers = baseHeaders(modelHeaders, additionalHeaders, accountId, token);
|
|
592
1098
|
headers.delete("accept");
|
|
@@ -596,9 +1102,65 @@ function websocketHeaders(
|
|
|
596
1102
|
headers.set("OpenAI-Beta", OPENAI_BETA_RESPONSES_WEBSOCKETS);
|
|
597
1103
|
headers.set("x-client-request-id", requestId);
|
|
598
1104
|
headers.set("session-id", requestId);
|
|
1105
|
+
applyCodexMetadataHeaders(headers, body);
|
|
1106
|
+
if (!headers.has("thread-id")) headers.set("thread-id", requestId);
|
|
1107
|
+
const threadId = headers.get("thread-id");
|
|
1108
|
+
if (threadId) headers.set("x-client-request-id", threadId);
|
|
1109
|
+
applyCodexRoutingHint(headers, body);
|
|
599
1110
|
return headers;
|
|
600
1111
|
}
|
|
601
1112
|
|
|
1113
|
+
function cacheIdentitySnapshot(
|
|
1114
|
+
body: JsonRecord,
|
|
1115
|
+
headers: Headers,
|
|
1116
|
+
accountId: string,
|
|
1117
|
+
): CacheIdentitySnapshot {
|
|
1118
|
+
const metadata = isObject(body.client_metadata) ? body.client_metadata : undefined;
|
|
1119
|
+
return {
|
|
1120
|
+
promptCacheKey: typeof body.prompt_cache_key === "string" ? body.prompt_cache_key : undefined,
|
|
1121
|
+
sessionHeader: headers.get("session-id"),
|
|
1122
|
+
threadHeader: headers.get("thread-id"),
|
|
1123
|
+
clientRequestHeader: headers.get("x-client-request-id"),
|
|
1124
|
+
...(typeof metadata?.[CODEX_INSTALLATION_ID_METADATA_KEY] === "string"
|
|
1125
|
+
? { installationId: metadata[CODEX_INSTALLATION_ID_METADATA_KEY] }
|
|
1126
|
+
: {}),
|
|
1127
|
+
...(typeof metadata?.[CODEX_WINDOW_ID_HEADER] === "string"
|
|
1128
|
+
? { windowId: metadata[CODEX_WINDOW_ID_HEADER] }
|
|
1129
|
+
: {}),
|
|
1130
|
+
routingHint: headers.get(CODEX_ROUTING_HINT_HEADER),
|
|
1131
|
+
accountId,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function cacheAffinityEnabled(identity: CacheIdentitySnapshot): boolean {
|
|
1136
|
+
return Boolean(identity.promptCacheKey || identity.sessionHeader || identity.clientRequestHeader);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function promptKeyAndHeaderAligned(identity: CacheIdentitySnapshot): boolean {
|
|
1140
|
+
return Boolean(
|
|
1141
|
+
identity.promptCacheKey &&
|
|
1142
|
+
identity.promptCacheKey === identity.sessionHeader &&
|
|
1143
|
+
identity.promptCacheKey === identity.threadHeader &&
|
|
1144
|
+
identity.promptCacheKey === identity.clientRequestHeader,
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function sameCacheIdentity(a: CacheIdentitySnapshot, b: CacheIdentitySnapshot): boolean {
|
|
1149
|
+
return (
|
|
1150
|
+
a.promptCacheKey === b.promptCacheKey &&
|
|
1151
|
+
a.sessionHeader === b.sessionHeader &&
|
|
1152
|
+
a.threadHeader === b.threadHeader &&
|
|
1153
|
+
a.clientRequestHeader === b.clientRequestHeader &&
|
|
1154
|
+
a.installationId === b.installationId &&
|
|
1155
|
+
a.windowId === b.windowId &&
|
|
1156
|
+
a.routingHint === b.routingHint
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function serializedBytes(value: string): number {
|
|
1161
|
+
return new TextEncoder().encode(value).byteLength;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
602
1164
|
function compressBody(body: string): Uint8Array | undefined {
|
|
603
1165
|
const zlib = nodeZlib();
|
|
604
1166
|
if (!zlib || typeof zlib.zstdCompressSync !== "function") return undefined;
|
|
@@ -688,17 +1250,6 @@ function socketReusable(socket: WebSocketLike): boolean {
|
|
|
688
1250
|
return socket.readyState === undefined || socket.readyState === 1;
|
|
689
1251
|
}
|
|
690
1252
|
|
|
691
|
-
function scheduleSocketExpiry(sessionId: string, accountId: string, entry: CachedWebSocket): void {
|
|
692
|
-
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
693
|
-
entry.idleTimer = setTimeout(() => {
|
|
694
|
-
if (entry.busy) return;
|
|
695
|
-
closeSocket(entry.socket, "idle_timeout");
|
|
696
|
-
const accountEntries = websocketSessions.get(sessionId);
|
|
697
|
-
if (accountEntries?.get(accountId) === entry) accountEntries.delete(accountId);
|
|
698
|
-
if (accountEntries?.size === 0) websocketSessions.delete(sessionId);
|
|
699
|
-
}, SESSION_WEBSOCKET_CACHE_TTL_MS);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
1253
|
async function connectWebSocket(
|
|
703
1254
|
url: string,
|
|
704
1255
|
headers: Headers,
|
|
@@ -779,12 +1330,7 @@ async function acquireWebSocket(
|
|
|
779
1330
|
if (sessionId) {
|
|
780
1331
|
let accountEntries = websocketSessions.get(sessionId);
|
|
781
1332
|
const cached = accountEntries?.get(accountId);
|
|
782
|
-
if (cached
|
|
783
|
-
clearTimeout(cached.idleTimer);
|
|
784
|
-
delete cached.idleTimer;
|
|
785
|
-
}
|
|
786
|
-
const expired = cached && Date.now() - cached.createdAt >= SESSION_WEBSOCKET_MAX_AGE_MS;
|
|
787
|
-
if (cached && !cached.busy && !expired && socketReusable(cached.socket)) {
|
|
1333
|
+
if (cached && !cached.busy && socketReusable(cached.socket)) {
|
|
788
1334
|
cached.busy = true;
|
|
789
1335
|
return {
|
|
790
1336
|
socket: cached.socket,
|
|
@@ -792,10 +1338,6 @@ async function acquireWebSocket(
|
|
|
792
1338
|
reused: true,
|
|
793
1339
|
release(keep) {
|
|
794
1340
|
if (!keep || !socketReusable(cached.socket)) {
|
|
795
|
-
if (cached.idleTimer) {
|
|
796
|
-
clearTimeout(cached.idleTimer);
|
|
797
|
-
delete cached.idleTimer;
|
|
798
|
-
}
|
|
799
1341
|
closeSocket(cached.socket);
|
|
800
1342
|
const currentEntries = websocketSessions.get(sessionId);
|
|
801
1343
|
if (currentEntries?.get(accountId) === cached) currentEntries.delete(accountId);
|
|
@@ -803,12 +1345,11 @@ async function acquireWebSocket(
|
|
|
803
1345
|
return;
|
|
804
1346
|
}
|
|
805
1347
|
cached.busy = false;
|
|
806
|
-
scheduleSocketExpiry(sessionId, accountId, cached);
|
|
807
1348
|
},
|
|
808
1349
|
};
|
|
809
1350
|
}
|
|
810
1351
|
if (cached && !cached.busy) {
|
|
811
|
-
closeSocket(cached.socket,
|
|
1352
|
+
closeSocket(cached.socket, "done");
|
|
812
1353
|
accountEntries?.delete(accountId);
|
|
813
1354
|
if (accountEntries?.size === 0) websocketSessions.delete(sessionId);
|
|
814
1355
|
}
|
|
@@ -818,7 +1359,7 @@ async function acquireWebSocket(
|
|
|
818
1359
|
}
|
|
819
1360
|
|
|
820
1361
|
const socket = await connectWebSocket(url, headers, signal, timeoutMs);
|
|
821
|
-
const entry: CachedWebSocket = { socket, busy: true
|
|
1362
|
+
const entry: CachedWebSocket = { socket, busy: true };
|
|
822
1363
|
accountEntries = websocketSessions.get(sessionId);
|
|
823
1364
|
if (!accountEntries) {
|
|
824
1365
|
accountEntries = new Map();
|
|
@@ -832,14 +1373,12 @@ async function acquireWebSocket(
|
|
|
832
1373
|
release(keep) {
|
|
833
1374
|
if (!keep || !socketReusable(socket)) {
|
|
834
1375
|
closeSocket(socket);
|
|
835
|
-
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
836
1376
|
const currentEntries = websocketSessions.get(sessionId);
|
|
837
1377
|
if (currentEntries?.get(accountId) === entry) currentEntries.delete(accountId);
|
|
838
1378
|
if (currentEntries?.size === 0) websocketSessions.delete(sessionId);
|
|
839
1379
|
return;
|
|
840
1380
|
}
|
|
841
1381
|
entry.busy = false;
|
|
842
|
-
scheduleSocketExpiry(sessionId, accountId, entry);
|
|
843
1382
|
},
|
|
844
1383
|
};
|
|
845
1384
|
}
|
|
@@ -852,9 +1391,24 @@ function requestWithoutHistory(body: JsonRecord): JsonRecord {
|
|
|
852
1391
|
const result = { ...body };
|
|
853
1392
|
delete result.input;
|
|
854
1393
|
delete result.previous_response_id;
|
|
1394
|
+
delete result.client_metadata;
|
|
1395
|
+
// This controls response delivery only, not the model context retained by
|
|
1396
|
+
// previous_response_id.
|
|
1397
|
+
delete result["stream_options"];
|
|
855
1398
|
return result;
|
|
856
1399
|
}
|
|
857
1400
|
|
|
1401
|
+
function responseItemsMatch(previous: unknown, current: unknown): boolean {
|
|
1402
|
+
if (!isObject(previous) || !isObject(current)) {
|
|
1403
|
+
return stableResponsesJson(previous) === stableResponsesJson(current);
|
|
1404
|
+
}
|
|
1405
|
+
const previousComparable = structuredClone(previous);
|
|
1406
|
+
const currentComparable = structuredClone(current);
|
|
1407
|
+
delete previousComparable["internal_chat_message_metadata_passthrough"];
|
|
1408
|
+
delete currentComparable["internal_chat_message_metadata_passthrough"];
|
|
1409
|
+
return stableResponsesJson(previousComparable) === stableResponsesJson(currentComparable);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
858
1412
|
function jsonWireRequestBody(body: JsonRecord): JsonRecord {
|
|
859
1413
|
const snapshot = JSON.parse(JSON.stringify(body)) as unknown;
|
|
860
1414
|
if (!isObject(snapshot)) {
|
|
@@ -863,36 +1417,95 @@ function jsonWireRequestBody(body: JsonRecord): JsonRecord {
|
|
|
863
1417
|
return snapshot;
|
|
864
1418
|
}
|
|
865
1419
|
|
|
866
|
-
|
|
1420
|
+
type CachedRequestDecision = {
|
|
1421
|
+
body: JsonRecord;
|
|
1422
|
+
contextMode: "full" | "delta";
|
|
1423
|
+
previousResponseId?: string;
|
|
1424
|
+
bypassReason?: CodexContinuationBypassReason;
|
|
1425
|
+
historyMismatch?: CodexContinuationHistoryMismatch;
|
|
1426
|
+
cacheIdentityPreserved?: boolean;
|
|
1427
|
+
};
|
|
1428
|
+
|
|
1429
|
+
type CodexWebSocketAttempt = {
|
|
1430
|
+
connection: "new" | "reused";
|
|
1431
|
+
contextMode: "full" | "delta";
|
|
1432
|
+
inputItems: number;
|
|
1433
|
+
fullInputItems: number;
|
|
1434
|
+
fullRequestBytes: number;
|
|
1435
|
+
wireRequestBytes: number;
|
|
1436
|
+
previousResponseId?: string;
|
|
1437
|
+
bypassReason?: CodexContinuationBypassReason;
|
|
1438
|
+
historyMismatch?: CodexContinuationHistoryMismatch;
|
|
1439
|
+
cacheIdentityPreserved?: boolean;
|
|
1440
|
+
turnStateReplayed?: boolean;
|
|
1441
|
+
turnStateReplayedValue?: string;
|
|
1442
|
+
};
|
|
1443
|
+
|
|
1444
|
+
function cachedRequestBody(entry: CachedWebSocket, body: JsonRecord): CachedRequestDecision {
|
|
867
1445
|
const continuation = entry.continuation;
|
|
868
|
-
if (!continuation) return body;
|
|
1446
|
+
if (!continuation) return { body, contextMode: "full" };
|
|
1447
|
+
const previousResponseId = continuation.lastResponseId;
|
|
1448
|
+
const cacheIdentityPreserved =
|
|
1449
|
+
continuation.lastRequestBody.prompt_cache_key === body.prompt_cache_key;
|
|
869
1450
|
if (
|
|
870
|
-
|
|
871
|
-
|
|
1451
|
+
stableResponsesJson(requestWithoutHistory(body)) !==
|
|
1452
|
+
stableResponsesJson(requestWithoutHistory(continuation.lastRequestBody))
|
|
872
1453
|
) {
|
|
873
1454
|
delete entry.continuation;
|
|
874
|
-
return
|
|
1455
|
+
return {
|
|
1456
|
+
body,
|
|
1457
|
+
contextMode: "full",
|
|
1458
|
+
previousResponseId,
|
|
1459
|
+
bypassReason: "request_template_changed",
|
|
1460
|
+
cacheIdentityPreserved,
|
|
1461
|
+
};
|
|
875
1462
|
}
|
|
876
1463
|
|
|
877
1464
|
const currentInput = body.input ?? [];
|
|
878
1465
|
const previousInput = continuation.lastRequestBody.input ?? [];
|
|
879
1466
|
if (!Array.isArray(currentInput) || !Array.isArray(previousInput)) {
|
|
880
1467
|
delete entry.continuation;
|
|
881
|
-
return
|
|
1468
|
+
return {
|
|
1469
|
+
body,
|
|
1470
|
+
contextMode: "full",
|
|
1471
|
+
previousResponseId,
|
|
1472
|
+
bypassReason: "non_array_input",
|
|
1473
|
+
cacheIdentityPreserved,
|
|
1474
|
+
};
|
|
882
1475
|
}
|
|
883
1476
|
const baseline = [...previousInput, ...continuation.lastResponseItems];
|
|
884
|
-
|
|
885
|
-
currentInput.length
|
|
886
|
-
|
|
887
|
-
) {
|
|
1477
|
+
const mismatchIndex = baseline.findIndex(
|
|
1478
|
+
(item, index) => index >= currentInput.length || !responseItemsMatch(item, currentInput[index]),
|
|
1479
|
+
);
|
|
1480
|
+
if (mismatchIndex >= 0) {
|
|
888
1481
|
delete entry.continuation;
|
|
889
|
-
return
|
|
1482
|
+
return {
|
|
1483
|
+
body,
|
|
1484
|
+
contextMode: "full",
|
|
1485
|
+
previousResponseId,
|
|
1486
|
+
bypassReason: "history_prefix_changed",
|
|
1487
|
+
historyMismatch: {
|
|
1488
|
+
index: mismatchIndex,
|
|
1489
|
+
baselineInputItems: baseline.length,
|
|
1490
|
+
currentInputItems: currentInput.length,
|
|
1491
|
+
baselineItem: structuredClone(baseline[mismatchIndex]),
|
|
1492
|
+
...(mismatchIndex < currentInput.length
|
|
1493
|
+
? { currentItem: structuredClone(currentInput[mismatchIndex]) }
|
|
1494
|
+
: {}),
|
|
1495
|
+
},
|
|
1496
|
+
cacheIdentityPreserved,
|
|
1497
|
+
};
|
|
890
1498
|
}
|
|
891
1499
|
|
|
892
1500
|
return {
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1501
|
+
body: {
|
|
1502
|
+
...body,
|
|
1503
|
+
previous_response_id: previousResponseId,
|
|
1504
|
+
input: currentInput.slice(baseline.length),
|
|
1505
|
+
},
|
|
1506
|
+
contextMode: "delta",
|
|
1507
|
+
previousResponseId,
|
|
1508
|
+
cacheIdentityPreserved,
|
|
896
1509
|
};
|
|
897
1510
|
}
|
|
898
1511
|
|
|
@@ -900,6 +1513,15 @@ function requestInputLength(body: JsonRecord): number {
|
|
|
900
1513
|
return typeof body.input === "string" || Array.isArray(body.input) ? body.input.length : 0;
|
|
901
1514
|
}
|
|
902
1515
|
|
|
1516
|
+
function webSocketEventStartsVisibleOutput(event: JsonRecord): boolean {
|
|
1517
|
+
return (
|
|
1518
|
+
event.type !== "response.created" &&
|
|
1519
|
+
event.type !== "response.queued" &&
|
|
1520
|
+
event.type !== "response.in_progress" &&
|
|
1521
|
+
event.type !== "response.metadata"
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
903
1525
|
async function decodeWebSocketData(data: unknown): Promise<string | undefined> {
|
|
904
1526
|
if (typeof data === "string") return data;
|
|
905
1527
|
if (data instanceof ArrayBuffer) return new TextDecoder().decode(new Uint8Array(data));
|
|
@@ -942,7 +1564,8 @@ async function* parseWebSocket(
|
|
|
942
1564
|
if (
|
|
943
1565
|
type === "response.completed" ||
|
|
944
1566
|
type === "response.done" ||
|
|
945
|
-
type === "response.incomplete"
|
|
1567
|
+
type === "response.incomplete" ||
|
|
1568
|
+
type === "response.failed"
|
|
946
1569
|
) {
|
|
947
1570
|
terminal = true;
|
|
948
1571
|
done = true;
|
|
@@ -1040,13 +1663,6 @@ function normalizeEvent(event: JsonRecord): JsonRecord | undefined {
|
|
|
1040
1663
|
event,
|
|
1041
1664
|
);
|
|
1042
1665
|
}
|
|
1043
|
-
if (type === "response.failed") {
|
|
1044
|
-
const response = isObject(event.response) ? event.response : undefined;
|
|
1045
|
-
const error = isObject(response?.["error"]) ? response["error"] : undefined;
|
|
1046
|
-
const message = typeof error?.["message"] === "string" ? error["message"] : undefined;
|
|
1047
|
-
const code = typeof error?.["code"] === "string" ? error["code"] : undefined;
|
|
1048
|
-
throw new CodexApiError(message || "Codex response failed", code, event);
|
|
1049
|
-
}
|
|
1050
1666
|
if (type === "response.done") {
|
|
1051
1667
|
return { ...event, type: "response.completed" };
|
|
1052
1668
|
}
|
|
@@ -1054,7 +1670,11 @@ function normalizeEvent(event: JsonRecord): JsonRecord | undefined {
|
|
|
1054
1670
|
}
|
|
1055
1671
|
|
|
1056
1672
|
function isTerminalEvent(event: JsonRecord): boolean {
|
|
1057
|
-
return
|
|
1673
|
+
return (
|
|
1674
|
+
event.type === "response.completed" ||
|
|
1675
|
+
event.type === "response.incomplete" ||
|
|
1676
|
+
event.type === "response.failed"
|
|
1677
|
+
);
|
|
1058
1678
|
}
|
|
1059
1679
|
|
|
1060
1680
|
async function* requestSse(
|
|
@@ -1063,6 +1683,7 @@ async function* requestSse(
|
|
|
1063
1683
|
options: CodexTransportOptions,
|
|
1064
1684
|
headers: Headers,
|
|
1065
1685
|
timeoutMs: number | undefined,
|
|
1686
|
+
onAttempt: (turnStateReplayedValue: string | undefined) => void,
|
|
1066
1687
|
): AsyncGenerator<JsonRecord> {
|
|
1067
1688
|
const compressed = compressBody(bodyJson);
|
|
1068
1689
|
if (compressed) headers.set("content-encoding", "zstd");
|
|
@@ -1074,6 +1695,7 @@ async function* requestSse(
|
|
|
1074
1695
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1075
1696
|
if (options.signal?.aborted) throw new Error("Request was aborted");
|
|
1076
1697
|
try {
|
|
1698
|
+
onAttempt(applyTurnStateHeader(headers, options.turnState));
|
|
1077
1699
|
const timeoutSignal =
|
|
1078
1700
|
timeoutMs !== undefined && timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;
|
|
1079
1701
|
const combined = combineAbortSignals([options.signal, timeoutSignal]);
|
|
@@ -1094,6 +1716,7 @@ async function* requestSse(
|
|
|
1094
1716
|
} finally {
|
|
1095
1717
|
combined.cleanup();
|
|
1096
1718
|
}
|
|
1719
|
+
captureTurnStateHeader(response.headers, options.turnState);
|
|
1097
1720
|
await options.onResponse?.(
|
|
1098
1721
|
{ status: response.status, headers: headersToRecord(response.headers) },
|
|
1099
1722
|
model,
|
|
@@ -1105,7 +1728,7 @@ async function* requestSse(
|
|
|
1105
1728
|
const requestedDelay = retryDelayMs(response.headers);
|
|
1106
1729
|
const delay =
|
|
1107
1730
|
requestedDelay === undefined
|
|
1108
|
-
? BASE_DELAY_MS
|
|
1731
|
+
? retryBackoffMs(BASE_DELAY_MS, attempt + 1)
|
|
1109
1732
|
: validateRetryDelay(requestedDelay, options);
|
|
1110
1733
|
await sleep(delay, options.signal);
|
|
1111
1734
|
continue;
|
|
@@ -1122,10 +1745,11 @@ async function* requestSse(
|
|
|
1122
1745
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1123
1746
|
if (
|
|
1124
1747
|
attempt < maxRetries &&
|
|
1748
|
+
!(lastError instanceof CodexHttpError) &&
|
|
1125
1749
|
!(lastError instanceof RetryDelayExceededError) &&
|
|
1126
1750
|
!lastError.message.includes("usage limit")
|
|
1127
1751
|
) {
|
|
1128
|
-
await sleep(BASE_DELAY_MS
|
|
1752
|
+
await sleep(retryBackoffMs(BASE_DELAY_MS, attempt + 1), options.signal);
|
|
1129
1753
|
continue;
|
|
1130
1754
|
}
|
|
1131
1755
|
throw lastError;
|
|
@@ -1135,12 +1759,18 @@ async function* requestSse(
|
|
|
1135
1759
|
if (!response?.ok) throw lastError ?? new Error("Failed after retries");
|
|
1136
1760
|
if (!response.body) throw new Error("No response body");
|
|
1137
1761
|
options.onTransportStart?.();
|
|
1762
|
+
let terminal = false;
|
|
1138
1763
|
for await (const event of parseSse(response, options.signal)) {
|
|
1139
1764
|
const normalized = normalizeEvent(event);
|
|
1140
1765
|
if (!normalized) continue;
|
|
1141
1766
|
yield normalized;
|
|
1142
|
-
if (isTerminalEvent(normalized))
|
|
1767
|
+
if (isTerminalEvent(normalized)) {
|
|
1768
|
+
terminal = true;
|
|
1769
|
+
break;
|
|
1770
|
+
}
|
|
1143
1771
|
}
|
|
1772
|
+
if (!terminal)
|
|
1773
|
+
throw new SseStreamIncompleteError("Codex SSE stream ended before a terminal event");
|
|
1144
1774
|
}
|
|
1145
1775
|
|
|
1146
1776
|
async function* requestWebSocket(
|
|
@@ -1153,6 +1783,7 @@ async function* requestWebSocket(
|
|
|
1153
1783
|
timeoutMs: number | undefined,
|
|
1154
1784
|
connectTimeoutMs: number,
|
|
1155
1785
|
requestBytes: number,
|
|
1786
|
+
onAttempt: (attempt: CodexWebSocketAttempt) => void,
|
|
1156
1787
|
): AsyncGenerator<JsonRecord> {
|
|
1157
1788
|
const acquired = await acquireWebSocket(
|
|
1158
1789
|
resolveCodexWebSocketUrl(model.baseUrl),
|
|
@@ -1175,12 +1806,57 @@ async function* requestWebSocket(
|
|
|
1175
1806
|
if (options.signal?.aborted) throw new Error("Request was aborted");
|
|
1176
1807
|
const useContinuation =
|
|
1177
1808
|
options.transport === "auto" || options.transport === "websocket-cached";
|
|
1178
|
-
const
|
|
1179
|
-
const
|
|
1180
|
-
useContinuation && acquired.entry ?
|
|
1809
|
+
const routed = withTurnStateMetadata(body, options.turnState);
|
|
1810
|
+
const fullBody =
|
|
1811
|
+
useContinuation && acquired.entry ? jsonWireRequestBody(routed.body) : routed.body;
|
|
1812
|
+
const decision =
|
|
1813
|
+
useContinuation && acquired.entry
|
|
1814
|
+
? cachedRequestBody(acquired.entry, fullBody)
|
|
1815
|
+
: ({ body: fullBody, contextMode: "full" } satisfies CachedRequestDecision);
|
|
1816
|
+
const requestBody = decision.body;
|
|
1817
|
+
const warmup = options.warmup === true;
|
|
1818
|
+
const requestStartedAtUnixMs = String(Date.now());
|
|
1819
|
+
const withRequestStart = (candidate: JsonRecord): JsonRecord => ({
|
|
1820
|
+
...candidate,
|
|
1821
|
+
client_metadata: {
|
|
1822
|
+
...(isObject(candidate.client_metadata) ? candidate.client_metadata : {}),
|
|
1823
|
+
[CODEX_WS_REQUEST_START_METADATA_KEY]: requestStartedAtUnixMs,
|
|
1824
|
+
},
|
|
1825
|
+
});
|
|
1826
|
+
const fullRequestJson = JSON.stringify({
|
|
1827
|
+
type: "response.create",
|
|
1828
|
+
...withRequestStart(fullBody),
|
|
1829
|
+
...(warmup ? { generate: false } : {}),
|
|
1830
|
+
});
|
|
1831
|
+
const wireRequestJson = JSON.stringify({
|
|
1832
|
+
type: "response.create",
|
|
1833
|
+
...withRequestStart(requestBody),
|
|
1834
|
+
...(warmup ? { generate: false } : {}),
|
|
1835
|
+
});
|
|
1836
|
+
onAttempt({
|
|
1837
|
+
connection: acquired.reused ? "reused" : "new",
|
|
1838
|
+
contextMode: decision.contextMode,
|
|
1839
|
+
inputItems: requestInputLength(requestBody),
|
|
1840
|
+
fullInputItems: requestInputLength(fullBody),
|
|
1841
|
+
fullRequestBytes: serializedBytes(fullRequestJson),
|
|
1842
|
+
wireRequestBytes: serializedBytes(wireRequestJson),
|
|
1843
|
+
...(decision.previousResponseId ? { previousResponseId: decision.previousResponseId } : {}),
|
|
1844
|
+
...(decision.bypassReason ? { bypassReason: decision.bypassReason } : {}),
|
|
1845
|
+
...(decision.historyMismatch ? { historyMismatch: decision.historyMismatch } : {}),
|
|
1846
|
+
...(decision.cacheIdentityPreserved === undefined
|
|
1847
|
+
? {}
|
|
1848
|
+
: { cacheIdentityPreserved: decision.cacheIdentityPreserved }),
|
|
1849
|
+
...(options.turnState
|
|
1850
|
+
? {
|
|
1851
|
+
turnStateReplayed: routed.replayedValue !== undefined,
|
|
1852
|
+
...(routed.replayedValue ? { turnStateReplayedValue: routed.replayedValue } : {}),
|
|
1853
|
+
}
|
|
1854
|
+
: {}),
|
|
1855
|
+
});
|
|
1181
1856
|
const stats = sessionId ? getOrCreateWebSocketDebugStats(sessionId) : undefined;
|
|
1182
1857
|
if (stats) {
|
|
1183
1858
|
stats.requests += 1;
|
|
1859
|
+
if (warmup) stats.prewarmRequests += 1;
|
|
1184
1860
|
if (acquired.reused) stats.connectionsReused += 1;
|
|
1185
1861
|
else stats.connectionsCreated += 1;
|
|
1186
1862
|
if (useContinuation) stats.cachedContextRequests += 1;
|
|
@@ -1206,10 +1882,13 @@ async function* requestWebSocket(
|
|
|
1206
1882
|
return true;
|
|
1207
1883
|
},
|
|
1208
1884
|
});
|
|
1209
|
-
acquired.socket.send(
|
|
1885
|
+
acquired.socket.send(wireRequestJson);
|
|
1210
1886
|
const responseItems: JsonRecord[] = [];
|
|
1211
1887
|
let responseId: string | undefined;
|
|
1888
|
+
let responseCompleted = false;
|
|
1212
1889
|
for await (const event of parseWebSocket(acquired.socket, options.signal, timeoutMs)) {
|
|
1890
|
+
captureTurnStateEvent(event, options.turnState);
|
|
1891
|
+
if (event.type === "response.metadata") continue;
|
|
1213
1892
|
if (
|
|
1214
1893
|
event.type === "response.created" &&
|
|
1215
1894
|
isObject(event.response) &&
|
|
@@ -1223,9 +1902,13 @@ async function* requestWebSocket(
|
|
|
1223
1902
|
if (
|
|
1224
1903
|
(event.type === "response.completed" ||
|
|
1225
1904
|
event.type === "response.done" ||
|
|
1226
|
-
event.type === "response.incomplete"
|
|
1905
|
+
event.type === "response.incomplete" ||
|
|
1906
|
+
event.type === "response.failed") &&
|
|
1227
1907
|
isObject(event.response)
|
|
1228
1908
|
) {
|
|
1909
|
+
if (event.type === "response.completed" || event.type === "response.done") {
|
|
1910
|
+
responseCompleted = true;
|
|
1911
|
+
}
|
|
1229
1912
|
if (typeof event.response.id === "string") responseId = event.response.id;
|
|
1230
1913
|
if (Array.isArray(event.response["output"])) {
|
|
1231
1914
|
const terminalItems = event.response["output"].filter(isObject);
|
|
@@ -1244,7 +1927,7 @@ async function* requestWebSocket(
|
|
|
1244
1927
|
if (isTerminalEvent(normalized)) break;
|
|
1245
1928
|
}
|
|
1246
1929
|
if (options.signal?.aborted) throw new Error("Request was aborted");
|
|
1247
|
-
if (useContinuation && acquired.entry && responseId) {
|
|
1930
|
+
if (useContinuation && acquired.entry && responseId && responseCompleted) {
|
|
1248
1931
|
const entry = acquired.entry;
|
|
1249
1932
|
const continuation = {
|
|
1250
1933
|
lastRequestBody: fullBody,
|
|
@@ -1317,6 +2000,99 @@ export async function requestCodexJson(
|
|
|
1317
2000
|
}
|
|
1318
2001
|
|
|
1319
2002
|
export class CodexTransport {
|
|
2003
|
+
async prewarm(
|
|
2004
|
+
model: Model<any>,
|
|
2005
|
+
body: JsonRecord,
|
|
2006
|
+
options: CodexTransportOptions,
|
|
2007
|
+
): Promise<boolean> {
|
|
2008
|
+
const transport = options.transport ?? "auto";
|
|
2009
|
+
const turnStateAvailableAtStart = options.turnState?.available ?? false;
|
|
2010
|
+
const turnStateRevisionAtStart = options.turnState?.revision ?? 0;
|
|
2011
|
+
const turnStateValueAtStart = options.turnState?.replayValue();
|
|
2012
|
+
const diagnosticDetails = () => {
|
|
2013
|
+
const turnStateReceived =
|
|
2014
|
+
(options.turnState?.revision ?? turnStateRevisionAtStart) > turnStateRevisionAtStart;
|
|
2015
|
+
const turnStateReceivedValue = options.turnState?.replayValue();
|
|
2016
|
+
return {
|
|
2017
|
+
...(options.cacheDiagnostics ? { cache: options.cacheDiagnostics } : {}),
|
|
2018
|
+
...(options.turnState
|
|
2019
|
+
? {
|
|
2020
|
+
turnStateAvailableAtStart,
|
|
2021
|
+
turnStateReceived,
|
|
2022
|
+
...(turnStateValueAtStart ? { turnStateAtStart: turnStateValueAtStart } : {}),
|
|
2023
|
+
...(turnStateReceived && turnStateReceivedValue ? { turnStateReceivedValue } : {}),
|
|
2024
|
+
}
|
|
2025
|
+
: {}),
|
|
2026
|
+
};
|
|
2027
|
+
};
|
|
2028
|
+
if (transport === "sse") {
|
|
2029
|
+
options.onTransportDiagnostic?.({
|
|
2030
|
+
type: "codex_transport_prewarm",
|
|
2031
|
+
timestamp: Date.now(),
|
|
2032
|
+
details: {
|
|
2033
|
+
outcome: "skipped",
|
|
2034
|
+
continuationReady: false,
|
|
2035
|
+
reason: "sse_configured",
|
|
2036
|
+
...diagnosticDetails(),
|
|
2037
|
+
},
|
|
2038
|
+
});
|
|
2039
|
+
return false;
|
|
2040
|
+
}
|
|
2041
|
+
const cacheSessionId = options.cacheRetention === "none" ? undefined : options.sessionId;
|
|
2042
|
+
if (isWebSocketSseFallbackActive(cacheSessionId)) {
|
|
2043
|
+
options.onTransportDiagnostic?.({
|
|
2044
|
+
type: "codex_transport_prewarm",
|
|
2045
|
+
timestamp: Date.now(),
|
|
2046
|
+
details: {
|
|
2047
|
+
outcome: "skipped",
|
|
2048
|
+
continuationReady: false,
|
|
2049
|
+
reason: "sticky_sse_fallback",
|
|
2050
|
+
...diagnosticDetails(),
|
|
2051
|
+
},
|
|
2052
|
+
});
|
|
2053
|
+
return false;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
let continuationReady = false;
|
|
2057
|
+
const notifyContinuationReady = (handle: CodexContinuationHandle): void => {
|
|
2058
|
+
options.onContinuationReady?.(handle);
|
|
2059
|
+
};
|
|
2060
|
+
try {
|
|
2061
|
+
for await (const _event of this.request(model, body, {
|
|
2062
|
+
...options,
|
|
2063
|
+
warmup: true,
|
|
2064
|
+
requestKind: "prewarm",
|
|
2065
|
+
onContinuationReady(handle) {
|
|
2066
|
+
continuationReady = true;
|
|
2067
|
+
notifyContinuationReady(handle);
|
|
2068
|
+
},
|
|
2069
|
+
})) {
|
|
2070
|
+
// A v2 warmup completes without model output.
|
|
2071
|
+
}
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
options.onTransportDiagnostic?.({
|
|
2074
|
+
type: "codex_transport_prewarm",
|
|
2075
|
+
timestamp: Date.now(),
|
|
2076
|
+
details: {
|
|
2077
|
+
outcome: "failed",
|
|
2078
|
+
continuationReady: false,
|
|
2079
|
+
...diagnosticDetails(),
|
|
2080
|
+
},
|
|
2081
|
+
});
|
|
2082
|
+
throw error;
|
|
2083
|
+
}
|
|
2084
|
+
options.onTransportDiagnostic?.({
|
|
2085
|
+
type: "codex_transport_prewarm",
|
|
2086
|
+
timestamp: Date.now(),
|
|
2087
|
+
details: {
|
|
2088
|
+
outcome: "completed",
|
|
2089
|
+
continuationReady,
|
|
2090
|
+
...diagnosticDetails(),
|
|
2091
|
+
},
|
|
2092
|
+
});
|
|
2093
|
+
return continuationReady;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
1320
2096
|
async *request(
|
|
1321
2097
|
model: Model<any>,
|
|
1322
2098
|
body: JsonRecord,
|
|
@@ -1326,27 +2102,71 @@ export class CodexTransport {
|
|
|
1326
2102
|
const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
|
|
1327
2103
|
const connectTimeoutMs =
|
|
1328
2104
|
normalizeTimeoutMs(options.websocketConnectTimeoutMs) ?? DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS;
|
|
1329
|
-
const
|
|
1330
|
-
const requestBytes = new TextEncoder().encode(bodyJson).byteLength;
|
|
2105
|
+
const sseBody = responsesLiteSsePayload(body);
|
|
1331
2106
|
const accountId = options.accountId ?? validateCodexAuthentication(model, options.apiKey);
|
|
1332
2107
|
const cacheSessionId = options.cacheRetention === "none" ? undefined : options.sessionId;
|
|
1333
2108
|
const requestId = codexCacheKey(cacheSessionId);
|
|
1334
2109
|
const transport = options.transport ?? "auto";
|
|
2110
|
+
const turnStateAvailableAtStart = options.turnState?.available ?? false;
|
|
2111
|
+
const turnStateRevisionAtStart = options.turnState?.revision ?? 0;
|
|
2112
|
+
const turnStateValueAtStart = options.turnState?.replayValue();
|
|
2113
|
+
const websocketMaxRetries = normalizeRetryCount(
|
|
2114
|
+
options.websocketMaxRetries,
|
|
2115
|
+
DEFAULT_WEBSOCKET_MAX_RETRIES,
|
|
2116
|
+
"websocketMaxRetries",
|
|
2117
|
+
);
|
|
2118
|
+
const websocketRetryBaseDelayMs = normalizeRetryCount(
|
|
2119
|
+
options.websocketRetryBaseDelayMs,
|
|
2120
|
+
DEFAULT_WEBSOCKET_RETRY_BASE_DELAY_MS,
|
|
2121
|
+
"websocketRetryBaseDelayMs",
|
|
2122
|
+
);
|
|
2123
|
+
const sseStreamMaxRetries = normalizeRetryCount(
|
|
2124
|
+
options.sseStreamMaxRetries,
|
|
2125
|
+
DEFAULT_SSE_STREAM_MAX_RETRIES,
|
|
2126
|
+
"sseStreamMaxRetries",
|
|
2127
|
+
);
|
|
2128
|
+
const sseStreamRetryBaseDelayMs = normalizeRetryCount(
|
|
2129
|
+
options.sseStreamRetryBaseDelayMs,
|
|
2130
|
+
DEFAULT_SSE_STREAM_RETRY_BASE_DELAY_MS,
|
|
2131
|
+
"sseStreamRetryBaseDelayMs",
|
|
2132
|
+
);
|
|
1335
2133
|
|
|
1336
2134
|
const websocketDisabled = transport !== "sse" && isWebSocketSseFallbackActive(cacheSessionId);
|
|
2135
|
+
let sseRecovery:
|
|
2136
|
+
| {
|
|
2137
|
+
trigger: "sse_after_websocket_failure" | "sticky_sse_after_websocket_failure";
|
|
2138
|
+
previousCacheIdentity: CacheIdentitySnapshot;
|
|
2139
|
+
}
|
|
2140
|
+
| undefined;
|
|
2141
|
+
const existingFallback = webSocketFallbackSession(cacheSessionId);
|
|
2142
|
+
if (websocketDisabled && existingFallback) {
|
|
2143
|
+
sseRecovery = {
|
|
2144
|
+
trigger: "sticky_sse_after_websocket_failure",
|
|
2145
|
+
previousCacheIdentity: existingFallback.cacheIdentity,
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
1337
2148
|
if (websocketDisabled) recordWebSocketSseFallback(cacheSessionId);
|
|
1338
2149
|
if (transport !== "sse" && !websocketDisabled) {
|
|
2150
|
+
const websocketRequestBytes = serializedBytes(JSON.stringify(body));
|
|
1339
2151
|
const headers = websocketHeaders(
|
|
1340
2152
|
model.headers,
|
|
1341
2153
|
options.headers,
|
|
1342
2154
|
accountId,
|
|
1343
2155
|
options.apiKey,
|
|
1344
2156
|
requestId || uuidv7(),
|
|
2157
|
+
body,
|
|
1345
2158
|
);
|
|
2159
|
+
const webSocketCacheIdentity = cacheIdentitySnapshot(body, headers, accountId);
|
|
1346
2160
|
let retriedConnectionLimit = false;
|
|
1347
2161
|
let retriedMissingContinuation = false;
|
|
2162
|
+
let websocketRetries = 0;
|
|
2163
|
+
let visibleOutputEmitted = false;
|
|
2164
|
+
let anyEventEmitted = false;
|
|
1348
2165
|
while (true) {
|
|
1349
2166
|
let emitted = false;
|
|
2167
|
+
let attempt: CodexWebSocketAttempt | undefined;
|
|
2168
|
+
let usage: CodexCacheUsageDiagnostic | undefined;
|
|
2169
|
+
let responseId: string | undefined;
|
|
1350
2170
|
try {
|
|
1351
2171
|
for await (const event of requestWebSocket(
|
|
1352
2172
|
model,
|
|
@@ -1357,12 +2177,58 @@ export class CodexTransport {
|
|
|
1357
2177
|
accountId,
|
|
1358
2178
|
timeoutMs,
|
|
1359
2179
|
connectTimeoutMs,
|
|
1360
|
-
|
|
2180
|
+
websocketRequestBytes,
|
|
2181
|
+
(currentAttempt) => {
|
|
2182
|
+
attempt = currentAttempt;
|
|
2183
|
+
if (!currentAttempt.bypassReason) return;
|
|
2184
|
+
options.onTransportDiagnostic?.(
|
|
2185
|
+
transportRecoveryDiagnostic({
|
|
2186
|
+
trigger: "local_continuation_bypass",
|
|
2187
|
+
configuredTransport: transport,
|
|
2188
|
+
attempts: [webSocketRecoveryAttempt(currentAttempt, "selected")],
|
|
2189
|
+
cacheIdentity: webSocketCacheIdentity,
|
|
2190
|
+
...(currentAttempt.previousResponseId
|
|
2191
|
+
? { previousResponseId: currentAttempt.previousResponseId }
|
|
2192
|
+
: {}),
|
|
2193
|
+
continuationBypassReason: currentAttempt.bypassReason,
|
|
2194
|
+
...(currentAttempt.historyMismatch
|
|
2195
|
+
? { historyMismatch: currentAttempt.historyMismatch }
|
|
2196
|
+
: {}),
|
|
2197
|
+
...(currentAttempt.cacheIdentityPreserved === undefined
|
|
2198
|
+
? {}
|
|
2199
|
+
: { cacheIdentityPreserved: currentAttempt.cacheIdentityPreserved }),
|
|
2200
|
+
accountIdentityPreserved: true,
|
|
2201
|
+
}),
|
|
2202
|
+
);
|
|
2203
|
+
},
|
|
1361
2204
|
)) {
|
|
1362
2205
|
if (!emitted) options.onTransportStart?.();
|
|
1363
2206
|
emitted = true;
|
|
2207
|
+
anyEventEmitted = true;
|
|
2208
|
+
if (webSocketEventStartsVisibleOutput(event)) visibleOutputEmitted = true;
|
|
2209
|
+
usage = cacheUsageDiagnostic(event) ?? usage;
|
|
2210
|
+
if (isObject(event.response) && typeof event.response.id === "string") {
|
|
2211
|
+
responseId = event.response.id;
|
|
2212
|
+
}
|
|
1364
2213
|
yield event;
|
|
1365
2214
|
}
|
|
2215
|
+
if (attempt && shouldReportRequestDiagnostic(options)) {
|
|
2216
|
+
options.onTransportDiagnostic?.(
|
|
2217
|
+
transportRequestDiagnostic({
|
|
2218
|
+
requestOptions: options,
|
|
2219
|
+
body,
|
|
2220
|
+
configuredTransport: transport,
|
|
2221
|
+
selectedTransport: "websocket",
|
|
2222
|
+
attempt,
|
|
2223
|
+
cacheIdentity: webSocketCacheIdentity,
|
|
2224
|
+
turnStateAvailableAtStart,
|
|
2225
|
+
turnStateRevisionAtStart,
|
|
2226
|
+
...(turnStateValueAtStart ? { turnStateValueAtStart } : {}),
|
|
2227
|
+
...(responseId ? { responseId } : {}),
|
|
2228
|
+
...(usage ? { usage } : {}),
|
|
2229
|
+
}),
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
1366
2232
|
return;
|
|
1367
2233
|
} catch (error) {
|
|
1368
2234
|
const aborted = options.signal?.aborted;
|
|
@@ -1370,34 +2236,211 @@ export class CodexTransport {
|
|
|
1370
2236
|
!emitted && isWebSocketConnectionLimitReachedError(error);
|
|
1371
2237
|
if (!aborted && isPreviousResponseNotFoundError(error) && !retriedMissingContinuation) {
|
|
1372
2238
|
retriedMissingContinuation = true;
|
|
2239
|
+
if (attempt) {
|
|
2240
|
+
const retryTurnStateValue = options.turnState?.replayValue();
|
|
2241
|
+
options.onTransportDiagnostic?.(
|
|
2242
|
+
transportRecoveryDiagnostic({
|
|
2243
|
+
trigger: "previous_response_not_found",
|
|
2244
|
+
configuredTransport: transport,
|
|
2245
|
+
attempts: [
|
|
2246
|
+
webSocketRecoveryAttempt(attempt, "previous_response_not_found"),
|
|
2247
|
+
{
|
|
2248
|
+
transport: "websocket",
|
|
2249
|
+
connection: "new",
|
|
2250
|
+
contextMode: "full",
|
|
2251
|
+
inputItems: attempt.fullInputItems,
|
|
2252
|
+
fullInputItems: attempt.fullInputItems,
|
|
2253
|
+
fullRequestBytes: attempt.fullRequestBytes,
|
|
2254
|
+
wireRequestBytes: attempt.fullRequestBytes,
|
|
2255
|
+
outcome: "retry_scheduled",
|
|
2256
|
+
...(options.turnState
|
|
2257
|
+
? {
|
|
2258
|
+
turnStateReplayed: options.turnState.available,
|
|
2259
|
+
...(retryTurnStateValue
|
|
2260
|
+
? { turnStateReplayedValue: retryTurnStateValue }
|
|
2261
|
+
: {}),
|
|
2262
|
+
}
|
|
2263
|
+
: {}),
|
|
2264
|
+
},
|
|
2265
|
+
],
|
|
2266
|
+
cacheIdentity: webSocketCacheIdentity,
|
|
2267
|
+
error,
|
|
2268
|
+
...(attempt.previousResponseId
|
|
2269
|
+
? { previousResponseId: attempt.previousResponseId }
|
|
2270
|
+
: {}),
|
|
2271
|
+
cacheIdentityPreserved: true,
|
|
2272
|
+
accountIdentityPreserved: true,
|
|
2273
|
+
}),
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
1373
2276
|
continue;
|
|
1374
2277
|
}
|
|
1375
2278
|
if (!aborted && connectionLimitBeforeStart && !retriedConnectionLimit) {
|
|
1376
2279
|
retriedConnectionLimit = true;
|
|
1377
2280
|
continue;
|
|
1378
2281
|
}
|
|
2282
|
+
const retryableTransportError =
|
|
2283
|
+
!aborted &&
|
|
2284
|
+
!visibleOutputEmitted &&
|
|
2285
|
+
(!isCodexNonTransportError(error) || connectionLimitBeforeStart);
|
|
2286
|
+
if (retryableTransportError && websocketRetries < websocketMaxRetries) {
|
|
2287
|
+
websocketRetries += 1;
|
|
2288
|
+
if (attempt) {
|
|
2289
|
+
options.onTransportDiagnostic?.(
|
|
2290
|
+
transportRecoveryDiagnostic({
|
|
2291
|
+
trigger: "websocket_retry",
|
|
2292
|
+
configuredTransport: transport,
|
|
2293
|
+
attempts: [webSocketRecoveryAttempt(attempt, "retry_scheduled")],
|
|
2294
|
+
cacheIdentity: webSocketCacheIdentity,
|
|
2295
|
+
error,
|
|
2296
|
+
cacheIdentityPreserved: true,
|
|
2297
|
+
accountIdentityPreserved: true,
|
|
2298
|
+
retryNumber: websocketRetries,
|
|
2299
|
+
maxRetries: websocketMaxRetries,
|
|
2300
|
+
}),
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2303
|
+
await sleep(
|
|
2304
|
+
retryBackoffMs(websocketRetryBaseDelayMs, websocketRetries),
|
|
2305
|
+
options.signal,
|
|
2306
|
+
);
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
1379
2309
|
if (aborted || (isCodexNonTransportError(error) && !connectionLimitBeforeStart)) {
|
|
1380
2310
|
throw error;
|
|
1381
2311
|
}
|
|
1382
2312
|
options.onTransportDiagnostic?.(
|
|
1383
|
-
transportDiagnostic(
|
|
2313
|
+
transportDiagnostic(
|
|
2314
|
+
error,
|
|
2315
|
+
transport,
|
|
2316
|
+
anyEventEmitted,
|
|
2317
|
+
websocketRequestBytes,
|
|
2318
|
+
!visibleOutputEmitted,
|
|
2319
|
+
),
|
|
1384
2320
|
);
|
|
1385
|
-
recordWebSocketFailure(cacheSessionId, error);
|
|
1386
|
-
if (
|
|
2321
|
+
recordWebSocketFailure(cacheSessionId, error, webSocketCacheIdentity);
|
|
2322
|
+
if (visibleOutputEmitted) throw error;
|
|
1387
2323
|
recordWebSocketSseFallback(cacheSessionId);
|
|
2324
|
+
if (options.warmup) throw error;
|
|
2325
|
+
sseRecovery = {
|
|
2326
|
+
trigger: "sse_after_websocket_failure",
|
|
2327
|
+
previousCacheIdentity: webSocketCacheIdentity,
|
|
2328
|
+
};
|
|
1388
2329
|
break;
|
|
1389
2330
|
}
|
|
1390
2331
|
}
|
|
1391
2332
|
}
|
|
1392
2333
|
|
|
2334
|
+
const bodyJson = JSON.stringify(sseBody);
|
|
2335
|
+
const sseRequestBytes = serializedBytes(bodyJson);
|
|
1393
2336
|
const headers = sseHeaders(
|
|
1394
2337
|
model.headers,
|
|
1395
2338
|
options.headers,
|
|
1396
2339
|
accountId,
|
|
1397
2340
|
options.apiKey,
|
|
1398
2341
|
requestId,
|
|
2342
|
+
body,
|
|
1399
2343
|
);
|
|
1400
|
-
|
|
2344
|
+
if (sseRecovery) {
|
|
2345
|
+
const sseCacheIdentity = cacheIdentitySnapshot(sseBody, headers, accountId);
|
|
2346
|
+
options.onTransportDiagnostic?.(
|
|
2347
|
+
transportRecoveryDiagnostic({
|
|
2348
|
+
trigger: sseRecovery.trigger,
|
|
2349
|
+
configuredTransport: transport,
|
|
2350
|
+
attempts: [
|
|
2351
|
+
sseRecoveryAttempt(sseBody, sseRequestBytes, options.turnState?.replayValue()),
|
|
2352
|
+
],
|
|
2353
|
+
cacheIdentity: sseCacheIdentity,
|
|
2354
|
+
previousCacheIdentity: sseRecovery.previousCacheIdentity,
|
|
2355
|
+
}),
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
const sseCacheIdentity = cacheIdentitySnapshot(sseBody, headers, accountId);
|
|
2359
|
+
let sseTurnStateReplayedValue: string | undefined;
|
|
2360
|
+
let usage: CodexCacheUsageDiagnostic | undefined;
|
|
2361
|
+
let responseId: string | undefined;
|
|
2362
|
+
let sseStreamRetries = 0;
|
|
2363
|
+
while (true) {
|
|
2364
|
+
let visibleOutputEmitted = false;
|
|
2365
|
+
try {
|
|
2366
|
+
for await (const event of requestSse(
|
|
2367
|
+
model,
|
|
2368
|
+
bodyJson,
|
|
2369
|
+
options,
|
|
2370
|
+
headers,
|
|
2371
|
+
timeoutMs,
|
|
2372
|
+
(replayedValue) => {
|
|
2373
|
+
sseTurnStateReplayedValue = replayedValue;
|
|
2374
|
+
},
|
|
2375
|
+
)) {
|
|
2376
|
+
if (webSocketEventStartsVisibleOutput(event)) visibleOutputEmitted = true;
|
|
2377
|
+
usage = cacheUsageDiagnostic(event) ?? usage;
|
|
2378
|
+
if (isObject(event.response) && typeof event.response.id === "string") {
|
|
2379
|
+
responseId = event.response.id;
|
|
2380
|
+
}
|
|
2381
|
+
yield event;
|
|
2382
|
+
}
|
|
2383
|
+
break;
|
|
2384
|
+
} catch (error) {
|
|
2385
|
+
const retryableError =
|
|
2386
|
+
error instanceof CodexHttpError ? error.retryable : !isCodexNonTransportError(error);
|
|
2387
|
+
const retryable =
|
|
2388
|
+
!options.signal?.aborted &&
|
|
2389
|
+
!visibleOutputEmitted &&
|
|
2390
|
+
retryableError &&
|
|
2391
|
+
thrownMessage(error) !== "Request was aborted" &&
|
|
2392
|
+
!(error instanceof RetryDelayExceededError);
|
|
2393
|
+
if (!retryable || sseStreamRetries >= sseStreamMaxRetries) throw error;
|
|
2394
|
+
sseStreamRetries += 1;
|
|
2395
|
+
options.onTransportDiagnostic?.(
|
|
2396
|
+
transportRecoveryDiagnostic({
|
|
2397
|
+
trigger: "sse_stream_retry",
|
|
2398
|
+
configuredTransport: transport,
|
|
2399
|
+
attempts: [
|
|
2400
|
+
{
|
|
2401
|
+
...sseRecoveryAttempt(sseBody, sseRequestBytes, sseTurnStateReplayedValue),
|
|
2402
|
+
outcome: "retry_scheduled",
|
|
2403
|
+
},
|
|
2404
|
+
],
|
|
2405
|
+
cacheIdentity: sseCacheIdentity,
|
|
2406
|
+
error,
|
|
2407
|
+
cacheIdentityPreserved: true,
|
|
2408
|
+
accountIdentityPreserved: true,
|
|
2409
|
+
retryNumber: sseStreamRetries,
|
|
2410
|
+
maxRetries: sseStreamMaxRetries,
|
|
2411
|
+
}),
|
|
2412
|
+
);
|
|
2413
|
+
await sleep(retryBackoffMs(sseStreamRetryBaseDelayMs, sseStreamRetries), options.signal);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
if (shouldReportRequestDiagnostic(options)) {
|
|
2417
|
+
const inputItems = requestInputLength(sseBody);
|
|
2418
|
+
options.onTransportDiagnostic?.(
|
|
2419
|
+
transportRequestDiagnostic({
|
|
2420
|
+
requestOptions: options,
|
|
2421
|
+
body: sseBody,
|
|
2422
|
+
configuredTransport: transport,
|
|
2423
|
+
selectedTransport: "sse",
|
|
2424
|
+
attempt: {
|
|
2425
|
+
contextMode: "full",
|
|
2426
|
+
inputItems,
|
|
2427
|
+
fullInputItems: inputItems,
|
|
2428
|
+
fullRequestBytes: sseRequestBytes,
|
|
2429
|
+
wireRequestBytes: sseRequestBytes,
|
|
2430
|
+
turnStateReplayed: sseTurnStateReplayedValue !== undefined,
|
|
2431
|
+
...(sseTurnStateReplayedValue
|
|
2432
|
+
? { turnStateReplayedValue: sseTurnStateReplayedValue }
|
|
2433
|
+
: {}),
|
|
2434
|
+
},
|
|
2435
|
+
cacheIdentity: sseCacheIdentity,
|
|
2436
|
+
turnStateAvailableAtStart,
|
|
2437
|
+
turnStateRevisionAtStart,
|
|
2438
|
+
...(turnStateValueAtStart ? { turnStateValueAtStart } : {}),
|
|
2439
|
+
...(responseId ? { responseId } : {}),
|
|
2440
|
+
...(usage ? { usage } : {}),
|
|
2441
|
+
}),
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
1401
2444
|
}
|
|
1402
2445
|
|
|
1403
2446
|
close(sessionId?: string): void {
|