@pure01fx/dsh-openai-codex-auth 0.12.0 → 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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.13.0
6
+
7
+ - Targets DSH 0.2.0-rc.2 with exact SDK peers, development dependencies and runtime helpers; retains the tool-message, shell and provenance migration below.
8
+ - Merges GPT-6.1 catalog routing, shared Retry-After handling, assistant phases and deterministic developer instructions from the latest source.
9
+ - Target build/typecheck and all 517 regression tests pass on DSH 0.2.0-rc.2. No live OAuth/model requests were made; collection-level Web validation is recorded separately.
10
+
5
11
  ## 0.12.0
6
12
 
7
13
  ### Migration
@@ -22,6 +28,15 @@
22
28
  - Developer messages: preserved in Responses API requests
23
29
  - Cloud context: requires toolCallId === source.callId and !isError
24
30
 
31
+ - Shares Retry-After parsing across HTTP, SSE, and WebSocket errors, including nested JSON headers and HTTP dates; preserves server deadlines across retries and HTTP fallback without truncating advice to local backoff limits.
32
+
33
+ - Preserves commentary, partial_answer, and final_answer phases through response events, durable replay, and WebSocket continuation matching; legacy phase-less replay remains readable.
34
+
35
+ - Moves Standard base instructions into the same deterministic developer-message prefix used by Lite, preserving each protocol’s tool and reasoning controls; changed instructions invalidate WebSocket prefix reuse.
36
+
37
+ - Requests the live model catalog as Codex 0.162.0 to discover GPT-6.1 Sol; verified that 0.155.0 omits it for the same account despite the bundled 0.153.0 minimum. Adds its exact Lite fallback and dynamic metadata/routing regressions without fabricating catalog availability.
38
+
39
+
25
40
  ## 0.11.2
26
41
 
27
42
  - Requests the live model catalog as Codex 0.155.0 so version-gated GPT-6 Sol can be advertised; preserves dynamic account-specific discovery and routes exact Sol requests through Responses Lite when catalog metadata is unavailable. Fast remains catalog-authorized.
@@ -1,5 +1,44 @@
1
1
  # Codex 云端能力兼容性跟踪
2
2
 
3
+ ## 重试、消息阶段与 Standard 指令对齐(Unreleased)
4
+
5
+ 本节记录对 `ea27864f` 的三项增量适配,取代下方 Retry-After、phase 和 Standard 指令位置的历史待办;其余工作仍独立跟踪。
6
+
7
+ - Standard 基础指令改为 input 首部的 developer message,和 Lite 共用稳定 UUID 与 `model.base_instructions` metadata 构造。Standard 顶层 tools、并行工具与 reasoning 参数保留各自协议;Lite 的工具前缀及目录模板/DSH 指令组合保持独立。回归验证相同指令保持 WS 前缀复用,改变指令会触发完整请求,HTTP 历史、图片、工具输出和加密 reasoning 顺序不变。
8
+ - 消息 phase 已接收并保留 commentary / partial_answer / final_answer,通过 v1 描述符的可选字段持久化再回放,并让 WS 复用比较使用同一有效 phase。added-only 的 phase 保留到 done,done 的显式 phase 优先;流中的 null/缺失值表示未提供,非法枚举拒绝。测试覆盖 JSON 保存/恢复、分叉和重启的请求构造、真实传输类的增量匹配与 phase 改变后完整重发。缺少 phase 的旧会话可读;旧插件的严格字段校验无法读取含 phase 的新描述符,降级时需考虑这一限制。
9
+ - Retry-After 共用秒数/HTTP-date 解析,支持 JSON 数字值,嵌套错误 headers 优先;保留单次收到建议的单调时钟截止时间,不用本地退避上限截短服务端等待。HTTP 读取错误正文及其超时不会重置或丢弃等待,WS 最后一次失败后切换 HTTP 也遵守剩余等待。长等待分段使用 Node timer,仍可取消。宿主 `LlmError` 只公开正数 duration,无法跨宿主持久化表达单调时钟 deadline;本插件默认 normal 重试策略在宿主等待超上限时交还错误,不会提前重试。宿主另行配置的 always 策略可能忽略长等待,不属于本次插件内改动。
10
+
11
+ 验证:`npm run build`、`npm test`(27 文件 / 511 项)、`npm run typecheck`、`git diff --check`、`npm pack --dry-run --ignore-scripts` 均通过;所有生成模块与新鲜编译一致,新 retry-after 模块已加入打包清单。独立审查发现的非法日期归一化问题已修复,并加入内层非法日期回退外层有效等待的回归。本轮为源码、模拟传输与 loopback 验证,未执行线上推理、发布、替换 hu profile 或重启 GUI。
12
+
13
+ ## GPT-6.1 Sol 增量对齐(Unreleased,2026-10-08)
14
+
15
+ `../codex` 已 fast-forward 到 `ea27864f99f0b086cec2f9f0251b7190fb9844f1`(本次获取的 origin/main),工作树干净。此轮针对模型发现与 Lite 路由,不宣称完整运行时对齐,原 Provider/cloud 跟踪提交保持不变。
16
+
17
+ ### 已确认的缺失原因与修复
18
+
19
+ 对同一当前账号执行只读线上目录请求(未调用推理、未记录凭据):
20
+
21
+ | client_version | originator | HTTP | 返回目录 |
22
+ | --- | --- | --- | --- |
23
+ | 0.153.4 | dsh | 200 | 不含 gpt-6-sol / gpt-6.1-sol |
24
+ | 0.155.0 | dsh | 200 | 含 gpt-6-sol,不含 gpt-6.1-sol |
25
+ | 0.162.0 | dsh | 200 | 含 gpt-6.1-sol,visibility=list |
26
+ | 0.162.0 | codex_cli_rs | 200 | 同样含 gpt-6.1-sol |
27
+
28
+ 上游 `models-manager/models.json` 将 6.1 Sol 的 minimal_client_version 标为 0.153.0,但它不等于线上实际发现门槛。请求版本升至本次已验证的 0.162.0(上游已有 rust-v0.162.0-alpha.20;目录发送去预发布后缀的整版本)。不更改 originator,不将静态模型强行加入账号目录。
29
+
30
+ 补充精确 `gpt-6.1-sol` 的 Lite 后备,目录存在时仍以 metadata 为准。新增精简上游 fixture,验证动态发现、六档 reasoning、默认 low / multi-agent xhigh、Fast 授权、metadata 优先及未知近似名称不误用 Lite。
31
+
32
+ 当前 3080 GUI 的 hu profile 安装的是插件 0.11.1,请求版本 0.153.4。本轮只修改并构建仓库,未替换 profile 安装或重启当前会话;仓库修改不会自动改变正在运行的 GUI。
33
+
34
+ 验证:`npm run build`、`npm test`(26 文件 / 469 项)、`npm run typecheck`、`git diff --check` 全部通过;生成产物一致性测试通过。使用新构建的 NativeCodexCatalog + NativeCodexAdapter 查询当前账号,已返回 `gpt-6.1-sol` 与 `gpt-6.1-sol-fast`,六档推理配置及默认 low 解析成功。本轮未执行模型推理,因此此结果验证发现与元数据,不代表线上推理端到端验收。
35
+
36
+ ### 独立后续兼容性工作
37
+
38
+ - Retry-After:新上游从 response.failed.response.error.headers 读取等待提示,WebSocket 优先嵌套 headers,支持数字 JSON 值和 HTTP 日期;插件仍遗漏部分嵌套提示并将等待上限压到 120 秒。建议单独对齐共享解析及 deadline 传递,避免早于服务端建议重试。依据 `codex-api/src/sse/responses_error.rs`、`endpoint/responses_websocket.rs`、`http-client/src/retry_after.rs`。这不是 6.1 模型目录缺失原因。
39
+ - 数字自定义 reasoning effort、Standard instructions 移入 developer input、partial_answer phase 回放保真分别存在增量差异,应单独设计与验证。6.1 现有命名 effort 和 Lite 路径不依赖这些改动。
40
+
41
+
3
42
  ## 已实现:Sol 与错误协议增量适配(0.11.2)
4
43
 
5
44
  参考上游 `c44deff7b1083e9660ac55d02122481f1cdf139b`,在既有 0.11.1 工作树上完成以下必要改动。此节取代下方历史审查中相应的“待适配”结论;不代表完整上游运行时对齐。
package/README.md CHANGED
@@ -17,9 +17,9 @@
17
17
 
18
18
  ## 快速开始
19
19
 
20
- 版本 `0.12.0` 明确适配 DeepSeek Harness `0.1.7-rc.2`,SDK peers/runtime helpers 精确锁定该版本;不声明兼容旧 SDK。旧 DSH `0.1.5-rc.1` profile 应保留原插件 `0.11.2`,升级时在独立 profile 安装新版插件并保留原 profile/锁文件以便回滚。无需迁移账号文件或浏览器隐藏列表;`dsh.engines` 仅作兼容性说明,不当作宿主硬门禁。
20
+ 版本 `0.13.0` 明确适配 DeepSeek Harness `0.2.0-rc.2`,SDK peers/runtime helpers 精确锁定该版本;不声明兼容旧 SDK。旧 DSH `0.1.5-rc.1` profile 应保留原插件 `0.11.2`,升级时在独立 profile 安装新版插件并保留原 profile/锁文件以便回滚。无需迁移账号文件或浏览器隐藏列表;`dsh.engines` 仅作兼容性说明,不当作宿主硬门禁。
21
21
 
22
- 本版本通过真实目标 LLM/tools 服务与本地 HTTP/SSE/WebSocket 回放、错误/取消回归;模型隐藏覆盖真实目录/catalog/store 的投影更新、重连和重新加载。验证命令为 `pnpm typecheck && pnpm build && pnpm test`;当前版本未执行线上 OAuth/付费模型验收。
22
+ 回归范围包含真实目标 LLM/tools 服务、本地 HTTP/SSE/WebSocket 回放、错误/取消,以及模型隐藏在真实目录/catalog/store 的投影更新、重连和重新加载。验证命令为 `pnpm typecheck && pnpm build && pnpm test`;0.13.0 候选版已通过目标版本构建、类型检查和 517 项测试;当前未执行线上 OAuth/付费模型验收。
23
23
 
24
24
  `0.11.0` 移除了插件额外设置的请求、响应、SSE/WebSocket、图片和重放状态大小上限,以及相应的 transport 选项(含 `maxRequestBodyBytes`);旧配置中的该字段应删除。搜索结果完整保留,WebSocket 继续优先复用增量上下文。服务端限制、协议校验及 DSH 文件/附件策略仍生效。
25
25
 
@@ -57,7 +57,8 @@ function responsesLiteMode(model, requestedModel = model?.slug) {
57
57
  if (model !== undefined && !model.useResponsesLite)
58
58
  return {};
59
59
  // Never downgrade known Lite-only slugs to Standard Responses during catalog outages.
60
- if (model === undefined && requestedModel !== 'gpt-6-astra' && requestedModel !== 'gpt-6-sol')
60
+ if (model === undefined && requestedModel !== 'gpt-6-astra'
61
+ && requestedModel !== 'gpt-6-sol' && requestedModel !== 'gpt-6.1-sol')
61
62
  return {};
62
63
  return {
63
64
  responsesLite: {
@@ -4,6 +4,7 @@ import { networkTelemetry, trackNetwork, observeNetworkOutput } from './network-
4
4
  import { EMPTY_RESPONSE_CODE, LlmError, ProviderRequestId, attributionHeaders, } from '@deepseek-ai/dsh-llm';
5
5
  import { nativeCodexAuthorityHash } from './catalog.js';
6
6
  import { nativeCodexEndpoint } from './endpoint.js';
7
+ import { retryAfterMs, retryError, remainingRetryDelay, sleepRetry, inheritRetryDeadline } from './retry-after.js';
7
8
  import { NATIVE_CODEX_CONNECTION_FAILED_CODE, NATIVE_CODEX_STREAM_INTERRUPTED_CODE, isNativeCodexConnectionFailure, } from './native-adapter.js';
8
9
  import { boundedCodexTurnState, codexRequestBody, codexResponseTurnState, streamResponses, responsesFailure, } from './responses.js';
9
10
  import { hasNativeCodexReplayKind } from './replay.js';
@@ -19,7 +20,7 @@ const INITIAL_CONNECTION_RETRY_DELAY_MS = 5_000;
19
20
  const MAX_CONNECTION_RETRY_DELAY_MS = 60_000;
20
21
  const MAX_ERROR_BODY_BYTES = 64 * 1024;
21
22
  function aborted(message = 'native Codex request was aborted') {
22
- return new LlmError(message, 'ABORTED');
23
+ return retryError(message, 'ABORTED');
23
24
  }
24
25
  function throwIfAborted(signal) {
25
26
  if (!signal?.aborted)
@@ -29,7 +30,7 @@ function throwIfAborted(signal) {
29
30
  throw aborted();
30
31
  }
31
32
  function fixedFailure(message, code, options) {
32
- return new LlmError(message, code, options);
33
+ return retryError(message, code, options);
33
34
  }
34
35
  function safePositiveInteger(value, fallback, name) {
35
36
  const resolved = value ?? fallback;
@@ -52,24 +53,6 @@ function stablePromptCacheKey(sessionId) {
52
53
  const hex = bytes.toString('hex');
53
54
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
54
55
  }
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
56
  function attemptWatchdog(parent, requestMs, idleMs) {
74
57
  const controller = new AbortController();
75
58
  let timeout;
@@ -252,18 +235,8 @@ async function boundedError(response, signal, telemetry) {
252
235
  return {};
253
236
  }
254
237
  }
255
- function retryAfterMs(response, maxDelayMs) {
256
- const raw = response.headers.get('retry-after');
257
- if (raw === null)
258
- return undefined;
259
- const seconds = Number(raw);
260
- const value = Number.isFinite(seconds)
261
- ? seconds * 1_000
262
- : Date.parse(raw) - Date.now();
263
- return Number.isFinite(value) && value > 0 ? Math.min(value, maxDelayMs) : undefined;
264
- }
265
- function errorFacts(response, maxDelayMs) {
266
- const providerRetryAfterMs = retryAfterMs(response, maxDelayMs);
238
+ function errorFacts(response) {
239
+ const providerRetryAfterMs = retryAfterMs(response.headers.get('retry-after'));
267
240
  const requestId = response.headers.get('x-request-id');
268
241
  return {
269
242
  status: response.status,
@@ -273,37 +246,43 @@ function errorFacts(response, maxDelayMs) {
273
246
  : { requestId: ProviderRequestId(requestId) },
274
247
  };
275
248
  }
276
- async function httpFailure(response, maxDelayMs, signal, telemetry) {
249
+ async function httpFailure(response, signal, telemetry) {
250
+ const facts = errorFacts(response);
251
+ const advice = retryError('native Codex HTTP retry advice', 'SERVER', facts);
252
+ const fail = (message, code, options) => inheritRetryDeadline(fixedFailure(message, code, options), advice);
277
253
  const detail = await boundedError(response, signal, telemetry);
278
- const facts = errorFacts(response, maxDelayMs);
279
254
  const classified = responsesFailure(detail.code, detail.message);
280
255
  if (response.status === 401 || response.status === 403) {
281
- return fixedFailure('native Codex rejected the configured credential', 'AUTH', facts);
256
+ return fail('native Codex rejected the configured credential', 'AUTH', facts);
282
257
  }
283
258
  if (classified.code !== 'SERVER') {
284
259
  const delay = facts?.providerRetryAfterMs ?? classified.failure.providerRetryAfterMs;
285
- return fixedFailure(classified.message, classified.code, {
260
+ return fail(classified.message, classified.code, {
286
261
  ...facts,
287
- ...(delay === undefined ? {} : { providerRetryAfterMs: Math.min(delay, maxDelayMs) }),
262
+ ...(delay === undefined ? {} : { providerRetryAfterMs: delay }),
288
263
  });
289
264
  }
290
265
  if (response.status === 429) {
291
- return fixedFailure('native Codex request was rate limited', 'RATE_LIMIT', facts);
266
+ return fail('native Codex request was rate limited', 'RATE_LIMIT', facts);
292
267
  }
293
268
  if (response.status === 408 || response.status === 504) {
294
- return fixedFailure('native Codex request timed out', 'TIMEOUT', facts);
269
+ return fail('native Codex request timed out', 'TIMEOUT', facts);
295
270
  }
296
271
  if (response.status >= 500) {
297
- return fixedFailure('native Codex server request failed', 'SERVER', facts);
272
+ return fail('native Codex server request failed', 'SERVER', facts);
298
273
  }
299
- return fixedFailure('native Codex request was rejected', 'INVALID_REQUEST', facts);
274
+ return fail('native Codex request was rejected', 'INVALID_REQUEST', facts);
300
275
  }
301
276
  function mappedFailure(error, watchdog, parent) {
302
277
  if (parent?.aborted) {
303
278
  return parent.reason instanceof LlmError ? parent.reason : aborted();
304
279
  }
305
280
  if (watchdog.timedOut())
306
- return fixedFailure('native Codex request timed out', 'TIMEOUT');
281
+ return fixedFailure('native Codex request timed out', 'TIMEOUT', {
282
+ cause: error,
283
+ ...(error instanceof LlmError && error.failure.providerRetryAfterMs !== undefined
284
+ ? { providerRetryAfterMs: error.failure.providerRetryAfterMs } : {}),
285
+ });
307
286
  if (error instanceof LlmError)
308
287
  return error;
309
288
  return fixedFailure('native Codex transport failed', 'TRANSPORT', { cause: error });
@@ -331,8 +310,7 @@ function retryable(error) {
331
310
  }
332
311
  function failedStepRetry(error) {
333
312
  const failure = error.failure;
334
- const providerRetryAfterMs = failure.providerRetryAfterMs === undefined
335
- ? undefined : Math.min(failure.providerRetryAfterMs, DEFAULT_MAX_RETRY_DELAY_MS);
313
+ const providerRetryAfterMs = failure.providerRetryAfterMs;
336
314
  return fixedFailure(`native Codex response stream was interrupted: ${error.message}`, NATIVE_CODEX_STREAM_INTERRUPTED_CODE, {
337
315
  cause: error,
338
316
  ...(failure.status === undefined ? {} : { status: failure.status }),
@@ -362,21 +340,21 @@ export class NativeCodexHttpTransport {
362
340
  }
363
341
  retryDelay(retry, providerDelay) {
364
342
  if (providerDelay !== undefined)
365
- return Math.min(providerDelay, this.maxDelayMs);
343
+ return providerDelay;
366
344
  const exponential = Math.min(this.initialDelayMs * (2 ** retry), this.maxDelayMs);
367
345
  const random = this.options.random?.() ?? Math.random();
368
346
  const jitter = 0.9 + Math.max(0, Math.min(1, random)) * 0.2;
369
347
  return Math.max(1, Math.round(exponential * jitter));
370
348
  }
371
349
  async wait(retry, error, signal, telemetry) {
372
- const delay = this.retryDelay(retry, error.failure.providerRetryAfterMs);
350
+ const delay = this.retryDelay(retry, remainingRetryDelay(error));
373
351
  telemetry.retry(delay);
374
- await (this.options.sleep ?? sleep)(delay, signal);
352
+ await (this.options.sleep ?? sleepRetry)(delay, signal);
375
353
  }
376
354
  async waitForConnection(delayMs, signal, telemetry) {
377
355
  telemetry.retry(delayMs, 'connection');
378
356
  this.options.warn?.(`native Codex network is unavailable; reconnecting in ${delayMs}ms`);
379
- await (this.options.sleep ?? sleep)(delayMs, signal);
357
+ await (this.options.sleep ?? sleepRetry)(delayMs, signal);
380
358
  }
381
359
  endpointUrl() { return this.endpoint; }
382
360
  async prepare(generation, mode = {}) {
@@ -527,7 +505,7 @@ export class NativeCodexHttpTransport {
527
505
  telemetry.retry(0);
528
506
  continue;
529
507
  }
530
- throw fixedFailure('native Codex rejected the configured credential', 'AUTH', errorFacts(response, this.maxDelayMs));
508
+ throw fixedFailure('native Codex rejected the configured credential', 'AUTH', errorFacts(response));
531
509
  }
532
510
  catch (error) {
533
511
  const connection = connectionFailure(error, watchdog, generation.signal);
@@ -542,7 +520,7 @@ export class NativeCodexHttpTransport {
542
520
  }
543
521
  }
544
522
  if (!response.ok) {
545
- let failure = await httpFailure(response, this.maxDelayMs, watchdog.signal, telemetry);
523
+ let failure = await httpFailure(response, watchdog.signal, telemetry);
546
524
  if (generation.signal?.aborted || watchdog.timedOut()) {
547
525
  failure = mappedFailure(failure, watchdog, generation.signal);
548
526
  }
@@ -3,11 +3,12 @@ import { LlmError, ProviderRequestId } from '@deepseek-ai/dsh-llm';
3
3
  import { HttpsProxyAgent } from 'https-proxy-agent';
4
4
  import { getProxyForUrl } from 'proxy-from-env';
5
5
  import { nativeCodexEndpoint } from './endpoint.js';
6
+ import { retryAfterMs, retryError } from './retry-after.js';
6
7
  import { NATIVE_CODEX_CONNECTION_FAILED_CODE, isNativeCodexConnectionFailure, } from './native-adapter.js';
7
8
  import WebSocket from 'ws';
8
9
  const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
9
10
  function failure(message, code, cause) {
10
- return new LlmError(message, code, cause === undefined ? undefined : { cause });
11
+ return retryError(message, code, cause === undefined ? undefined : { cause });
11
12
  }
12
13
  function abortFailure(signal) {
13
14
  return signal?.reason instanceof LlmError
@@ -17,9 +18,7 @@ function abortFailure(signal) {
17
18
  function handshakeFacts(status, headers) {
18
19
  const first = (value) => Array.isArray(value) ? value[0] : value;
19
20
  const requestId = first(headers['x-request-id']);
20
- const retryAfter = Number(first(headers['retry-after']));
21
- const providerRetryAfterMs = Number.isFinite(retryAfter) && retryAfter > 0
22
- ? Math.min(retryAfter * 1_000, 120_000) : undefined;
21
+ const providerRetryAfterMs = retryAfterMs(first(headers['retry-after']));
23
22
  return {
24
23
  ...(status === undefined ? {} : { status }),
25
24
  ...(providerRetryAfterMs === undefined ? {} : { providerRetryAfterMs }),
@@ -202,7 +201,7 @@ export class NodeNativeCodexWebSocketFactory {
202
201
  ? 'WS_RETRYABLE'
203
202
  : response.statusCode !== undefined && response.statusCode >= 400
204
203
  ? 'INVALID_REQUEST' : 'WS_HANDSHAKE';
205
- reject(new LlmError(`native Codex WebSocket handshake failed (HTTP ${response.statusCode ?? 0})`, code, handshakeFacts(response.statusCode, response.headers)));
204
+ reject(retryError(`native Codex WebSocket handshake failed (HTTP ${response.statusCode ?? 0})`, code, handshakeFacts(response.statusCode, response.headers)));
206
205
  });
207
206
  socket.on('open', () => {
208
207
  options.signal?.removeEventListener('abort', abort);
@@ -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,28 @@ 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);
212
193
  }
213
194
  /** Prefer WebSocket v2; once safe retries exhaust, keep that DSH session on HTTP. */
214
195
  export class NativeCodexWebSocketTransport {
@@ -239,9 +220,9 @@ export class NativeCodexWebSocketTransport {
239
220
  this.maxRetryDelayMs = boundedPositive(options.maxRetryDelayMs, DEFAULT_MAX_RETRY_DELAY_MS, 120_000, 'WebSocket maximum retry delay');
240
221
  }
241
222
  retryDelay(retry, error) {
242
- const providerDelay = error.failure.providerRetryAfterMs;
223
+ const providerDelay = remainingRetryDelay(error);
243
224
  if (providerDelay !== undefined)
244
- return Math.min(providerDelay, this.maxRetryDelayMs);
225
+ return providerDelay;
245
226
  const exponential = Math.min(this.initialRetryDelayMs * (2 ** retry), this.maxRetryDelayMs);
246
227
  const random = this.options.random?.() ?? Math.random();
247
228
  const jitter = 0.9 + Math.max(0, Math.min(1, random)) * 0.2;
@@ -250,12 +231,12 @@ export class NativeCodexWebSocketTransport {
250
231
  async wait(retry, error, signal, telemetry) {
251
232
  const delay = this.retryDelay(retry, error);
252
233
  telemetry.retry(delay);
253
- await (this.options.sleep ?? sleep)(delay, signal);
234
+ await (this.options.sleep ?? sleepRetry)(delay, signal);
254
235
  }
255
236
  async waitForConnection(delayMs, signal, telemetry) {
256
237
  telemetry.retry(delayMs, 'connection');
257
238
  this.options.warn?.(`native Codex network is unavailable; reconnecting in ${delayMs}ms`);
258
- await (this.options.sleep ?? sleep)(delayMs, signal);
239
+ await (this.options.sleep ?? sleepRetry)(delayMs, signal);
259
240
  }
260
241
  closeEntry(entry) {
261
242
  entry.socket?.close();
@@ -425,7 +406,8 @@ export class NativeCodexWebSocketTransport {
425
406
  if (nextTurnState !== undefined)
426
407
  entry.turnState = nextTurnState;
427
408
  }
428
- const output = normalizedOutputItem(event);
409
+ const chunks = translator.push(event);
410
+ const output = normalizedOutputItem(event, translator);
429
411
  if (output !== undefined)
430
412
  outputItems.push(output);
431
413
  if (event.type === 'response.completed') {
@@ -436,7 +418,6 @@ export class NativeCodexWebSocketTransport {
436
418
  const responseId = typeof response?.id === 'string' ? response.id : '';
437
419
  entry.protocol.complete(responseId, outputItems);
438
420
  }
439
- const chunks = translator.push(event);
440
421
  if (!prewarm)
441
422
  for (const chunk of chunks)
442
423
  yield chunk;
@@ -617,7 +598,11 @@ export class NativeCodexWebSocketTransport {
617
598
  }
618
599
  if (failureValue.code === 'WS_UPGRADE_REQUIRED' || retryable) {
619
600
  entry.disabled = true;
620
- telemetry.retry(0);
601
+ if (remainingRetryDelay(failureValue) !== undefined) {
602
+ await this.wait(reconnects, failureValue, signal, telemetry);
603
+ }
604
+ else
605
+ telemetry.retry(0);
621
606
  yield* this.http.stream(activeGeneration, fallbackMode(), telemetry);
622
607
  requestCompleted = true;
623
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 {
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pure01fx/dsh-openai-codex-auth",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Native ChatGPT Codex provider, device-code-first login, and same-origin Web integration for DeepSeek Harness",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -53,6 +53,8 @@
53
53
  "lib/responses.d.ts",
54
54
  "lib/replay.js",
55
55
  "lib/replay.d.ts",
56
+ "lib/retry-after.js",
57
+ "lib/retry-after.d.ts",
56
58
  "lib/rate-limits.js",
57
59
  "lib/rate-limits.d.ts",
58
60
  "lib/response-usage.js",
@@ -96,7 +98,7 @@
96
98
  },
97
99
  "dsh": {
98
100
  "engines": {
99
- "dsh": "0.1.7-rc.2"
101
+ "dsh": "0.2.0-rc.2"
100
102
  },
101
103
  "bundle": {
102
104
  "patch": "./cordis.patch.yml"
@@ -113,19 +115,19 @@
113
115
  },
114
116
  "peerDependencies": {
115
117
  "@deepseek-ai/cordis": "~4.0.4",
116
- "@deepseek-ai/dsh-attachment": "0.1.7-rc.2",
117
- "@deepseek-ai/dsh-credentials": "0.1.7-rc.2",
118
- "@deepseek-ai/dsh-fs": "0.1.7-rc.2",
119
- "@deepseek-ai/dsh-host-webserver": "0.1.7-rc.2",
120
- "@deepseek-ai/dsh-llm": "0.1.7-rc.2",
121
- "@deepseek-ai/dsh-sandbox": "0.1.7-rc.2",
122
- "@deepseek-ai/dsh-sandbox-policy": "0.1.7-rc.2",
123
- "@deepseek-ai/dsh-shell": "0.1.7-rc.2",
124
- "@deepseek-ai/dsh-tools": "0.1.7-rc.2"
118
+ "@deepseek-ai/dsh-attachment": "0.2.0-rc.2",
119
+ "@deepseek-ai/dsh-credentials": "0.2.0-rc.2",
120
+ "@deepseek-ai/dsh-fs": "0.2.0-rc.2",
121
+ "@deepseek-ai/dsh-host-webserver": "0.2.0-rc.2",
122
+ "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
123
+ "@deepseek-ai/dsh-sandbox": "0.2.0-rc.2",
124
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2",
125
+ "@deepseek-ai/dsh-shell": "0.2.0-rc.2",
126
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.2"
125
127
  },
126
128
  "dependencies": {
127
- "@deepseek-ai/dsh-atomic-write": "0.1.7-rc.2",
128
- "@deepseek-ai/dsh-home-paths": "0.1.7-rc.2",
129
+ "@deepseek-ai/dsh-atomic-write": "0.2.0-rc.2",
130
+ "@deepseek-ai/dsh-home-paths": "0.2.0-rc.2",
129
131
  "@deepseek-ai/schemastery": "^3.18.1",
130
132
  "https-proxy-agent": "7.0.6",
131
133
  "image-size": "^2.0.2",
@@ -134,23 +136,23 @@
134
136
  },
135
137
  "devDependencies": {
136
138
  "@deepseek-ai/cordis": "~4.0.4",
137
- "@deepseek-ai/dsh-agent": "0.1.7-rc.2",
138
- "@deepseek-ai/dsh-attachment": "0.1.7-rc.2",
139
- "@deepseek-ai/dsh-attachment-local": "0.1.7-rc.2",
140
- "@deepseek-ai/dsh-client-store": "0.1.7-rc.2",
141
- "@deepseek-ai/dsh-client-ui-model-selection": "0.1.7-rc.2",
142
- "@deepseek-ai/dsh-credentials": "0.1.7-rc.2",
143
- "@deepseek-ai/dsh-fs": "0.1.7-rc.2",
144
- "@deepseek-ai/dsh-host-webserver": "0.1.7-rc.2",
145
- "@deepseek-ai/dsh-llm": "0.1.7-rc.2",
146
- "@deepseek-ai/dsh-sandbox": "0.1.7-rc.2",
147
- "@deepseek-ai/dsh-sandbox-policy": "0.1.7-rc.2",
148
- "@deepseek-ai/dsh-scope": "0.1.7-rc.2",
149
- "@deepseek-ai/dsh-session": "0.1.7-rc.2",
150
- "@deepseek-ai/dsh-shell": "0.1.7-rc.2",
151
- "@deepseek-ai/dsh-system-prompt": "0.1.7-rc.2",
152
- "@deepseek-ai/dsh-tools": "0.1.7-rc.2",
153
- "@deepseek-ai/dsh-web-frontend": "0.1.7-rc.2",
139
+ "@deepseek-ai/dsh-agent": "0.2.0-rc.2",
140
+ "@deepseek-ai/dsh-attachment": "0.2.0-rc.2",
141
+ "@deepseek-ai/dsh-attachment-local": "0.2.0-rc.2",
142
+ "@deepseek-ai/dsh-client-store": "0.2.0-rc.2",
143
+ "@deepseek-ai/dsh-client-ui-model-selection": "0.2.0-rc.2",
144
+ "@deepseek-ai/dsh-credentials": "0.2.0-rc.2",
145
+ "@deepseek-ai/dsh-fs": "0.2.0-rc.2",
146
+ "@deepseek-ai/dsh-host-webserver": "0.2.0-rc.2",
147
+ "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
148
+ "@deepseek-ai/dsh-sandbox": "0.2.0-rc.2",
149
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2",
150
+ "@deepseek-ai/dsh-scope": "0.2.0-rc.2",
151
+ "@deepseek-ai/dsh-session": "0.2.0-rc.2",
152
+ "@deepseek-ai/dsh-shell": "0.2.0-rc.2",
153
+ "@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2",
154
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
155
+ "@deepseek-ai/dsh-web-frontend": "0.2.0-rc.2",
154
156
  "@types/node": "^22.20.0",
155
157
  "@types/proxy-from-env": "1.0.4",
156
158
  "@types/ws": "8.18.1",