@pure01fx/dsh-openai-codex-auth 0.11.2 → 0.13.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.
@@ -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)
@@ -98,7 +78,8 @@ function sessionKey(generation, routingId) {
98
78
  function turnKey(generation) {
99
79
  for (let index = generation.messages.length - 1; index >= 0; index--) {
100
80
  const message = generation.messages[index];
101
- if (message?.source.kind === 'user') {
81
+ // RequestUserInput doesn't have source or id
82
+ if (message && 'source' in message && message.source?.kind === 'user' && 'id' in message) {
102
83
  return createHash('sha256').update(String(message.id)).digest('base64url');
103
84
  }
104
85
  }
@@ -122,15 +103,17 @@ function withRequestMetadata(request, turnState, responsesLite) {
122
103
  },
123
104
  };
124
105
  }
125
- function normalizedOutputItem(event) {
106
+ function normalizedOutputItem(event, translator) {
126
107
  if (event.type !== 'response.output_item.done' || event.item === undefined)
127
108
  return undefined;
128
109
  const item = event.item;
129
110
  const id = replayableItemId(item.id);
130
111
  if (item.type === 'message') {
112
+ const phase = item.id === undefined ? undefined : translator.messagePhase(item.id);
131
113
  return {
132
114
  type: 'message', ...(id === undefined ? {} : { id }), role: 'assistant',
133
115
  content: (item.content ?? []).map(part => ({ type: 'output_text', text: part.text })),
116
+ ...(phase === undefined ? {} : { phase }),
134
117
  };
135
118
  }
136
119
  if (item.type === 'reasoning') {
@@ -163,10 +146,9 @@ function eventHeader(event, name) {
163
146
  }
164
147
  function eventErrorFacts(event, status) {
165
148
  const requestId = eventHeader(event, 'x-request-id');
166
- const retryAfter = eventHeader(event, 'retry-after');
167
- const seconds = retryAfter === undefined ? undefined : Number(retryAfter);
168
- const retryMs = seconds !== undefined && Number.isFinite(seconds) && seconds > 0
169
- ? 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);
170
152
  return {
171
153
  ...(status === undefined ? {} : { status }),
172
154
  ...(retryMs === undefined ? {} : { providerRetryAfterMs: retryMs }),
@@ -186,28 +168,28 @@ function eventFailure(event) {
186
168
  const facts = eventErrorFacts(event, status);
187
169
  if (providerCode === 'websocket_connection_limit_reached'
188
170
  || providerCode === 'previous_response_not_found') {
189
- 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);
190
172
  }
191
173
  if (providerCode === 'unauthorized' || providerCode === 'invalid_api_key' || status === 401) {
192
- return new LlmError('native Codex WebSocket authentication failed', 'WS_AUTH', facts);
174
+ return retryError('native Codex WebSocket authentication failed', 'WS_AUTH', facts);
193
175
  }
194
176
  if (status === 403)
195
- return new LlmError('native Codex WebSocket authorization failed', 'AUTH', facts);
177
+ return retryError('native Codex WebSocket authorization failed', 'AUTH', facts);
196
178
  const classified = responsesFailure(providerCode, typeof error.message === 'string' ? error.message : undefined);
197
179
  if (classified.code !== 'SERVER') {
198
180
  const delay = facts?.providerRetryAfterMs ?? classified.failure.providerRetryAfterMs;
199
- 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, {
200
182
  ...facts,
201
- ...(delay === undefined ? {} : { providerRetryAfterMs: Math.min(delay, 120_000) }),
183
+ ...(delay === undefined ? {} : { providerRetryAfterMs: delay }),
202
184
  });
203
185
  }
204
186
  if (providerCode === 'usage_limit_reached' || status === 429) {
205
- 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);
206
188
  }
207
189
  if (status !== undefined && status >= 500) {
208
- return new LlmError('native Codex WebSocket service failed', 'WS_RETRYABLE', facts);
190
+ return retryError('native Codex WebSocket service failed', 'WS_RETRYABLE', facts);
209
191
  }
210
- return new LlmError('native Codex WebSocket request was rejected', 'INVALID_REQUEST', facts);
192
+ return retryError('native Codex WebSocket request was rejected', 'INVALID_REQUEST', facts);
211
193
  }
212
194
  /** Prefer WebSocket v2; once safe retries exhaust, keep that DSH session on HTTP. */
213
195
  export class NativeCodexWebSocketTransport {
@@ -238,9 +220,9 @@ export class NativeCodexWebSocketTransport {
238
220
  this.maxRetryDelayMs = boundedPositive(options.maxRetryDelayMs, DEFAULT_MAX_RETRY_DELAY_MS, 120_000, 'WebSocket maximum retry delay');
239
221
  }
240
222
  retryDelay(retry, error) {
241
- const providerDelay = error.failure.providerRetryAfterMs;
223
+ const providerDelay = remainingRetryDelay(error);
242
224
  if (providerDelay !== undefined)
243
- return Math.min(providerDelay, this.maxRetryDelayMs);
225
+ return providerDelay;
244
226
  const exponential = Math.min(this.initialRetryDelayMs * (2 ** retry), this.maxRetryDelayMs);
245
227
  const random = this.options.random?.() ?? Math.random();
246
228
  const jitter = 0.9 + Math.max(0, Math.min(1, random)) * 0.2;
@@ -249,12 +231,12 @@ export class NativeCodexWebSocketTransport {
249
231
  async wait(retry, error, signal, telemetry) {
250
232
  const delay = this.retryDelay(retry, error);
251
233
  telemetry.retry(delay);
252
- await (this.options.sleep ?? sleep)(delay, signal);
234
+ await (this.options.sleep ?? sleepRetry)(delay, signal);
253
235
  }
254
236
  async waitForConnection(delayMs, signal, telemetry) {
255
237
  telemetry.retry(delayMs, 'connection');
256
238
  this.options.warn?.(`native Codex network is unavailable; reconnecting in ${delayMs}ms`);
257
- await (this.options.sleep ?? sleep)(delayMs, signal);
239
+ await (this.options.sleep ?? sleepRetry)(delayMs, signal);
258
240
  }
259
241
  closeEntry(entry) {
260
242
  entry.socket?.close();
@@ -424,7 +406,8 @@ export class NativeCodexWebSocketTransport {
424
406
  if (nextTurnState !== undefined)
425
407
  entry.turnState = nextTurnState;
426
408
  }
427
- const output = normalizedOutputItem(event);
409
+ const chunks = translator.push(event);
410
+ const output = normalizedOutputItem(event, translator);
428
411
  if (output !== undefined)
429
412
  outputItems.push(output);
430
413
  if (event.type === 'response.completed') {
@@ -435,7 +418,6 @@ export class NativeCodexWebSocketTransport {
435
418
  const responseId = typeof response?.id === 'string' ? response.id : '';
436
419
  entry.protocol.complete(responseId, outputItems);
437
420
  }
438
- const chunks = translator.push(event);
439
421
  if (!prewarm)
440
422
  for (const chunk of chunks)
441
423
  yield chunk;
@@ -616,7 +598,11 @@ export class NativeCodexWebSocketTransport {
616
598
  }
617
599
  if (failureValue.code === 'WS_UPGRADE_REQUIRED' || retryable) {
618
600
  entry.disabled = true;
619
- telemetry.retry(0);
601
+ if (remainingRetryDelay(failureValue) !== undefined) {
602
+ await this.wait(reconnects, failureValue, signal, telemetry);
603
+ }
604
+ else
605
+ telemetry.retry(0);
620
606
  yield* this.http.stream(activeGeneration, fallbackMode(), telemetry);
621
607
  requestCompleted = true;
622
608
  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 {
@@ -9,18 +9,14 @@ export interface ResolvedImagePart {
9
9
  dataBase64: string;
10
10
  detail?: CodexImageDetail;
11
11
  }
12
- export interface ResolvedToolResultPart {
13
- type: 'tool-result';
14
- toolCallId: ToolCallId;
15
- content: readonly ResolvedContentPart[];
16
- isError?: boolean;
17
- }
18
12
  export type ResolvedContentPart = Exclude<ContentBlock, {
19
- type: 'image' | 'tool-result';
20
- }> | ResolvedImagePart | ResolvedToolResultPart;
13
+ type: 'image';
14
+ }> | ResolvedImagePart;
21
15
  export interface ResolvedMessage {
22
- role: 'system' | 'user' | 'assistant';
16
+ role: 'system' | 'user' | 'assistant' | 'tool' | 'developer';
23
17
  content: readonly ResolvedContentPart[];
18
+ toolCallId?: ToolCallId;
19
+ isError?: boolean;
24
20
  replaySource?: NativeCodexReplaySource;
25
21
  }
26
22
  export interface ResponsesRequestInput {
@@ -57,7 +53,7 @@ export interface ResponsesUsage {
57
53
  /** Map provider totals to DSH's strict disjoint token counts. */
58
54
  export declare function mapResponsesUsage(usage: ResponsesUsage): TokenUsage;
59
55
  /** Classify in-band failure data without reflecting provider text. */
60
- export declare function responsesFailure(code?: string, message?: string): LlmError;
56
+ export declare function responsesFailure(code?: string, message?: string, headers?: unknown): LlmError;
61
57
  interface ResponsesOutputItem {
62
58
  type?: string;
63
59
  id?: string;
@@ -68,6 +64,7 @@ interface ResponsesOutputItem {
68
64
  encrypted_content?: string;
69
65
  summary?: unknown[];
70
66
  status?: string;
67
+ phase?: CodexMessagePhase | null;
71
68
  content?: Array<{
72
69
  type?: string;
73
70
  text?: string;
@@ -90,6 +87,7 @@ export interface ResponsesStreamEvent {
90
87
  error?: {
91
88
  code?: string;
92
89
  message?: string;
90
+ headers?: unknown;
93
91
  };
94
92
  incomplete_details?: {
95
93
  reason?: string;
@@ -107,11 +105,15 @@ export declare class ResponsesStreamTranslator {
107
105
  private readonly replayContext?;
108
106
  private readonly blocks;
109
107
  private readonly order;
108
+ private readonly messagePhases;
110
109
  private readonly replayCapture;
111
110
  private nextIndex;
112
111
  private sawToolCall;
113
112
  terminated: boolean;
114
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;
115
117
  private append;
116
118
  private fill;
117
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_';
@@ -26,12 +27,12 @@ function imageItem(image) {
26
27
  return { type: 'input_image', image_url: `data:${image.mediaType};base64,${image.dataBase64}`,
27
28
  ...image.detail === undefined ? {} : { detail: image.detail } };
28
29
  }
29
- function toolOutput(block) {
30
- const images = block.content.some(part => part.type === 'image');
30
+ function toolOutput(content) {
31
+ const images = content.some(part => part.type === 'image');
31
32
  if (!images) {
32
- return block.content.map(part => part.type === 'text' ? part.text : '').join('');
33
+ return content.map(part => part.type === 'text' ? part.text : '').join('');
33
34
  }
34
- return block.content.map((part) => {
35
+ return content.map((part) => {
35
36
  if (part.type === 'text')
36
37
  return { type: 'input_text', text: part.text };
37
38
  if (part.type === 'image' && 'dataBase64' in part)
@@ -67,6 +68,29 @@ export function toResponsesInput(messages, system) {
67
68
  input.push({ type: 'message', role: message.role, content });
68
69
  content = [];
69
70
  };
71
+ // Handle tool messages separately - they have no content blocks, just top-level fields
72
+ if (message.role === 'tool') {
73
+ if (message.toolCallId === undefined) {
74
+ throw fixedError('native Codex tool message requires toolCallId', 'INVALID_ARGS');
75
+ }
76
+ input.push({
77
+ type: 'function_call_output',
78
+ call_id: String(message.toolCallId),
79
+ output: toolOutput(message.content),
80
+ ...message.isError === true ? { is_error: true } : {},
81
+ });
82
+ continue;
83
+ }
84
+ // Handle developer messages - map to developer role in Responses API
85
+ if (message.role === 'developer') {
86
+ for (const block of message.content) {
87
+ if (block.type === 'text') {
88
+ content.push({ type: 'input_text', text: block.text });
89
+ }
90
+ }
91
+ flush();
92
+ continue;
93
+ }
70
94
  for (const block of message.content) {
71
95
  if (message.role === 'system') {
72
96
  if (block.type === 'text')
@@ -89,18 +113,15 @@ export function toResponsesInput(messages, system) {
89
113
  content.push(imageItem(block));
90
114
  break;
91
115
  case 'tool-call':
116
+ if (message.role !== 'assistant') {
117
+ throw fixedError('native Codex tool calls require assistant messages', 'INVALID_ARGS');
118
+ }
92
119
  flush();
93
120
  input.push({
94
121
  type: 'function_call', call_id: String(block.id),
95
122
  name: block.name, arguments: block.arguments,
96
123
  });
97
124
  break;
98
- case 'tool-result':
99
- flush();
100
- input.push({
101
- type: 'function_call_output', call_id: String(block.toolCallId), output: toolOutput(block),
102
- });
103
- break;
104
125
  case 'reasoning':
105
126
  break; // Visible reasoning is never replayed; M4 may add encrypted items.
106
127
  default:
@@ -191,11 +212,21 @@ export function codexRequestBody(options, messages, mode = {}) {
191
212
  ...mode.serviceTier === undefined ? {} : { service_tier: mode.serviceTier },
192
213
  ...options.sessionId === undefined ? {} : { prompt_cache_key: String(options.sessionId) },
193
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
+ }];
194
226
  if (mode.responsesLite === undefined) {
195
227
  return {
196
228
  ...common,
197
- instructions,
198
- input,
229
+ input: [...instructionPrefix, ...input],
199
230
  ...options.tools !== undefined && options.tools.length > 0
200
231
  ? { tools: toResponsesTools(options.tools) } : {},
201
232
  parallel_tool_calls: true,
@@ -204,7 +235,6 @@ export function codexRequestBody(options, messages, mode = {}) {
204
235
  };
205
236
  }
206
237
  const tools = toResponsesLiteTools(options.tools ?? []);
207
- const baseInstructions = mode.responsesLite.instructionsTemplate ?? instructions;
208
238
  const contextualInstructions = mode.responsesLite.instructionsTemplate !== undefined
209
239
  && resolved.instructions !== undefined && resolved.instructions !== baseInstructions
210
240
  ? [{
@@ -212,21 +242,12 @@ export function codexRequestBody(options, messages, mode = {}) {
212
242
  content: [{ type: 'input_text', text: resolved.instructions }],
213
243
  }]
214
244
  : [];
215
- const prefixNamespace = uuidBytes(uuidV5(UUID_NAMESPACE_OID, String(options.sessionId ?? options.model)));
216
245
  const prefix = [{
217
246
  type: 'additional_tools',
218
247
  id: `at_${uuidV5(prefixNamespace, JSON.stringify(tools))}`,
219
248
  role: 'developer',
220
249
  tools,
221
- }, {
222
- type: 'message',
223
- id: `msg_${uuidV5(prefixNamespace, baseInstructions)}`,
224
- role: 'developer',
225
- content: [{ type: 'input_text', text: baseInstructions }],
226
- internal_chat_message_metadata_passthrough: {
227
- content_item_kinds: ['model.base_instructions'],
228
- },
229
- }];
250
+ }, ...instructionPrefix];
230
251
  return {
231
252
  ...common,
232
253
  input: [...prefix, ...contextualInstructions, ...input],
@@ -273,10 +294,11 @@ function retryDelay(message) {
273
294
  const value = Number(match[1]);
274
295
  if (!Number.isFinite(value) || value <= 0)
275
296
  return undefined;
276
- 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;
277
299
  }
278
300
  /** Classify in-band failure data without reflecting provider text. */
279
- export function responsesFailure(code, message) {
301
+ export function responsesFailure(code, message, headers) {
280
302
  const detail = `${code ?? ''} ${message ?? ''}`;
281
303
  if (code === 'context_length_exceeded' || code === 'context_window_exceeded'
282
304
  || isContextWindowExceededError(detail)) {
@@ -288,8 +310,8 @@ export function responsesFailure(code, message) {
288
310
  return fixedError('native Codex account quota is exhausted', QUOTA_EXCEEDED_CODE);
289
311
  }
290
312
  if (code === 'rate_limit_exceeded' || code === 'slow_down') {
291
- const delay = retryDelay(message);
292
- 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', {
293
315
  ...delay === undefined ? {} : { providerRetryAfterMs: delay },
294
316
  });
295
317
  }
@@ -299,7 +321,10 @@ export function responsesFailure(code, message) {
299
321
  if (code === 'bio_policy') {
300
322
  return fixedError('native Codex rejected the request', 'INVALID_REQUEST');
301
323
  }
302
- 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
+ });
303
328
  }
304
329
  function eventItemId(event) {
305
330
  if (typeof event.item_id === 'string' && event.item_id.length > 0)
@@ -323,6 +348,7 @@ export class ResponsesStreamTranslator {
323
348
  replayContext;
324
349
  blocks = new Map();
325
350
  order = [];
351
+ messagePhases = new Map();
326
352
  replayCapture;
327
353
  nextIndex = 0;
328
354
  sawToolCall = false;
@@ -333,6 +359,18 @@ export class ResponsesStreamTranslator {
333
359
  ? undefined
334
360
  : new NativeCodexReplayCapture(replayContext.provider, replayContext.model);
335
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
+ }
336
374
  append(block, delta) {
337
375
  block.text += delta;
338
376
  }
@@ -380,6 +418,8 @@ export class ResponsesStreamTranslator {
380
418
  switch (event.type) {
381
419
  case 'response.output_item.added': {
382
420
  const item = event.item;
421
+ if (item?.type === 'message')
422
+ this.captureMessagePhase(item);
383
423
  if (item?.type !== 'function_call')
384
424
  return chunks;
385
425
  if (item.id === undefined || item.id.length === 0
@@ -463,6 +503,8 @@ export class ResponsesStreamTranslator {
463
503
  });
464
504
  }
465
505
  else if (item.type === 'message') {
506
+ this.captureMessagePhase(item);
507
+ const phase = this.messagePhase(item.id);
466
508
  if (!Array.isArray(item.content)) {
467
509
  throw fixedError('native Codex message item has invalid content', 'MALFORMED_RESPONSE');
468
510
  }
@@ -487,6 +529,7 @@ export class ResponsesStreamTranslator {
487
529
  if (this.replayContext !== undefined)
488
530
  this.replayCapture?.add({
489
531
  type: 'message', ...(replayId === undefined ? {} : { id: replayId }), blocks: refs,
532
+ ...(phase === undefined ? {} : { phase }),
490
533
  });
491
534
  }
492
535
  else if (item.type === 'reasoning') {
@@ -575,7 +618,7 @@ export class ResponsesStreamTranslator {
575
618
  return chunks;
576
619
  }
577
620
  case 'response.failed':
578
- 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);
579
622
  case 'error':
580
623
  throw responsesFailure(event.code, event.message);
581
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
+ }