@pure01fx/dsh-openai-codex-auth 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,602 @@
1
+ /** Native ChatGPT Codex HTTP/SSE transport with safe pre-output retries. */
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, attributionHeaders, } from '@deepseek-ai/dsh-llm';
4
+ import { nativeCodexAuthorityHash } from './catalog.js';
5
+ import { nativeCodexEndpoint } from './endpoint.js';
6
+ import { NATIVE_CODEX_CONNECTION_FAILED_CODE, NATIVE_CODEX_STREAM_INTERRUPTED_CODE, isNativeCodexConnectionFailure, } from './native-adapter.js';
7
+ import { boundedCodexTurnState, codexRequestBody, codexResponseTurnState, streamResponses, } from './responses.js';
8
+ import { hasNativeCodexReplayKind } from './replay.js';
9
+ import { parseCodexResponseUsageMetadata, publishCodexResponseUsage, } from './response-usage.js';
10
+ import { parseCodexRateLimitEvent, parseCodexRateLimitHeaders, publishCodexRateLimits, } from './rate-limits.js';
11
+ export const CODEX_RESPONSES_URL = 'https://chatgpt.com/backend-api/codex/responses';
12
+ const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
13
+ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300_000;
14
+ const DEFAULT_MAX_TRANSIENT_RETRIES = 4;
15
+ const DEFAULT_INITIAL_RETRY_DELAY_MS = 200;
16
+ const DEFAULT_MAX_RETRY_DELAY_MS = 10_000;
17
+ const INITIAL_CONNECTION_RETRY_DELAY_MS = 5_000;
18
+ const MAX_CONNECTION_RETRY_DELAY_MS = 60_000;
19
+ const DEFAULT_MAX_REQUEST_BODY_BYTES = 24 * 1024 * 1024;
20
+ const MAX_ERROR_BODY_BYTES = 64 * 1024;
21
+ function aborted(message = 'native Codex request was aborted') {
22
+ return new LlmError(message, 'ABORTED');
23
+ }
24
+ function throwIfAborted(signal) {
25
+ if (!signal?.aborted)
26
+ return;
27
+ if (signal.reason instanceof LlmError)
28
+ throw signal.reason;
29
+ throw aborted();
30
+ }
31
+ function fixedFailure(message, code, options) {
32
+ return new LlmError(message, code, options);
33
+ }
34
+ function safePositiveInteger(value, fallback, name) {
35
+ const resolved = value ?? fallback;
36
+ if (!Number.isSafeInteger(resolved) || resolved <= 0) {
37
+ throw fixedFailure(`native Codex ${name} is invalid`, 'INVALID_CONFIG');
38
+ }
39
+ return resolved;
40
+ }
41
+ function safeRetryCount(value) {
42
+ const resolved = value ?? DEFAULT_MAX_TRANSIENT_RETRIES;
43
+ if (!Number.isSafeInteger(resolved) || resolved < 0 || resolved > 10) {
44
+ throw fixedFailure('native Codex retry count is invalid', 'INVALID_CONFIG');
45
+ }
46
+ return resolved;
47
+ }
48
+ function stablePromptCacheKey(sessionId) {
49
+ const bytes = createHash('sha256').update(sessionId).digest().subarray(0, 16);
50
+ bytes[6] = (bytes[6] & 0x0f) | 0x50;
51
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
52
+ const hex = bytes.toString('hex');
53
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
54
+ }
55
+ function sleep(delayMs, signal) {
56
+ throwIfAborted(signal);
57
+ return new Promise((resolve, reject) => {
58
+ const timer = setTimeout(done, delayMs);
59
+ const abort = () => {
60
+ clearTimeout(timer);
61
+ signal?.removeEventListener('abort', abort);
62
+ const reason = signal?.reason;
63
+ reject(reason instanceof LlmError
64
+ ? reason : aborted('native Codex retry wait was aborted'));
65
+ };
66
+ function done() {
67
+ signal?.removeEventListener('abort', abort);
68
+ resolve();
69
+ }
70
+ signal?.addEventListener('abort', abort, { once: true });
71
+ });
72
+ }
73
+ function attemptWatchdog(parent, requestMs, idleMs) {
74
+ const controller = new AbortController();
75
+ let timeout;
76
+ let expired = false;
77
+ let idle = false;
78
+ const expire = () => {
79
+ expired = true;
80
+ controller.abort();
81
+ };
82
+ const arm = () => {
83
+ if (timeout !== undefined)
84
+ clearTimeout(timeout);
85
+ timeout = setTimeout(expire, idle ? idleMs : requestMs);
86
+ };
87
+ const fromParent = () => { controller.abort(parent?.reason); };
88
+ if (parent?.aborted)
89
+ fromParent();
90
+ else
91
+ parent?.addEventListener('abort', fromParent, { once: true });
92
+ arm();
93
+ return {
94
+ signal: controller.signal,
95
+ timedOut: () => expired,
96
+ beginIdle: () => { idle = true; arm(); },
97
+ pulse: () => { if (idle)
98
+ arm(); },
99
+ stop: () => {
100
+ if (timeout !== undefined)
101
+ clearTimeout(timeout);
102
+ parent?.removeEventListener('abort', fromParent);
103
+ if (!controller.signal.aborted)
104
+ controller.abort();
105
+ },
106
+ };
107
+ }
108
+ async function resolveImage(block, options, signal) {
109
+ if (options.readImage === undefined) {
110
+ throw fixedFailure('native Codex image input requires the attachment service', 'UNSUPPORTED');
111
+ }
112
+ throwIfAborted(signal);
113
+ const stored = await options.readImage(block.attachment, signal);
114
+ throwIfAborted(signal);
115
+ if (!(stored.data instanceof Uint8Array) || stored.data.byteLength !== block.attachment.bytes) {
116
+ throw fixedFailure('native Codex attachment bytes failed verification', 'INVALID_ATTACHMENT');
117
+ }
118
+ return {
119
+ type: 'image',
120
+ mediaType: block.attachment.mediaType,
121
+ dataBase64: Buffer.from(stored.data).toString('base64'),
122
+ };
123
+ }
124
+ async function resolveToolResult(block, options, signal) {
125
+ const content = [];
126
+ for (const part of block.content) {
127
+ throwIfAborted(signal);
128
+ if (part.type === 'text')
129
+ content.push(part);
130
+ else if (part.type === 'image')
131
+ content.push(await resolveImage(part, options, signal));
132
+ else {
133
+ throw fixedFailure('native Codex tool output contains an unsupported content block', 'UNSUPPORTED');
134
+ }
135
+ }
136
+ return {
137
+ type: 'tool-result',
138
+ toolCallId: block.toolCallId,
139
+ content,
140
+ ...block.isError === undefined ? {} : { isError: block.isError },
141
+ };
142
+ }
143
+ async function resolveMessages(generation, options) {
144
+ const messages = [];
145
+ for (const message of generation.messages) {
146
+ const content = [];
147
+ const source = message.source;
148
+ const sourceKind = source?.kind;
149
+ const subagentSettlement = message.role === 'user' && sourceKind === 'subagent-settled';
150
+ for (const block of message.content) {
151
+ throwIfAborted(generation.signal);
152
+ switch (block.type) {
153
+ case 'text':
154
+ content.push(block);
155
+ break;
156
+ case 'reasoning':
157
+ // DSH may relay an assistant's complete output as user-role context (for
158
+ // example, a background subagent settlement notice). Keep reasoning only
159
+ // where native replay can consume it; never promote relayed reasoning to
160
+ // user input or reject the otherwise valid context message.
161
+ if (message.role === 'assistant')
162
+ content.push(block);
163
+ break;
164
+ case 'image':
165
+ if (message.role !== 'user') {
166
+ throw fixedFailure('native Codex supports image input only in user messages', 'UNSUPPORTED');
167
+ }
168
+ content.push(await resolveImage(block, options, generation.signal));
169
+ break;
170
+ case 'tool-call':
171
+ if (message.role === 'assistant')
172
+ content.push(block);
173
+ else if (!subagentSettlement) {
174
+ throw fixedFailure('native Codex tool calls require assistant messages', 'INVALID_ARGS');
175
+ }
176
+ // A settlement copies the child's assistant output into user-role context.
177
+ // Its tool calls belong to the child and must not become parent wire calls.
178
+ break;
179
+ case 'tool-result':
180
+ if (message.role !== 'user') {
181
+ throw fixedFailure('native Codex tool results require user messages', 'INVALID_ARGS');
182
+ }
183
+ content.push(await resolveToolResult(block, options, generation.signal));
184
+ break;
185
+ default:
186
+ throw fixedFailure('native Codex request contains an unsupported content block', 'UNSUPPORTED');
187
+ }
188
+ }
189
+ const replaySource = message.role === 'assistant' && source?.kind === 'model'
190
+ && source.replayState !== undefined && hasNativeCodexReplayKind(source.replayState)
191
+ ? { provider: source.provider, model: source.model, replayState: source.replayState }
192
+ : undefined;
193
+ messages.push({
194
+ role: message.role,
195
+ content,
196
+ ...(replaySource === undefined ? {} : { replaySource }),
197
+ });
198
+ }
199
+ return messages;
200
+ }
201
+ async function boundedError(response, signal) {
202
+ if (response.body === null)
203
+ return {};
204
+ const reader = response.body.getReader();
205
+ const chunks = [];
206
+ let total = 0;
207
+ let cancelled = false;
208
+ const onAbort = () => {
209
+ cancelled = true;
210
+ void reader.cancel(signal?.reason).catch(() => { });
211
+ };
212
+ if (signal?.aborted)
213
+ onAbort();
214
+ else
215
+ signal?.addEventListener('abort', onAbort, { once: true });
216
+ try {
217
+ while (true) {
218
+ if (cancelled)
219
+ return {};
220
+ const { done, value } = await reader.read();
221
+ if (cancelled)
222
+ return {};
223
+ if (done)
224
+ break;
225
+ total += value.byteLength;
226
+ if (total > MAX_ERROR_BODY_BYTES) {
227
+ await reader.cancel().catch(() => { });
228
+ return {};
229
+ }
230
+ chunks.push(value);
231
+ }
232
+ }
233
+ catch {
234
+ return {};
235
+ }
236
+ finally {
237
+ signal?.removeEventListener('abort', onAbort);
238
+ reader.releaseLock();
239
+ }
240
+ try {
241
+ const value = JSON.parse(Buffer.concat(chunks.map(chunk => Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength)), total).toString('utf8'));
242
+ if (typeof value !== 'object' || value === null)
243
+ return {};
244
+ const outer = value;
245
+ const row = typeof outer.error === 'object' && outer.error !== null
246
+ ? outer.error
247
+ : outer;
248
+ return {
249
+ ...typeof row.code === 'string' ? { code: row.code } : {},
250
+ ...typeof row.message === 'string' ? { message: row.message } : {},
251
+ };
252
+ }
253
+ catch {
254
+ return {};
255
+ }
256
+ }
257
+ function retryAfterMs(response, maxDelayMs) {
258
+ const raw = response.headers.get('retry-after');
259
+ if (raw === null)
260
+ return undefined;
261
+ const seconds = Number(raw);
262
+ const value = Number.isFinite(seconds)
263
+ ? seconds * 1_000
264
+ : Date.parse(raw) - Date.now();
265
+ return Number.isFinite(value) && value > 0 ? Math.min(value, maxDelayMs) : undefined;
266
+ }
267
+ function errorFacts(response, maxDelayMs) {
268
+ const providerRetryAfterMs = retryAfterMs(response, maxDelayMs);
269
+ const requestId = response.headers.get('x-request-id');
270
+ return {
271
+ status: response.status,
272
+ ...providerRetryAfterMs === undefined ? {} : { providerRetryAfterMs },
273
+ ...requestId === null || requestId.length === 0 || requestId.length > 256
274
+ ? {}
275
+ : { requestId: ProviderRequestId(requestId) },
276
+ };
277
+ }
278
+ async function httpFailure(response, maxDelayMs, signal) {
279
+ const detail = await boundedError(response, signal);
280
+ const facts = errorFacts(response, maxDelayMs);
281
+ const classification = `${detail.code ?? ''} ${detail.message ?? ''}`;
282
+ if (response.status === 401 || response.status === 403) {
283
+ return fixedFailure('native Codex rejected the configured credential', 'AUTH', facts);
284
+ }
285
+ if (response.status === 429) {
286
+ if (isQuotaExceededError(classification)) {
287
+ return fixedFailure('native Codex account quota is exhausted', QUOTA_EXCEEDED_CODE, facts);
288
+ }
289
+ return fixedFailure('native Codex request was rate limited', 'RATE_LIMIT', facts);
290
+ }
291
+ if (detail.code === 'context_length_exceeded'
292
+ || detail.code === 'context_window_exceeded'
293
+ || isContextWindowExceededError(classification)) {
294
+ return fixedFailure('native Codex request exceeded the model context window', CONTEXT_WINDOW_EXCEEDED_CODE, facts);
295
+ }
296
+ if (isQuotaExceededError(classification)) {
297
+ return fixedFailure('native Codex account quota is exhausted', QUOTA_EXCEEDED_CODE, facts);
298
+ }
299
+ if (response.status === 408 || response.status === 504) {
300
+ return fixedFailure('native Codex request timed out', 'TIMEOUT', facts);
301
+ }
302
+ if (response.status >= 500) {
303
+ return fixedFailure('native Codex server request failed', 'SERVER', facts);
304
+ }
305
+ return fixedFailure('native Codex request was rejected', 'INVALID_REQUEST', facts);
306
+ }
307
+ function mappedFailure(error, watchdog, parent) {
308
+ if (parent?.aborted) {
309
+ return parent.reason instanceof LlmError ? parent.reason : aborted();
310
+ }
311
+ if (watchdog.timedOut())
312
+ return fixedFailure('native Codex request timed out', 'TIMEOUT');
313
+ if (error instanceof LlmError)
314
+ return error;
315
+ return fixedFailure('native Codex transport failed', 'TRANSPORT', { cause: error });
316
+ }
317
+ function connectionFailure(error, watchdog, parent) {
318
+ if (parent?.aborted || watchdog.timedOut())
319
+ return undefined;
320
+ if (error instanceof LlmError && error.code === NATIVE_CODEX_CONNECTION_FAILED_CODE) {
321
+ return error;
322
+ }
323
+ if (!isNativeCodexConnectionFailure(error))
324
+ return undefined;
325
+ return fixedFailure('native Codex HTTP connection failed', NATIVE_CODEX_CONNECTION_FAILED_CODE, { cause: error });
326
+ }
327
+ function unexpectedRedirect(error, depth = 0) {
328
+ if (depth > 4 || typeof error !== 'object' || error === null)
329
+ return false;
330
+ const candidate = error;
331
+ if (candidate.message === 'unexpected redirect')
332
+ return true;
333
+ return unexpectedRedirect(candidate.cause, depth + 1);
334
+ }
335
+ function retryable(error) {
336
+ return ['TRANSPORT', 'SERVER', 'TIMEOUT', 'STREAM_CLOSED'].includes(error.code);
337
+ }
338
+ function failedStepRetry(error) {
339
+ const failure = error.failure;
340
+ const providerRetryAfterMs = failure.providerRetryAfterMs === undefined
341
+ ? undefined : Math.min(failure.providerRetryAfterMs, DEFAULT_MAX_RETRY_DELAY_MS);
342
+ return fixedFailure(`native Codex response stream was interrupted: ${error.message}`, NATIVE_CODEX_STREAM_INTERRUPTED_CODE, {
343
+ cause: error,
344
+ ...(failure.status === undefined ? {} : { status: failure.status }),
345
+ ...(providerRetryAfterMs === undefined ? {} : { providerRetryAfterMs }),
346
+ ...(failure.requestId === undefined ? {} : { requestId: failure.requestId }),
347
+ });
348
+ }
349
+ /** HTTP Responses transport. It never retains a credential outside one attempt. */
350
+ export class NativeCodexHttpTransport {
351
+ options;
352
+ fetchImpl;
353
+ endpoint;
354
+ requestTimeoutMs;
355
+ idleTimeoutMs;
356
+ maxRetries;
357
+ initialDelayMs;
358
+ maxDelayMs;
359
+ maxBodyBytes;
360
+ constructor(options) {
361
+ this.options = options;
362
+ this.fetchImpl = options.fetch ?? fetch;
363
+ this.endpoint = nativeCodexEndpoint(options.endpoint ?? CODEX_RESPONSES_URL).toString();
364
+ this.requestTimeoutMs = safePositiveInteger(options.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS, 'request timeout');
365
+ this.idleTimeoutMs = safePositiveInteger(options.streamIdleTimeoutMs, DEFAULT_STREAM_IDLE_TIMEOUT_MS, 'stream idle timeout');
366
+ this.maxRetries = safeRetryCount(options.maxTransientRetries);
367
+ this.initialDelayMs = safePositiveInteger(options.initialRetryDelayMs, DEFAULT_INITIAL_RETRY_DELAY_MS, 'initial retry delay');
368
+ this.maxDelayMs = safePositiveInteger(options.maxRetryDelayMs, DEFAULT_MAX_RETRY_DELAY_MS, 'maximum retry delay');
369
+ this.maxBodyBytes = safePositiveInteger(options.maxRequestBodyBytes, DEFAULT_MAX_REQUEST_BODY_BYTES, 'request body limit');
370
+ }
371
+ retryDelay(retry, providerDelay) {
372
+ if (providerDelay !== undefined)
373
+ return Math.min(providerDelay, this.maxDelayMs);
374
+ const exponential = Math.min(this.initialDelayMs * (2 ** retry), this.maxDelayMs);
375
+ const random = this.options.random?.() ?? Math.random();
376
+ const jitter = 0.9 + Math.max(0, Math.min(1, random)) * 0.2;
377
+ return Math.max(1, Math.round(exponential * jitter));
378
+ }
379
+ async wait(retry, error, signal) {
380
+ const delay = this.retryDelay(retry, error.failure.providerRetryAfterMs);
381
+ await (this.options.sleep ?? sleep)(delay, signal);
382
+ }
383
+ async waitForConnection(delayMs, signal) {
384
+ this.options.warn?.(`native Codex network is unavailable; reconnecting in ${delayMs}ms`);
385
+ await (this.options.sleep ?? sleep)(delayMs, signal);
386
+ }
387
+ endpointUrl() { return this.endpoint; }
388
+ async prepare(generation, mode = {}) {
389
+ throwIfAborted(generation.signal);
390
+ if (generation.model.length === 0 || generation.model.length > 256
391
+ || /[\r\n\0;]/u.test(generation.model)) {
392
+ throw fixedFailure('native Codex model identity is invalid', 'INVALID_ARGS');
393
+ }
394
+ const messages = await resolveMessages(generation, this.options);
395
+ throwIfAborted(generation.signal);
396
+ const sessionId = generation.sessionId === undefined ? undefined : String(generation.sessionId);
397
+ if (sessionId !== undefined
398
+ && (sessionId.length === 0 || sessionId.length > 256 || /[\r\n\0]/u.test(sessionId))) {
399
+ throw fixedFailure('native Codex session identity is invalid', 'INVALID_ARGS');
400
+ }
401
+ const routingId = sessionId ?? (this.options.createRequestId?.() ?? randomUUID());
402
+ const routingHint = mode.serviceTier === undefined
403
+ ? `model=${generation.model}`
404
+ : `model=${generation.model};tier=${mode.serviceTier}`;
405
+ const wireOptions = sessionId === undefined
406
+ ? generation
407
+ : {
408
+ ...generation,
409
+ sessionId: stablePromptCacheKey(sessionId),
410
+ };
411
+ let request;
412
+ let body;
413
+ try {
414
+ request = codexRequestBody(wireOptions, messages, mode);
415
+ body = JSON.stringify(request);
416
+ }
417
+ catch (error) {
418
+ if (error instanceof LlmError)
419
+ throw error;
420
+ throw fixedFailure('native Codex request could not be encoded', 'INVALID_ARGS', { cause: error });
421
+ }
422
+ if (Buffer.byteLength(body) > this.maxBodyBytes) {
423
+ throw fixedFailure('native Codex request exceeded the size limit', 'REQUEST_TOO_LARGE');
424
+ }
425
+ return { generation, mode, request, body, routingId, routingHint };
426
+ }
427
+ async *stream(generation, mode = {}) {
428
+ const prepared = await this.prepare(generation, mode);
429
+ const { body, routingId, routingHint } = prepared;
430
+ let activeTurnState = mode.turnState;
431
+ if (activeTurnState !== undefined
432
+ && (activeTurnState.length === 0 || Buffer.byteLength(activeTurnState) > 4096
433
+ || /[\r\n\0]/u.test(activeTurnState))) {
434
+ throw fixedFailure('native Codex turn routing state is invalid', 'INVALID_ARGS');
435
+ }
436
+ let transientRetries = 0;
437
+ let connectionRetryDelayMs = INITIAL_CONNECTION_RETRY_DELAY_MS;
438
+ let recovered = false;
439
+ while (true) {
440
+ throwIfAborted(generation.signal);
441
+ const watchdog = attemptWatchdog(generation.signal, this.requestTimeoutMs, this.idleTimeoutMs);
442
+ let response;
443
+ let credential;
444
+ let fetching = false;
445
+ try {
446
+ credential = await this.options.resolveCredential(watchdog.signal);
447
+ throwIfAborted(watchdog.signal);
448
+ if (mode.serviceTier !== undefined
449
+ && (mode.authorityHash === undefined
450
+ || nativeCodexAuthorityHash(credential.accountId) !== mode.authorityHash)) {
451
+ throw fixedFailure('native Codex Fast capability authority changed before request', 'FAST_CAPABILITY_UNAVAILABLE');
452
+ }
453
+ fetching = true;
454
+ response = await this.fetchImpl(this.endpoint, {
455
+ method: 'POST',
456
+ redirect: 'error',
457
+ headers: {
458
+ authorization: `Bearer ${credential.accessToken}`,
459
+ 'chatgpt-account-id': credential.accountId,
460
+ originator: 'dsh',
461
+ 'session-id': routingId,
462
+ 'thread-id': routingId,
463
+ 'x-client-request-id': routingId,
464
+ 'x-codex-routing-hint': routingHint,
465
+ ...(activeTurnState === undefined ? {} : { 'x-codex-turn-state': activeTurnState }),
466
+ ...(generation.purpose === 'compaction' ? { 'x-openai-subagent': 'compact' } : {}),
467
+ accept: 'text/event-stream',
468
+ 'content-type': 'application/json',
469
+ ...attributionHeaders(),
470
+ },
471
+ body,
472
+ signal: watchdog.signal,
473
+ });
474
+ }
475
+ catch (error) {
476
+ const redirectRejected = fetching && unexpectedRedirect(error);
477
+ const connection = !redirectRejected
478
+ ? connectionFailure(error, watchdog, generation.signal) : undefined;
479
+ const failure = redirectRejected
480
+ ? fixedFailure('native Codex HTTP redirect was rejected', 'INVALID_REQUEST', { cause: error })
481
+ : connection ?? mappedFailure(error, watchdog, generation.signal);
482
+ watchdog.stop();
483
+ if (connection !== undefined) {
484
+ await this.waitForConnection(connectionRetryDelayMs, generation.signal);
485
+ connectionRetryDelayMs = Math.min(connectionRetryDelayMs * 2, MAX_CONNECTION_RETRY_DELAY_MS);
486
+ continue;
487
+ }
488
+ if (retryable(failure) && transientRetries < this.maxRetries) {
489
+ await this.wait(transientRetries++, failure, generation.signal);
490
+ continue;
491
+ }
492
+ throw failure;
493
+ }
494
+ publishCodexRateLimits(credential.accountId, parseCodexRateLimitHeaders(response.headers), this.options.onRateLimits, this.options.warn);
495
+ if (activeTurnState === undefined) {
496
+ const headerTurnState = boundedCodexTurnState(response.headers.get('x-codex-turn-state'));
497
+ if (headerTurnState !== undefined) {
498
+ activeTurnState = headerTurnState;
499
+ mode.captureTurnState?.(headerTurnState);
500
+ }
501
+ }
502
+ if (response.status === 401 && !recovered && this.options.recoverCredential !== undefined) {
503
+ try {
504
+ await response.body?.cancel(watchdog.signal.reason);
505
+ const changed = await this.options.recoverCredential(credential, watchdog.signal);
506
+ recovered = true;
507
+ watchdog.stop();
508
+ if (changed)
509
+ continue;
510
+ throw fixedFailure('native Codex rejected the configured credential', 'AUTH', errorFacts(response, this.maxDelayMs));
511
+ }
512
+ catch (error) {
513
+ const connection = connectionFailure(error, watchdog, generation.signal);
514
+ const failure = connection ?? mappedFailure(error, watchdog, generation.signal);
515
+ watchdog.stop();
516
+ if (connection !== undefined) {
517
+ await this.waitForConnection(connectionRetryDelayMs, generation.signal);
518
+ connectionRetryDelayMs = Math.min(connectionRetryDelayMs * 2, MAX_CONNECTION_RETRY_DELAY_MS);
519
+ continue;
520
+ }
521
+ throw failure;
522
+ }
523
+ }
524
+ if (!response.ok) {
525
+ let failure = await httpFailure(response, this.maxDelayMs, watchdog.signal);
526
+ if (generation.signal?.aborted || watchdog.timedOut()) {
527
+ failure = mappedFailure(failure, watchdog, generation.signal);
528
+ }
529
+ watchdog.stop();
530
+ if (retryable(failure) && transientRetries < this.maxRetries) {
531
+ await this.wait(transientRetries++, failure, generation.signal);
532
+ continue;
533
+ }
534
+ throw failure;
535
+ }
536
+ if (response.body === null) {
537
+ watchdog.stop();
538
+ throw fixedFailure('native Codex returned no response stream', EMPTY_RESPONSE_CODE);
539
+ }
540
+ watchdog.beginIdle();
541
+ let emitted = false;
542
+ let completed = false;
543
+ try {
544
+ for await (const chunk of streamResponses(response.body, {
545
+ signal: watchdog.signal,
546
+ onActivity: watchdog.pulse,
547
+ ...this.options.maxSseEventBytes === undefined
548
+ ? {}
549
+ : { maxEventBytes: this.options.maxSseEventBytes },
550
+ onMalformedEvent: () => {
551
+ this.options.warn?.('native Codex ignored a malformed SSE event');
552
+ },
553
+ onEvent: (event) => {
554
+ publishCodexResponseUsage(credential.accountId, parseCodexResponseUsageMetadata(event), this.options.onResponseUsage, this.options.warn);
555
+ const eventRateLimits = parseCodexRateLimitEvent(event);
556
+ publishCodexRateLimits(credential.accountId, eventRateLimits === undefined ? [] : [eventRateLimits], this.options.onRateLimits, this.options.warn);
557
+ const rawEvent = event;
558
+ publishCodexRateLimits(credential.accountId, parseCodexRateLimitHeaders(typeof rawEvent.headers === 'object' && rawEvent.headers !== null
559
+ ? rawEvent.headers : undefined), this.options.onRateLimits, this.options.warn);
560
+ const nextTurnState = codexResponseTurnState(event);
561
+ if (activeTurnState === undefined && nextTurnState !== undefined) {
562
+ activeTurnState = nextTurnState;
563
+ mode.captureTurnState?.(nextTurnState);
564
+ }
565
+ },
566
+ replayContext: {
567
+ provider: generation.provider,
568
+ model: mode.publicModel ?? generation.model,
569
+ },
570
+ })) {
571
+ emitted = true;
572
+ if (chunk.type === 'finish'
573
+ && ['stop', 'tool-calls', 'max-tokens'].includes(chunk.reason.kind))
574
+ completed = true;
575
+ yield chunk;
576
+ }
577
+ if (completed && this.options.onCompleted !== undefined) {
578
+ try {
579
+ this.options.onCompleted();
580
+ }
581
+ catch {
582
+ this.options.warn?.('native Codex usage refresh could not be scheduled');
583
+ }
584
+ }
585
+ return;
586
+ }
587
+ catch (error) {
588
+ const failure = mappedFailure(error, watchdog, generation.signal);
589
+ if (!emitted && retryable(failure) && transientRetries < this.maxRetries) {
590
+ await this.wait(transientRetries++, failure, generation.signal);
591
+ continue;
592
+ }
593
+ if (emitted && retryable(failure))
594
+ throw failedStepRetry(failure);
595
+ throw failure;
596
+ }
597
+ finally {
598
+ watchdog.stop();
599
+ }
600
+ }
601
+ }
602
+ }
@@ -0,0 +1,14 @@
1
+ export interface NativeCodexWebSocketRequestPlan {
2
+ payload: Record<string, unknown>;
3
+ incremental: boolean;
4
+ previousResponseId?: string;
5
+ }
6
+ /** One socket chain. Reset it whenever the socket reconnects or a request fails. */
7
+ export declare class NativeCodexWebSocketSessionState {
8
+ private completed;
9
+ private pending;
10
+ plan(request: Record<string, unknown>, allowEmptySuffix?: boolean): NativeCodexWebSocketRequestPlan;
11
+ prewarm(request: Record<string, unknown>): NativeCodexWebSocketRequestPlan;
12
+ complete(responseId: string, outputItems: readonly Record<string, unknown>[]): void;
13
+ reset(): void;
14
+ }