@pure01fx/dsh-openai-codex-auth 0.12.0 → 0.13.1

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.
@@ -1,5 +1,6 @@
1
1
  /** WebSocket v2 Responses transport with bounded session state and sticky HTTP fallback. */
2
2
  import { createHash } from 'node:crypto';
3
+ import { retryError, remainingRetryDelay, sleepRetry, jsonRetryAfterMs } from './retry-after.js';
3
4
  import { networkTelemetry, trackNetwork, observeNetworkOutput } from './network-telemetry.js';
4
5
  import { LlmError, ProviderRequestId, attributionHeaders, } from '@deepseek-ai/dsh-llm';
5
6
  import { nativeCodexAuthorityHash } from './catalog.js';
@@ -22,7 +23,7 @@ const INITIAL_CONNECTION_RETRY_DELAY_MS = 5_000;
22
23
  const MAX_CONNECTION_RETRY_DELAY_MS = 60_000;
23
24
  const MAX_TURN_STATE_BYTES = 4096;
24
25
  function failure(message, code, cause) {
25
- return new LlmError(message, code, cause === undefined ? undefined : { cause });
26
+ return retryError(message, code, cause === undefined ? undefined : { cause });
26
27
  }
27
28
  function reconnectable(code) {
28
29
  return [
@@ -36,9 +37,8 @@ function failedStepRetryable(code) {
36
37
  }
37
38
  function failedStepRetry(error) {
38
39
  const facts = error.failure;
39
- const providerRetryAfterMs = facts.providerRetryAfterMs === undefined
40
- ? undefined : Math.min(facts.providerRetryAfterMs, DEFAULT_MAX_RETRY_DELAY_MS);
41
- return new LlmError(`native Codex response stream was interrupted: ${error.message}`, NATIVE_CODEX_STREAM_INTERRUPTED_CODE, {
40
+ const providerRetryAfterMs = facts.providerRetryAfterMs;
41
+ return retryError(`native Codex response stream was interrupted: ${error.message}`, NATIVE_CODEX_STREAM_INTERRUPTED_CODE, {
42
42
  cause: error,
43
43
  ...(facts.status === undefined ? {} : { status: facts.status }),
44
44
  ...(providerRetryAfterMs === undefined ? {} : { providerRetryAfterMs }),
@@ -65,26 +65,6 @@ function retryCount(value) {
65
65
  }
66
66
  return resolved;
67
67
  }
68
- function sleep(delayMs, signal) {
69
- if (signal?.aborted) {
70
- return Promise.reject(failure('native Codex WebSocket retry wait was aborted', 'ABORTED'));
71
- }
72
- return new Promise((resolve, reject) => {
73
- const timer = setTimeout(done, delayMs);
74
- const abort = () => {
75
- clearTimeout(timer);
76
- signal?.removeEventListener('abort', abort);
77
- const reason = signal?.reason;
78
- reject(reason instanceof LlmError
79
- ? reason : failure('native Codex WebSocket retry wait was aborted', 'ABORTED'));
80
- };
81
- function done() {
82
- signal?.removeEventListener('abort', abort);
83
- resolve();
84
- }
85
- signal?.addEventListener('abort', abort, { once: true });
86
- });
87
- }
88
68
  function socketCredential(credential) {
89
69
  return createHash('sha256')
90
70
  .update(credential.accountId).update('\0').update(credential.accessToken)
@@ -123,15 +103,17 @@ function withRequestMetadata(request, turnState, responsesLite) {
123
103
  },
124
104
  };
125
105
  }
126
- function normalizedOutputItem(event) {
106
+ function normalizedOutputItem(event, translator) {
127
107
  if (event.type !== 'response.output_item.done' || event.item === undefined)
128
108
  return undefined;
129
109
  const item = event.item;
130
110
  const id = replayableItemId(item.id);
131
111
  if (item.type === 'message') {
112
+ const phase = item.id === undefined ? undefined : translator.messagePhase(item.id);
132
113
  return {
133
114
  type: 'message', ...(id === undefined ? {} : { id }), role: 'assistant',
134
115
  content: (item.content ?? []).map(part => ({ type: 'output_text', text: part.text })),
116
+ ...(phase === undefined ? {} : { phase }),
135
117
  };
136
118
  }
137
119
  if (item.type === 'reasoning') {
@@ -164,10 +146,9 @@ function eventHeader(event, name) {
164
146
  }
165
147
  function eventErrorFacts(event, status) {
166
148
  const requestId = eventHeader(event, 'x-request-id');
167
- const retryAfter = eventHeader(event, 'retry-after');
168
- const seconds = retryAfter === undefined ? undefined : Number(retryAfter);
169
- const retryMs = seconds !== undefined && Number.isFinite(seconds) && seconds > 0
170
- ? Math.min(seconds * 1_000, 120_000) : undefined;
149
+ const nested = typeof event.error === 'object' && event.error !== null
150
+ ? event.error : undefined;
151
+ const retryMs = jsonRetryAfterMs(nested?.headers) ?? jsonRetryAfterMs(event.headers);
171
152
  return {
172
153
  ...(status === undefined ? {} : { status }),
173
154
  ...(retryMs === undefined ? {} : { providerRetryAfterMs: retryMs }),
@@ -187,28 +168,44 @@ function eventFailure(event) {
187
168
  const facts = eventErrorFacts(event, status);
188
169
  if (providerCode === 'websocket_connection_limit_reached'
189
170
  || providerCode === 'previous_response_not_found') {
190
- return new LlmError('native Codex WebSocket requested a fresh connection', 'WS_RETRYABLE_RESET', facts);
171
+ return retryError('native Codex WebSocket requested a fresh connection', 'WS_RETRYABLE_RESET', facts);
191
172
  }
192
173
  if (providerCode === 'unauthorized' || providerCode === 'invalid_api_key' || status === 401) {
193
- return new LlmError('native Codex WebSocket authentication failed', 'WS_AUTH', facts);
174
+ return retryError('native Codex WebSocket authentication failed', 'WS_AUTH', facts);
194
175
  }
195
176
  if (status === 403)
196
- return new LlmError('native Codex WebSocket authorization failed', 'AUTH', facts);
177
+ return retryError('native Codex WebSocket authorization failed', 'AUTH', facts);
197
178
  const classified = responsesFailure(providerCode, typeof error.message === 'string' ? error.message : undefined);
198
179
  if (classified.code !== 'SERVER') {
199
180
  const delay = facts?.providerRetryAfterMs ?? classified.failure.providerRetryAfterMs;
200
- return new LlmError(classified.message, classified.code === 'RATE_LIMIT' ? 'RATE_LIMITED' : classified.code, {
181
+ return retryError(classified.message, classified.code === 'RATE_LIMIT' ? 'RATE_LIMITED' : classified.code, {
201
182
  ...facts,
202
- ...(delay === undefined ? {} : { providerRetryAfterMs: Math.min(delay, 120_000) }),
183
+ ...(delay === undefined ? {} : { providerRetryAfterMs: delay }),
203
184
  });
204
185
  }
205
186
  if (providerCode === 'usage_limit_reached' || status === 429) {
206
- return new LlmError('native Codex WebSocket rate limit was reached', 'RATE_LIMITED', facts);
187
+ return retryError('native Codex WebSocket rate limit was reached', 'RATE_LIMITED', facts);
207
188
  }
208
189
  if (status !== undefined && status >= 500) {
209
- return new LlmError('native Codex WebSocket service failed', 'WS_RETRYABLE', facts);
190
+ return retryError('native Codex WebSocket service failed', 'WS_RETRYABLE', facts);
210
191
  }
211
- return new LlmError('native Codex WebSocket request was rejected', 'INVALID_REQUEST', facts);
192
+ return retryError('native Codex WebSocket request was rejected', 'INVALID_REQUEST', facts);
193
+ }
194
+ /**
195
+ * One bounded log line naming the provider's own rejection, so an operator can
196
+ * see why the socket refused a request. The provider's free-form text stays in
197
+ * the log; it never becomes the provider-neutral error message.
198
+ */
199
+ function rejectionDiagnostic(event) {
200
+ const error = typeof event.error === 'object' && event.error !== null
201
+ ? event.error : event;
202
+ const code = typeof error.code === 'string' && error.code.length > 0 ? error.code : undefined;
203
+ const raw = typeof error.message === 'string' ? error.message : '';
204
+ const message = raw.replace(/\s+/g, ' ').trim().slice(0, 300);
205
+ const status = typeof event.status === 'number' ? ` status=${String(event.status)}` : '';
206
+ const detail = [code, message.length === 0 ? undefined : message]
207
+ .filter((part) => part !== undefined).join(': ');
208
+ return `native Codex WebSocket rejected the request${status}${detail.length === 0 ? '' : ` (${detail})`}`;
212
209
  }
213
210
  /** Prefer WebSocket v2; once safe retries exhaust, keep that DSH session on HTTP. */
214
211
  export class NativeCodexWebSocketTransport {
@@ -239,9 +236,9 @@ export class NativeCodexWebSocketTransport {
239
236
  this.maxRetryDelayMs = boundedPositive(options.maxRetryDelayMs, DEFAULT_MAX_RETRY_DELAY_MS, 120_000, 'WebSocket maximum retry delay');
240
237
  }
241
238
  retryDelay(retry, error) {
242
- const providerDelay = error.failure.providerRetryAfterMs;
239
+ const providerDelay = remainingRetryDelay(error);
243
240
  if (providerDelay !== undefined)
244
- return Math.min(providerDelay, this.maxRetryDelayMs);
241
+ return providerDelay;
245
242
  const exponential = Math.min(this.initialRetryDelayMs * (2 ** retry), this.maxRetryDelayMs);
246
243
  const random = this.options.random?.() ?? Math.random();
247
244
  const jitter = 0.9 + Math.max(0, Math.min(1, random)) * 0.2;
@@ -250,12 +247,12 @@ export class NativeCodexWebSocketTransport {
250
247
  async wait(retry, error, signal, telemetry) {
251
248
  const delay = this.retryDelay(retry, error);
252
249
  telemetry.retry(delay);
253
- await (this.options.sleep ?? sleep)(delay, signal);
250
+ await (this.options.sleep ?? sleepRetry)(delay, signal);
254
251
  }
255
252
  async waitForConnection(delayMs, signal, telemetry) {
256
253
  telemetry.retry(delayMs, 'connection');
257
254
  this.options.warn?.(`native Codex network is unavailable; reconnecting in ${delayMs}ms`);
258
- await (this.options.sleep ?? sleep)(delayMs, signal);
255
+ await (this.options.sleep ?? sleepRetry)(delayMs, signal);
259
256
  }
260
257
  closeEntry(entry) {
261
258
  entry.socket?.close();
@@ -418,14 +415,17 @@ export class NativeCodexWebSocketTransport {
418
415
  publishCodexRateLimits(accountId, parseCodexRateLimitHeaders(typeof rawEvent.headers === 'object' && rawEvent.headers !== null
419
416
  ? rawEvent.headers : undefined), this.options.onRateLimits, this.options.warn);
420
417
  const wrapped = eventFailure(rawEvent);
421
- if (wrapped !== undefined)
418
+ if (wrapped !== undefined) {
419
+ this.options.warn?.(rejectionDiagnostic(rawEvent));
422
420
  throw wrapped;
421
+ }
423
422
  if (entry.turnState === undefined) {
424
423
  const nextTurnState = codexResponseTurnState(event);
425
424
  if (nextTurnState !== undefined)
426
425
  entry.turnState = nextTurnState;
427
426
  }
428
- const output = normalizedOutputItem(event);
427
+ const chunks = translator.push(event);
428
+ const output = normalizedOutputItem(event, translator);
429
429
  if (output !== undefined)
430
430
  outputItems.push(output);
431
431
  if (event.type === 'response.completed') {
@@ -436,7 +436,6 @@ export class NativeCodexWebSocketTransport {
436
436
  const responseId = typeof response?.id === 'string' ? response.id : '';
437
437
  entry.protocol.complete(responseId, outputItems);
438
438
  }
439
- const chunks = translator.push(event);
440
439
  if (!prewarm)
441
440
  for (const chunk of chunks)
442
441
  yield chunk;
@@ -497,10 +496,16 @@ export class NativeCodexWebSocketTransport {
497
496
  throw failure('native Codex WebSocket transport was disposed', 'DISPOSED');
498
497
  const key = sessionKey(generation, prepared.routingId);
499
498
  const entry = this.entry(key);
500
- if (entry.busy)
501
- throw failure('native Codex WebSocket session already has an active request', 'CONCURRENT_REQUEST');
502
- if (this.active.size >= this.maxSessions) {
503
- throw failure('native Codex WebSocket active session limit was reached', 'WS_SESSION_LIMIT');
499
+ // A second request for the same DSH session (the session-title call racing
500
+ // the first step, for example) cannot share one socket chain, and a
501
+ // saturated pool has no socket to lend. Both still have a correct transport
502
+ // over HTTP/SSE, so neither may fail the turn.
503
+ if (entry.busy || this.active.size >= this.maxSessions) {
504
+ this.options.warn?.(entry.busy
505
+ ? 'native Codex WebSocket session already has an active request; serving this request over HTTP/SSE'
506
+ : 'native Codex WebSocket active session limit was reached; serving this request over HTTP/SSE');
507
+ yield* this.http.stream(activeGeneration, mode, telemetry);
508
+ return;
504
509
  }
505
510
  entry.busy = true;
506
511
  this.active.set(entry, lifecycle);
@@ -564,6 +569,16 @@ export class NativeCodexWebSocketTransport {
564
569
  throw failedStepRetry(failureValue);
565
570
  throw failureValue;
566
571
  }
572
+ // A rejected request is not a transient socket condition: retrying the
573
+ // socket repeats the rejection. The same payload has a correct
574
+ // transport over HTTP/SSE, so keep this DSH session on HTTP from now on
575
+ // instead of failing every later turn.
576
+ if (failureValue.code === 'INVALID_REQUEST') {
577
+ entry.disabled = true;
578
+ yield* this.http.stream(activeGeneration, fallbackMode(), telemetry);
579
+ requestCompleted = true;
580
+ return;
581
+ }
567
582
  // Credential resolution may need the network itself, so wait until it recovers.
568
583
  // Once a credential is available, however, a WebSocket-only outage must consume
569
584
  // the bounded reconnect budget and then fall back to HTTP/SSE instead of leaving
@@ -617,7 +632,11 @@ export class NativeCodexWebSocketTransport {
617
632
  }
618
633
  if (failureValue.code === 'WS_UPGRADE_REQUIRED' || retryable) {
619
634
  entry.disabled = true;
620
- telemetry.retry(0);
635
+ if (remainingRetryDelay(failureValue) !== undefined) {
636
+ await this.wait(reconnects, failureValue, signal, telemetry);
637
+ }
638
+ else
639
+ telemetry.retry(0);
621
640
  yield* this.http.stream(activeGeneration, fallbackMode(), telemetry);
622
641
  requestCompleted = true;
623
642
  return;
package/lib/replay.d.ts CHANGED
@@ -2,10 +2,13 @@
2
2
  import { type ContentBlock } from '@deepseek-ai/dsh-llm';
3
3
  export declare const NATIVE_CODEX_REPLAY_KIND = "openai-codex-native.responses-replay";
4
4
  export declare const NATIVE_CODEX_REPLAY_VERSION = 1;
5
+ export type CodexMessagePhase = 'commentary' | 'partial_answer' | 'final_answer';
6
+ export declare function isCodexMessagePhase(value: unknown): value is CodexMessagePhase;
5
7
  export type NativeCodexReplayDescriptor = {
6
8
  type: 'message';
7
9
  id?: string;
8
10
  blocks: number[];
11
+ phase?: CodexMessagePhase;
9
12
  } | {
10
13
  type: 'reasoning';
11
14
  id?: string;
package/lib/replay.js CHANGED
@@ -2,6 +2,9 @@
2
2
  import { LlmError } from '@deepseek-ai/dsh-llm';
3
3
  export const NATIVE_CODEX_REPLAY_KIND = 'openai-codex-native.responses-replay';
4
4
  export const NATIVE_CODEX_REPLAY_VERSION = 1;
5
+ export function isCodexMessagePhase(value) {
6
+ return value === 'commentary' || value === 'partial_answer' || value === 'final_answer';
7
+ }
5
8
  function failure(message, code = 'INVALID_REPLAY_STATE') {
6
9
  return new LlmError(message, code);
7
10
  }
@@ -55,10 +58,12 @@ function parseDescriptor(value) {
55
58
  }
56
59
  if (row.type === 'message') {
57
60
  const blocks = validateBlockArray(row.blocks);
58
- if (blocks === undefined || !onlyKeys(row, ['type', 'id', 'blocks'])) {
61
+ if (blocks === undefined || (row.phase !== undefined && !isCodexMessagePhase(row.phase))
62
+ || !onlyKeys(row, ['type', 'id', 'blocks', 'phase'])) {
59
63
  throw failure('native Codex message replay descriptor is invalid');
60
64
  }
61
- return { type: 'message', ...(id === undefined ? {} : { id }), blocks };
65
+ return { type: 'message', ...(id === undefined ? {} : { id }), blocks,
66
+ ...(row.phase === undefined ? {} : { phase: row.phase }) };
62
67
  }
63
68
  if (row.type === 'reasoning') {
64
69
  const blocks = validateBlockArray(row.blocks);
@@ -179,6 +184,7 @@ export function replayAssistantInput(content, source) {
179
184
  input.push({
180
185
  type: 'message', ...(item.id === undefined ? {} : { id: item.id }),
181
186
  role: 'assistant', content: parts,
187
+ ...(item.phase === undefined ? {} : { phase: item.phase }),
182
188
  });
183
189
  }
184
190
  else if (item.type === 'reasoning') {
@@ -1,6 +1,6 @@
1
1
  import { ToolCallId, LlmError, type ContentBlock, type GenerateOptions, type StreamChunk, type TokenUsage, type ToolSchema } from '@deepseek-ai/dsh-llm';
2
2
  import { type ParseSseOptions } from './sse.js';
3
- import { type NativeCodexReplaySource } from './replay.js';
3
+ import { type CodexMessagePhase, type NativeCodexReplaySource } from './replay.js';
4
4
  export declare const DEFAULT_CODEX_INSTRUCTIONS = "You are Codex, an AI coding agent. Help the user with software engineering tasks.";
5
5
  export type CodexImageDetail = 'auto' | 'low' | 'high' | 'original';
6
6
  export interface ResolvedImagePart {
@@ -53,7 +53,7 @@ export interface ResponsesUsage {
53
53
  /** Map provider totals to DSH's strict disjoint token counts. */
54
54
  export declare function mapResponsesUsage(usage: ResponsesUsage): TokenUsage;
55
55
  /** Classify in-band failure data without reflecting provider text. */
56
- export declare function responsesFailure(code?: string, message?: string): LlmError;
56
+ export declare function responsesFailure(code?: string, message?: string, headers?: unknown): LlmError;
57
57
  interface ResponsesOutputItem {
58
58
  type?: string;
59
59
  id?: string;
@@ -64,6 +64,7 @@ interface ResponsesOutputItem {
64
64
  encrypted_content?: string;
65
65
  summary?: unknown[];
66
66
  status?: string;
67
+ phase?: CodexMessagePhase | null;
67
68
  content?: Array<{
68
69
  type?: string;
69
70
  text?: string;
@@ -86,6 +87,7 @@ export interface ResponsesStreamEvent {
86
87
  error?: {
87
88
  code?: string;
88
89
  message?: string;
90
+ headers?: unknown;
89
91
  };
90
92
  incomplete_details?: {
91
93
  reason?: string;
@@ -103,11 +105,15 @@ export declare class ResponsesStreamTranslator {
103
105
  private readonly replayContext?;
104
106
  private readonly blocks;
105
107
  private readonly order;
108
+ private readonly messagePhases;
106
109
  private readonly replayCapture;
107
110
  private nextIndex;
108
111
  private sawToolCall;
109
112
  terminated: boolean;
110
113
  constructor(replayContext?: ResponsesReplayContext | undefined);
114
+ /** The same effective phase feeds durable replay and WebSocket prefix comparison. */
115
+ messagePhase(id: string): CodexMessagePhase | undefined;
116
+ private captureMessagePhase;
111
117
  private append;
112
118
  private fill;
113
119
  private open;
package/lib/responses.js CHANGED
@@ -2,7 +2,8 @@
2
2
  import { createHash } from 'node:crypto';
3
3
  import { ToolCallId, CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError, QUOTA_EXCEEDED_CODE, } from '@deepseek-ai/dsh-llm';
4
4
  import { parseSse } from './sse.js';
5
- import { NativeCodexReplayCapture, replayAssistantInput, replayableItemId, } from './replay.js';
5
+ import { jsonRetryAfterMs, retryError } from './retry-after.js';
6
+ import { NativeCodexReplayCapture, isCodexMessagePhase, replayAssistantInput, replayableItemId, } from './replay.js';
6
7
  export const DEFAULT_CODEX_INSTRUCTIONS = 'You are Codex, an AI coding agent. Help the user with software engineering tasks.';
7
8
  const CALL_ID_MAX_LENGTH = 64;
8
9
  const CALL_ID_PREFIX = 'call_';
@@ -211,11 +212,21 @@ export function codexRequestBody(options, messages, mode = {}) {
211
212
  ...mode.serviceTier === undefined ? {} : { service_tier: mode.serviceTier },
212
213
  ...options.sessionId === undefined ? {} : { prompt_cache_key: String(options.sessionId) },
213
214
  };
215
+ const baseInstructions = mode.responsesLite?.instructionsTemplate ?? instructions;
216
+ const prefixNamespace = uuidBytes(uuidV5(UUID_NAMESPACE_OID, String(options.sessionId ?? options.model)));
217
+ const instructionPrefix = baseInstructions.length === 0 ? [] : [{
218
+ type: 'message',
219
+ id: `msg_${uuidV5(prefixNamespace, baseInstructions)}`,
220
+ role: 'developer',
221
+ content: [{ type: 'input_text', text: baseInstructions }],
222
+ internal_chat_message_metadata_passthrough: {
223
+ content_item_kinds: ['model.base_instructions'],
224
+ },
225
+ }];
214
226
  if (mode.responsesLite === undefined) {
215
227
  return {
216
228
  ...common,
217
- instructions,
218
- input,
229
+ input: [...instructionPrefix, ...input],
219
230
  ...options.tools !== undefined && options.tools.length > 0
220
231
  ? { tools: toResponsesTools(options.tools) } : {},
221
232
  parallel_tool_calls: true,
@@ -224,7 +235,6 @@ export function codexRequestBody(options, messages, mode = {}) {
224
235
  };
225
236
  }
226
237
  const tools = toResponsesLiteTools(options.tools ?? []);
227
- const baseInstructions = mode.responsesLite.instructionsTemplate ?? instructions;
228
238
  const contextualInstructions = mode.responsesLite.instructionsTemplate !== undefined
229
239
  && resolved.instructions !== undefined && resolved.instructions !== baseInstructions
230
240
  ? [{
@@ -232,21 +242,12 @@ export function codexRequestBody(options, messages, mode = {}) {
232
242
  content: [{ type: 'input_text', text: resolved.instructions }],
233
243
  }]
234
244
  : [];
235
- const prefixNamespace = uuidBytes(uuidV5(UUID_NAMESPACE_OID, String(options.sessionId ?? options.model)));
236
245
  const prefix = [{
237
246
  type: 'additional_tools',
238
247
  id: `at_${uuidV5(prefixNamespace, JSON.stringify(tools))}`,
239
248
  role: 'developer',
240
249
  tools,
241
- }, {
242
- type: 'message',
243
- id: `msg_${uuidV5(prefixNamespace, baseInstructions)}`,
244
- role: 'developer',
245
- content: [{ type: 'input_text', text: baseInstructions }],
246
- internal_chat_message_metadata_passthrough: {
247
- content_item_kinds: ['model.base_instructions'],
248
- },
249
- }];
250
+ }, ...instructionPrefix];
250
251
  return {
251
252
  ...common,
252
253
  input: [...prefix, ...contextualInstructions, ...input],
@@ -293,10 +294,11 @@ function retryDelay(message) {
293
294
  const value = Number(match[1]);
294
295
  if (!Number.isFinite(value) || value <= 0)
295
296
  return undefined;
296
- return Math.min(match[2]?.toLowerCase() === 'ms' ? value : value * 1000, 600_000);
297
+ const delay = match[2]?.toLowerCase() === 'ms' ? value : value * 1000;
298
+ return Number.isFinite(delay) && delay <= Number.MAX_SAFE_INTEGER ? delay : undefined;
297
299
  }
298
300
  /** Classify in-band failure data without reflecting provider text. */
299
- export function responsesFailure(code, message) {
301
+ export function responsesFailure(code, message, headers) {
300
302
  const detail = `${code ?? ''} ${message ?? ''}`;
301
303
  if (code === 'context_length_exceeded' || code === 'context_window_exceeded'
302
304
  || isContextWindowExceededError(detail)) {
@@ -308,8 +310,8 @@ export function responsesFailure(code, message) {
308
310
  return fixedError('native Codex account quota is exhausted', QUOTA_EXCEEDED_CODE);
309
311
  }
310
312
  if (code === 'rate_limit_exceeded' || code === 'slow_down') {
311
- const delay = retryDelay(message);
312
- return new LlmError('native Codex request was rate limited', 'RATE_LIMIT', {
313
+ const delay = jsonRetryAfterMs(headers) ?? retryDelay(message);
314
+ return retryError('native Codex request was rate limited', 'RATE_LIMIT', {
313
315
  ...delay === undefined ? {} : { providerRetryAfterMs: delay },
314
316
  });
315
317
  }
@@ -319,7 +321,10 @@ export function responsesFailure(code, message) {
319
321
  if (code === 'bio_policy') {
320
322
  return fixedError('native Codex rejected the request', 'INVALID_REQUEST');
321
323
  }
322
- return fixedError('native Codex reported a failed response', 'SERVER');
324
+ const delay = jsonRetryAfterMs(headers);
325
+ return retryError('native Codex reported a failed response', 'SERVER', {
326
+ ...delay === undefined ? {} : { providerRetryAfterMs: delay },
327
+ });
323
328
  }
324
329
  function eventItemId(event) {
325
330
  if (typeof event.item_id === 'string' && event.item_id.length > 0)
@@ -343,6 +348,7 @@ export class ResponsesStreamTranslator {
343
348
  replayContext;
344
349
  blocks = new Map();
345
350
  order = [];
351
+ messagePhases = new Map();
346
352
  replayCapture;
347
353
  nextIndex = 0;
348
354
  sawToolCall = false;
@@ -353,6 +359,18 @@ export class ResponsesStreamTranslator {
353
359
  ? undefined
354
360
  : new NativeCodexReplayCapture(replayContext.provider, replayContext.model);
355
361
  }
362
+ /** The same effective phase feeds durable replay and WebSocket prefix comparison. */
363
+ messagePhase(id) {
364
+ return this.messagePhases.get(id);
365
+ }
366
+ captureMessagePhase(item) {
367
+ if (item.phase === undefined || item.phase === null)
368
+ return;
369
+ if (!isCodexMessagePhase(item.phase) || typeof item.id !== 'string' || item.id.length === 0) {
370
+ throw fixedError('native Codex message phase is invalid', 'MALFORMED_RESPONSE');
371
+ }
372
+ this.messagePhases.set(item.id, item.phase);
373
+ }
356
374
  append(block, delta) {
357
375
  block.text += delta;
358
376
  }
@@ -400,6 +418,8 @@ export class ResponsesStreamTranslator {
400
418
  switch (event.type) {
401
419
  case 'response.output_item.added': {
402
420
  const item = event.item;
421
+ if (item?.type === 'message')
422
+ this.captureMessagePhase(item);
403
423
  if (item?.type !== 'function_call')
404
424
  return chunks;
405
425
  if (item.id === undefined || item.id.length === 0
@@ -483,6 +503,8 @@ export class ResponsesStreamTranslator {
483
503
  });
484
504
  }
485
505
  else if (item.type === 'message') {
506
+ this.captureMessagePhase(item);
507
+ const phase = this.messagePhase(item.id);
486
508
  if (!Array.isArray(item.content)) {
487
509
  throw fixedError('native Codex message item has invalid content', 'MALFORMED_RESPONSE');
488
510
  }
@@ -507,6 +529,7 @@ export class ResponsesStreamTranslator {
507
529
  if (this.replayContext !== undefined)
508
530
  this.replayCapture?.add({
509
531
  type: 'message', ...(replayId === undefined ? {} : { id: replayId }), blocks: refs,
532
+ ...(phase === undefined ? {} : { phase }),
510
533
  });
511
534
  }
512
535
  else if (item.type === 'reasoning') {
@@ -595,7 +618,7 @@ export class ResponsesStreamTranslator {
595
618
  return chunks;
596
619
  }
597
620
  case 'response.failed':
598
- throw responsesFailure(event.response?.error?.code, event.response?.error?.message);
621
+ throw responsesFailure(event.response?.error?.code, event.response?.error?.message, event.response?.error?.headers);
599
622
  case 'error':
600
623
  throw responsesFailure(event.code, event.message);
601
624
  default:
@@ -0,0 +1,9 @@
1
+ /** Server advice is separate from the client's bounded exponential backoff. */
2
+ import { LlmError } from '@deepseek-ai/dsh-llm';
3
+ export declare function retryAfterMs(raw: unknown): number | undefined;
4
+ export declare function jsonRetryAfterMs(headers: unknown): number | undefined;
5
+ export declare function retryError(message: string, code: string, options?: ConstructorParameters<typeof LlmError>[2]): LlmError;
6
+ export declare function inheritRetryDeadline(error: LlmError, source: LlmError): LlmError;
7
+ export declare function remainingRetryDelay(error: LlmError): number | undefined;
8
+ /** Node timers overflow above 2^31-1; chunk long waits without shortening advice. */
9
+ export declare function sleepRetry(delay: number, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1,109 @@
1
+ /** Server advice is separate from the client's bounded exponential backoff. */
2
+ import { LlmError } from '@deepseek-ai/dsh-llm';
3
+ const deadlines = new WeakMap();
4
+ function httpDate(value) {
5
+ const standard = /^([A-Za-z]{3}), ([0-9]{2}) ([A-Za-z]{3}) ([0-9]{4}) ([0-9]{2}:[0-9]{2}:[0-9]{2}) GMT$/.exec(value);
6
+ const obsolete = /^([A-Za-z]+), ([0-9]{2})-([A-Za-z]{3})-([0-9]{2}) ([0-9]{2}:[0-9]{2}:[0-9]{2}) GMT$/.exec(value);
7
+ const ascii = /^([A-Za-z]{3}) ([A-Za-z]{3}) ([ 0-9][0-9]) ([0-9]{2}:[0-9]{2}:[0-9]{2}) ([0-9]{4})$/.exec(value);
8
+ const parts = standard ?? obsolete;
9
+ const weekday = parts?.[1] ?? ascii?.[1];
10
+ const day = parts?.[2] ?? ascii?.[3];
11
+ const month = parts?.[3] ?? ascii?.[2];
12
+ const year = parts?.[4] ?? ascii?.[5];
13
+ const time = parts?.[5] ?? ascii?.[4];
14
+ if (!weekday || !day || !month || !year || !time)
15
+ return undefined;
16
+ const timestamp = Date.parse(value.endsWith('GMT') ? value : value + ' GMT');
17
+ if (!Number.isFinite(timestamp))
18
+ return undefined;
19
+ const date = new Date(timestamp);
20
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
21
+ const weekdays = obsolete
22
+ ? ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
23
+ : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
24
+ // Date.parse normalizes impossible dates; HTTP header parsing must reject them.
25
+ if (date.getUTCDate() !== Number(day) || months[date.getUTCMonth()] !== month
26
+ || weekdays[date.getUTCDay()] !== weekday
27
+ || (obsolete ? date.getUTCFullYear() % 100 : date.getUTCFullYear()) !== Number(year)
28
+ || date.toISOString().slice(11, 19) !== time)
29
+ return undefined;
30
+ return timestamp;
31
+ }
32
+ export function retryAfterMs(raw) {
33
+ if (typeof raw !== 'string' && typeof raw !== 'number')
34
+ return undefined;
35
+ const text = String(raw);
36
+ if (/[^\t\x20-\x7e]/.test(text))
37
+ return undefined;
38
+ const value = text.trim();
39
+ let delay;
40
+ if (/^\d+$/.test(value))
41
+ delay = Number(value) * 1000;
42
+ else {
43
+ const timestamp = httpDate(value);
44
+ if (timestamp === undefined)
45
+ return undefined;
46
+ delay = Math.max(0, timestamp - Date.now());
47
+ }
48
+ return Number.isSafeInteger(delay) && delay >= 0 ? delay : undefined;
49
+ }
50
+ export function jsonRetryAfterMs(headers) {
51
+ if (typeof headers !== 'object' || headers === null || Array.isArray(headers))
52
+ return undefined;
53
+ let value;
54
+ // serde_json's ordered map inserts later case variants over earlier ones.
55
+ for (const key of Object.keys(headers).sort()) {
56
+ const raw = headers[key];
57
+ if (key.toLowerCase() === 'retry-after'
58
+ && (typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean')
59
+ && !/[^\t\x20-\x7e]/.test(String(raw)))
60
+ value = raw;
61
+ }
62
+ return retryAfterMs(value);
63
+ }
64
+ export function retryError(message, code, options) {
65
+ const delay = options?.providerRetryAfterMs;
66
+ // The host permits only positive duration metadata; retain zero advice locally.
67
+ const normalized = { ...options };
68
+ if (delay === 0)
69
+ delete normalized.providerRetryAfterMs;
70
+ const error = new LlmError(message, code, normalized);
71
+ const inherited = options?.cause instanceof LlmError ? deadlines.get(options.cause) : undefined;
72
+ if (inherited !== undefined || delay !== undefined) {
73
+ deadlines.set(error, inherited ?? performance.now() + delay);
74
+ }
75
+ return error;
76
+ }
77
+ export function inheritRetryDeadline(error, source) {
78
+ const deadline = deadlines.get(source);
79
+ if (deadline !== undefined)
80
+ deadlines.set(error, deadline);
81
+ return error;
82
+ }
83
+ export function remainingRetryDelay(error) {
84
+ let deadline = deadlines.get(error);
85
+ if (deadline === undefined) {
86
+ const delay = error.failure.providerRetryAfterMs;
87
+ if (delay === undefined)
88
+ return undefined;
89
+ deadline = performance.now() + delay;
90
+ deadlines.set(error, deadline);
91
+ }
92
+ return Math.max(0, Math.ceil(deadline - performance.now()));
93
+ }
94
+ /** Node timers overflow above 2^31-1; chunk long waits without shortening advice. */
95
+ export async function sleepRetry(delay, signal) {
96
+ const deadline = performance.now() + delay;
97
+ do {
98
+ if (signal?.aborted)
99
+ throw signal.reason instanceof LlmError ? signal.reason : new LlmError('native Codex retry wait was aborted', 'ABORTED');
100
+ const remaining = Math.max(0, Math.ceil(deadline - performance.now()));
101
+ if (remaining === 0)
102
+ return;
103
+ await new Promise((resolve, reject) => {
104
+ const abort = () => { clearTimeout(timer); reject(signal?.reason instanceof LlmError ? signal.reason : new LlmError('native Codex retry wait was aborted', 'ABORTED')); };
105
+ const timer = setTimeout(() => { signal?.removeEventListener('abort', abort); resolve(); }, Math.min(remaining, 2_147_483_647));
106
+ signal?.addEventListener('abort', abort, { once: true });
107
+ });
108
+ } while (true);
109
+ }
package/lib/upstream.d.ts CHANGED
@@ -4,5 +4,5 @@ export declare const TRACKED_CODEX_COMMIT = "ddf04ad26789d040f9ef6a96736f76602e3
4
4
  export declare const TRACKED_CODEX_RELEASE = "main@ddf04ad";
5
5
  /** Independently audited cloud search/image contracts; not local Codex executor parity. */
6
6
  export declare const TRACKED_CODEX_CLOUD_COMMIT = "b348fc26674189f758d5941cdab3f78f258b2aa7";
7
- /** Whole stable release version sent to the Codex model-catalog endpoint. */
8
- export declare const CODEX_CLIENT_VERSION = "0.155.0";
7
+ /** Whole client version sent to the catalog; live gating can exceed bundled model minima. */
8
+ export declare const CODEX_CLIENT_VERSION = "0.162.0";
package/lib/upstream.js CHANGED
@@ -4,5 +4,5 @@ export const TRACKED_CODEX_COMMIT = 'ddf04ad26789d040f9ef6a96736f76602e35a6cc';
4
4
  export const TRACKED_CODEX_RELEASE = 'main@ddf04ad';
5
5
  /** Independently audited cloud search/image contracts; not local Codex executor parity. */
6
6
  export const TRACKED_CODEX_CLOUD_COMMIT = 'b348fc26674189f758d5941cdab3f78f258b2aa7';
7
- /** Whole stable release version sent to the Codex model-catalog endpoint. */
8
- export const CODEX_CLIENT_VERSION = '0.155.0';
7
+ /** Whole client version sent to the catalog; live gating can exceed bundled model minima. */
8
+ export const CODEX_CLIENT_VERSION = '0.162.0';
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "OpenAI Codex Auth",
4
+ "description": "Native ChatGPT Codex provider with device-code login, its own model catalog and usage reporting, wired into the DSH Web GUI from the same origin."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "OpenAI Codex 认证",
4
+ "description": "原生 ChatGPT Codex 提供方:设备码登录、自带的模型目录与用量统计,与 DSH Web GUI 同源集成。"
5
+ }
6
+ }