@pure01fx/dsh-openai-codex-auth 0.11.0 → 0.11.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.11.1
6
+
7
+ - Adds live network status and process-wide request, retry, and received-payload statistics to the quota panel, distinguishing network activity from model output.
8
+ - Shows a breathing quota indicator during unbounded connection recovery, including while the panel is closed; respects reduced-motion preferences and clears the indicator after recovery, cancellation, or stale status updates.
9
+ - Exposes bounded, content-free telemetry through the protected local network endpoint without additional upstream quota requests or changes to timeout and retry behavior.
10
+
5
11
  ## 0.11.0
6
12
 
7
13
  - Removes plugin payload byte ceilings for native and cloud requests, SSE events, WebSocket messages and queues, response accumulation, encrypted replay state, model catalogs, and image data; removes the corresponding transport options, including `maxRequestBodyBytes`. WebSocket disables the underlying `ws` message-size ceiling explicitly.
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  ## 快速开始
19
19
 
20
- 版本 `0.11.0` 明确适配 DeepSeek Harness `0.1.5-rc.1`,SDK peers/runtime helpers 精确锁定该版本;不声明兼容旧 SDK。旧 DSH `0.1.1-rc.2` profile 应保留原插件 `0.10.0`,升级时在独立 profile 安装新版插件并保留原 profile/锁文件以便回滚。无需迁移账号文件或浏览器隐藏列表;`dsh.engines` 仅作兼容性说明,不当作宿主硬门禁。
20
+ 版本 `0.11.1` 明确适配 DeepSeek Harness `0.1.5-rc.1`,SDK peers/runtime helpers 精确锁定该版本;不声明兼容旧 SDK。旧 DSH `0.1.1-rc.2` profile 应保留原插件 `0.10.0`,升级时在独立 profile 安装新版插件并保留原 profile/锁文件以便回滚。无需迁移账号文件或浏览器隐藏列表;`dsh.engines` 仅作兼容性说明,不当作宿主硬门禁。
21
21
 
22
22
  本版本通过真实目标 LLM/tools 服务与本地 HTTP/SSE/WebSocket 回放、错误/取消回归;模型隐藏覆盖真实目录/catalog/store 的投影更新、重连和重新加载。验证命令为 `pnpm typecheck && pnpm build && pnpm test`;当前版本未执行线上 OAuth/付费模型验收。
23
23
 
package/client.js CHANGED
@@ -225,6 +225,8 @@ window.__ModuleLoader__.load({
225
225
  .codexQuotaTrack{fill:none;stroke:var(--dsw-alias-border-l3);stroke-width:2px}.codexQuotaAvailable{fill:none;stroke:var(--dsw-alias-state-success-primary);stroke-width:2px;stroke-linecap:round;transition:stroke .25s ease,stroke-dasharray .25s ease,stroke-dashoffset .25s ease}.codexQuotaAvailable.high{stroke:var(--dsw-alias-state-warn-label)}.codexQuotaAvailable.critical{stroke:var(--dsw-alias-state-error-primary)}.codexQuotaLoading{animation:codexQuotaPulse .8s ease-in-out infinite alternate}
226
226
  .codexQuotaPanel{z-index:100;box-sizing:border-box;width:272px;border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;padding:12px;background:var(--dsw-specific-menu);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-secondary);cursor:default;font-size:12px;line-height:20px;position:absolute;right:0;bottom:calc(100% + 8px)}.codexQuotaPanelHead{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:8px}.codexQuotaPanelHead strong{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}.codexQuotaPanelHead span{color:var(--dsw-alias-label-tertiary)}
227
227
  .codexQuotaWindow{padding:7px 0}.codexQuotaWindow+.codexQuotaWindow{border-top:1px solid var(--dsw-alias-border-l2)}.codexQuotaWindowHead{display:flex;align-items:center;justify-content:space-between;gap:10px}.codexQuotaWindowHead strong{font-weight:500;color:var(--dsw-alias-label-secondary)}.codexQuotaWindowHead span{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary)}.codexQuotaBar{height:4px;margin:6px 0;border-radius:999px;overflow:hidden;background:var(--dsw-alias-interactive-bg-hover)}.codexQuotaBarFill{height:100%;border-radius:inherit;background:var(--dsw-alias-state-success-primary)}.codexQuotaBarFill.high{background:var(--dsw-alias-state-warn-label)}.codexQuotaBarFill.critical{background:var(--dsw-alias-state-error-primary)}.codexQuotaReset{color:var(--dsw-alias-label-tertiary)}.codexQuotaMessage{margin:4px 0;color:var(--dsw-alias-label-tertiary)}.codexQuotaMessage.error{color:var(--dsw-alias-state-error-primary)}
228
+ .codexNetwork{margin-top:8px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2)}.codexNetworkHead,.codexNetworkRow{display:flex;align-items:baseline;justify-content:space-between;gap:12px}.codexNetworkHead{margin-bottom:5px}.codexNetworkHead strong{font-weight:500;color:var(--dsw-alias-label-primary)}.codexNetworkRow>span:first-child,.codexNetworkScope{color:var(--dsw-alias-label-tertiary)}.codexNetworkRow>span:last-child{font-variant-numeric:tabular-nums;text-align:right}.codexNetworkScope{margin:6px 0 0;font-size:11px;line-height:16px}.codexNetworkWarning{color:var(--dsw-alias-state-warn-label)}.codexQuotaPanel{max-height:min(680px,75vh);overflow-y:auto;width:min(300px,calc(100vw - 24px))}
229
+ .codexQuotaReconnecting .codexQuotaTrigger svg,.codexQuotaReconnecting .codexQuotaBarFill{animation:codexReconnectBreath 2s ease-in-out infinite}.codexQuotaReconnecting .codexQuotaTrack,.codexQuotaReconnecting .codexQuotaAvailable{stroke:var(--dsw-alias-state-warn-label)}@keyframes codexReconnectBreath{0%,100%{opacity:1}50%{opacity:.3}}@media(prefers-reduced-motion:reduce){.codexQuotaReconnecting .codexQuotaTrigger svg,.codexQuotaReconnecting .codexQuotaBarFill{animation:none}}
228
230
  @keyframes codexPulse{to{background-position:-200% 0}}@keyframes codexQuotaPulse{to{opacity:.35}}@media(prefers-reduced-motion:reduce){.codexSkeleton,.codexQuotaLoading{animation:none}}@media(max-width:620px){.codexHero{padding:14px;flex-direction:column}.codexBody{padding:14px}.codexGrid,.codexModelVisibilityList{grid-template-columns:1fr}.codexAccountOption{align-items:stretch;flex-direction:column;gap:10px}.codexAccountActions{align-self:flex-start}.codexCode{font-size:20px}}
229
231
  `
230
232
  if (typeof document !== 'undefined' && document.querySelector('style[data-plugin="' + PLUGIN_ID + '"]') === null) {
@@ -408,10 +410,120 @@ window.__ModuleLoader__.load({
408
410
  )
409
411
  }
410
412
 
413
+ function networkDuration(ms) {
414
+ const seconds = Math.max(0, Math.floor(ms / 1000))
415
+ if (seconds < 60) return seconds + ' 秒'
416
+ return Math.floor(seconds / 60) + ' 分 ' + seconds % 60 + ' 秒'
417
+ }
418
+
419
+ function networkBytes(bytes) {
420
+ if (bytes < 1024) return Math.round(bytes) + ' B'
421
+ if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'
422
+ return (bytes / 1048576).toFixed(1) + ' MB'
423
+ }
424
+
425
+ function networkSummary(snapshot) {
426
+ const active = snapshot.requests.filter((request) => !['completed', 'failed', 'cancelled'].includes(request.state))
427
+ const priority = { retrying: 0, waiting: 1, connecting: 2, preparing: 3, streaming: 4 }
428
+ const current = active.sort((a, b) => priority[a.state] - priority[b.state] || a.startedAt - b.startedAt)[0]
429
+ || snapshot.requests.slice().sort((a, b) => b.startedAt - a.startedAt)[0]
430
+ const labels = { preparing: '准备请求', connecting: '连接中', waiting: '等待响应', streaming: '接收中', retrying: '等待重试', completed: '最近请求已完成', failed: '最近请求失败', cancelled: '最近请求已取消' }
431
+ const stalled = current && current.state === 'streaming' && snapshot.updatedAt - (current.lastActivityAt || current.startedAt) >= 10000
432
+ return { current, label: stalled ? '等待后续数据' : current ? labels[current.state] || '状态未知' : '尚无请求', active: active.length > 0 }
433
+ }
434
+
435
+ function useNetworkStats(enabled) {
436
+ const [sample, setSample] = useState(null)
437
+ const [error, setError] = useState('')
438
+ useEffect(() => {
439
+ if (!enabled) return undefined
440
+ let disposed = false
441
+ let timer
442
+ let controller
443
+ let timeout
444
+ let previous
445
+ const poll = async () => {
446
+ if (disposed || document.visibilityState === 'hidden') return
447
+ controller = new AbortController()
448
+ timeout = window.setTimeout(() => controller.abort(), 5000)
449
+ try {
450
+ const response = await fetch(BASE + '/network', { cache: 'no-store', signal: controller.signal })
451
+ if (!response.ok) throw new Error('HTTP ' + response.status)
452
+ const snapshot = await response.json()
453
+ if (!snapshot.totals || !Array.isArray(snapshot.requests)) throw new Error('网络统计暂不可用')
454
+ const elapsed = previous && snapshot.updatedAt - previous.updatedAt
455
+ const delta = previous && snapshot.totals.receivedBytes - previous.totals.receivedBytes
456
+ const rate = previous && previous.startedAt === snapshot.startedAt && elapsed > 0 && delta >= 0
457
+ ? delta * 1000 / elapsed : null
458
+ previous = snapshot
459
+ if (!disposed) { setSample({ snapshot, rate }); setError('') }
460
+ } catch (failure) {
461
+ if (!disposed) setError('网络状态更新失败(' + messageOf(failure) + ')')
462
+ } finally {
463
+ window.clearTimeout(timeout)
464
+ controller = null
465
+ if (!disposed) timer = window.setTimeout(poll, 2000)
466
+ }
467
+ }
468
+ const onVisibility = () => {
469
+ if (document.visibilityState !== 'hidden' && !controller) {
470
+ window.clearTimeout(timer)
471
+ previous = null
472
+ void poll()
473
+ }
474
+ }
475
+ void poll()
476
+ document.addEventListener('visibilitychange', onVisibility)
477
+ return () => {
478
+ disposed = true
479
+ window.clearTimeout(timer)
480
+ window.clearTimeout(timeout)
481
+ if (controller) controller.abort()
482
+ document.removeEventListener('visibilitychange', onVisibility)
483
+ }
484
+ }, [enabled])
485
+ return { sample, error }
486
+ }
487
+
488
+ function networkReconnecting(network) {
489
+ return !network.error && network.sample && network.sample.snapshot.requests.some((request) =>
490
+ request.retryKind === 'connection' && ['retrying', 'preparing', 'connecting'].includes(request.state))
491
+ }
492
+
493
+ function CodexNetworkStats({ sample, error }) {
494
+ const snapshot = sample && sample.snapshot
495
+ const summary = snapshot && networkSummary(snapshot)
496
+ const current = summary && summary.current
497
+ const row = (label, value) => h('div', { className: 'codexNetworkRow', key: label }, h('span', null, label), h('span', null, value))
498
+ return h('section', { className: 'codexNetwork', 'aria-label': 'Codex 网络状态' },
499
+ h('div', { className: 'codexNetworkHead' }, h('strong', null, '网络'),
500
+ h('span', { className: error ? 'codexNetworkWarning' : '' }, error ? sample ? '数据已过期' : '读取失败' : summary ? summary.label : '读取中…')),
501
+ error ? h('p', { className: 'codexQuotaMessage error', role: 'status' }, error) : null,
502
+ snapshot ? h(React.Fragment, null,
503
+ row('进行中', snapshot.activeCount + ' 个请求'),
504
+ current ? row('传输', current.transport === 'websocket' ? 'WebSocket' : 'HTTP / SSE') : null,
505
+ summary.active && current ? h(React.Fragment, null,
506
+ row('本次已等待', networkDuration(snapshot.updatedAt - current.startedAt)),
507
+ row('最近收包', current.lastActivityAt === null ? '尚未收到' : networkDuration(snapshot.updatedAt - current.lastActivityAt) + '前'),
508
+ row('最近输出', current.lastOutputAt === null ? '尚无输出' : networkDuration(snapshot.updatedAt - current.lastOutputAt) + '前'),
509
+ current.state === 'retrying' && current.retryAt !== null ? row('下次重试', networkDuration(current.retryAt - snapshot.updatedAt) + '后') : null,
510
+ row('本次尝试 / 重试', current.attempts + ' / ' + current.retries),
511
+ ) : null,
512
+ row('接收速率', error || sample.rate === null ? '—' : networkBytes(sample.rate) + '/s'),
513
+ row('累计接收', networkBytes(snapshot.totals.receivedBytes)),
514
+ row('请求 / 成功 / 失败', snapshot.totals.requests + ' / ' + snapshot.totals.succeeded + ' / ' + snapshot.totals.failed),
515
+ row('累计重试 / 取消', snapshot.totals.retries + ' / ' + snapshot.totals.cancelled),
516
+ h('p', { className: 'codexNetworkScope' }, '本次加载以来 · 所有会话的原生 Codex 请求。接收量为响应及心跳载荷,不含协议开销。' + (snapshot.activeCount > 1 ? '上方显示优先等待的请求。' : '') + (snapshot.truncated ? '仅保留最近请求明细。' : '')),
517
+ ) : null,
518
+ )
519
+ }
520
+
411
521
  function CodexQuotaRing(props) {
412
522
  const running = props.useSession((snapshot) => snapshot.running)
413
523
  const status = useSharedStatus()
414
524
  const [open, setOpen] = useState(false)
525
+ const network = useNetworkStats(Boolean(status && status.loggedIn))
526
+ const reconnecting = networkReconnecting(network)
415
527
  const [loading, setLoading] = useState(false)
416
528
  const [error, setError] = useState('')
417
529
  const rootRef = useRef(null)
@@ -462,7 +574,7 @@ window.__ModuleLoader__.load({
462
574
  const used = meter ? Math.max(0, Math.min(100, Number(meter.usedPercent) || 0)) : 0
463
575
  const remaining = Math.max(0, 100 - used)
464
576
  const reading = meter ? Math.round(remaining) + '%' : '待更新'
465
- const title = loading ? '正在刷新 Codex 额度' : 'Codex 额度剩余 ' + reading + ';点击刷新'
577
+ const title = reconnecting ? 'Codex 网络不可用,正在持续重连' : loading ? '正在刷新 Codex 额度' : 'Codex 额度剩余 ' + reading + ';点击刷新'
466
578
  const circumference = 2 * Math.PI * 5.5
467
579
  const usedLength = circumference * used / 100
468
580
  const remainingLength = circumference - usedLength
@@ -472,7 +584,7 @@ window.__ModuleLoader__.load({
472
584
 
473
585
  return h('span', {
474
586
  ref: rootRef,
475
- className: 'codexQuotaRoot',
587
+ className: 'codexQuotaRoot' + (reconnecting ? ' codexQuotaReconnecting' : ''),
476
588
  onMouseEnter: () => { setOpen(true) },
477
589
  onMouseLeave: () => { setOpen(false) },
478
590
  },
@@ -510,6 +622,7 @@ window.__ModuleLoader__.load({
510
622
  windows.length > 0
511
623
  ? windows.map((row) => h(QuotaWindow, { key: row.name, name: row.name, window: row.window }))
512
624
  : h('p', { className: 'codexQuotaMessage' }, '点击圆圈刷新最新额度。'),
625
+ h(CodexNetworkStats, network),
513
626
  status.usageError ? h('p', { className: 'codexQuotaMessage error', role: 'status' }, '额度读取失败:' + status.usageError) : null,
514
627
  error ? h('p', { className: 'codexQuotaMessage error', role: 'status' }, '无法连接额度接口:' + error) : null,
515
628
  ) : null,
package/lib/index.d.ts CHANGED
@@ -200,6 +200,7 @@ export declare class OpenAICodexAuth extends Service {
200
200
  private sendText;
201
201
  private trustedManagementRequest;
202
202
  private requireCsrf;
203
+ private handleNetwork;
203
204
  private handleStatus;
204
205
  private handleAccountUsage;
205
206
  private handleDeviceStart;
package/lib/index.js CHANGED
@@ -18,6 +18,7 @@ import { NativeCodexCloudClient } from './cloud-http.js';
18
18
  import { registerCodexImageTools } from './cloud-tools.js';
19
19
  import { registerCodexWeb } from './cloud-web-tool.js';
20
20
  import { mergeDirectUsage, normalizeUsage } from './usage.js';
21
+ import { networkTelemetry } from './network-telemetry.js';
21
22
  export { normalizeUsage } from './usage.js';
22
23
  export { CODEX_CLIENT_VERSION, TRACKED_CODEX_COMMIT, TRACKED_CODEX_RELEASE, TRACKED_CODEX_REPOSITORY, } from './upstream.js';
23
24
  const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
@@ -829,6 +830,7 @@ export class OpenAICodexAuth extends Service {
829
830
  disposers.push(ctx.webServer.register({ kind: 'exact', path, handler }));
830
831
  };
831
832
  register('/openai-codex/status', (req, res) => this.handleStatus(req, res));
833
+ register('/openai-codex/network', (req, res) => this.handleNetwork(req, res));
832
834
  register('/openai-codex/device/start', (req, res) => this.handleDeviceStart(req, res));
833
835
  register('/openai-codex/browser/start', (req, res) => this.handleBrowserStart(req, res));
834
836
  register('/openai-codex/browser/prepare', (req, res) => this.handleBrowserPrepare(req, res));
@@ -2013,6 +2015,15 @@ export class OpenAICodexAuth extends Service {
2013
2015
  this.sendJson(res, 403, { error: 'Invalid CSRF token.' });
2014
2016
  return false;
2015
2017
  }
2018
+ handleNetwork(req, res) {
2019
+ if (!this.trustedManagementRequest(req, res))
2020
+ return;
2021
+ if (req.method !== 'GET') {
2022
+ this.sendJson(res, 405, { error: 'GET only' }, { allow: 'GET' });
2023
+ return;
2024
+ }
2025
+ this.sendJson(res, 200, networkTelemetry.snapshot());
2026
+ }
2016
2027
  async handleStatus(req, res) {
2017
2028
  if (!this.trustedManagementRequest(req, res))
2018
2029
  return;
@@ -1,3 +1,4 @@
1
+ import { type NetworkTelemetry, type NetworkRequestHandle } from './network-telemetry.js';
1
2
  import { type ContentBlock, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm';
2
3
  import { type NativeCodexCredential } from './catalog.js';
3
4
  import { type NativeCodexTransportMode } from './native-adapter.js';
@@ -16,6 +17,7 @@ export interface NativeCodexHttpOptions {
16
17
  resolveCredential(signal?: AbortSignal): Promise<NativeCodexCredential>;
17
18
  recoverCredential?(previous: NativeCodexCredential, signal?: AbortSignal): Promise<boolean>;
18
19
  readImage?(attachment: ImageBlock['attachment'], signal?: AbortSignal): Promise<NativeCodexImageRead>;
20
+ telemetry?: NetworkTelemetry;
19
21
  fetch?: typeof fetch;
20
22
  endpoint?: string;
21
23
  requestTimeoutMs?: number;
@@ -54,6 +56,7 @@ export declare class NativeCodexHttpTransport {
54
56
  private waitForConnection;
55
57
  endpointUrl(): string;
56
58
  prepare(generation: GenerateOptions, mode?: NativeCodexTransportMode): Promise<NativeCodexPreparedRequest>;
57
- stream(generation: GenerateOptions, mode?: NativeCodexTransportMode): AsyncIterable<StreamChunk>;
59
+ stream(generation: GenerateOptions, mode?: NativeCodexTransportMode, telemetry?: NetworkRequestHandle): AsyncIterable<StreamChunk>;
60
+ private streamTracked;
58
61
  }
59
62
  export {};
@@ -1,5 +1,6 @@
1
1
  /** Native ChatGPT Codex HTTP/SSE transport with safe pre-output retries. */
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
+ import { networkTelemetry, trackNetwork, observeNetworkOutput } from './network-telemetry.js';
3
4
  import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, attributionHeaders, } from '@deepseek-ai/dsh-llm';
4
5
  import { nativeCodexAuthorityHash } from './catalog.js';
5
6
  import { nativeCodexEndpoint } from './endpoint.js';
@@ -198,7 +199,7 @@ async function resolveMessages(generation, options) {
198
199
  }
199
200
  return messages;
200
201
  }
201
- async function boundedError(response, signal) {
202
+ async function boundedError(response, signal, telemetry) {
202
203
  if (response.body === null)
203
204
  return {};
204
205
  const reader = response.body.getReader();
@@ -222,6 +223,7 @@ async function boundedError(response, signal) {
222
223
  return {};
223
224
  if (done)
224
225
  break;
226
+ telemetry.activity(value.byteLength);
225
227
  total += value.byteLength;
226
228
  if (total > MAX_ERROR_BODY_BYTES) {
227
229
  await reader.cancel().catch(() => { });
@@ -275,8 +277,8 @@ function errorFacts(response, maxDelayMs) {
275
277
  : { requestId: ProviderRequestId(requestId) },
276
278
  };
277
279
  }
278
- async function httpFailure(response, maxDelayMs, signal) {
279
- const detail = await boundedError(response, signal);
280
+ async function httpFailure(response, maxDelayMs, signal, telemetry) {
281
+ const detail = await boundedError(response, signal, telemetry);
280
282
  const facts = errorFacts(response, maxDelayMs);
281
283
  const classification = `${detail.code ?? ''} ${detail.message ?? ''}`;
282
284
  if (response.status === 401 || response.status === 403) {
@@ -374,11 +376,13 @@ export class NativeCodexHttpTransport {
374
376
  const jitter = 0.9 + Math.max(0, Math.min(1, random)) * 0.2;
375
377
  return Math.max(1, Math.round(exponential * jitter));
376
378
  }
377
- async wait(retry, error, signal) {
379
+ async wait(retry, error, signal, telemetry) {
378
380
  const delay = this.retryDelay(retry, error.failure.providerRetryAfterMs);
381
+ telemetry.retry(delay);
379
382
  await (this.options.sleep ?? sleep)(delay, signal);
380
383
  }
381
- async waitForConnection(delayMs, signal) {
384
+ async waitForConnection(delayMs, signal, telemetry) {
385
+ telemetry.retry(delayMs, 'connection');
382
386
  this.options.warn?.(`native Codex network is unavailable; reconnecting in ${delayMs}ms`);
383
387
  await (this.options.sleep ?? sleep)(delayMs, signal);
384
388
  }
@@ -419,7 +423,15 @@ export class NativeCodexHttpTransport {
419
423
  // Serialize only in the selected transport.
420
424
  return { generation, mode, request, routingId, routingHint };
421
425
  }
422
- async *stream(generation, mode = {}) {
426
+ async *stream(generation, mode = {}, telemetry) {
427
+ if (telemetry !== undefined) {
428
+ yield* this.streamTracked(generation, mode, telemetry);
429
+ return;
430
+ }
431
+ yield* trackNetwork(this.options.telemetry ?? networkTelemetry, 'http', generation.signal, handle => this.streamTracked(generation, mode, handle));
432
+ }
433
+ async *streamTracked(generation, mode, telemetry) {
434
+ telemetry.state('preparing');
423
435
  const prepared = await this.prepare(generation, mode);
424
436
  const { request, routingId, routingHint } = prepared;
425
437
  let body;
@@ -441,6 +453,7 @@ export class NativeCodexHttpTransport {
441
453
  let pinnedAccountId = mode.pinnedAccountId;
442
454
  while (true) {
443
455
  throwIfAborted(generation.signal);
456
+ telemetry.state('preparing');
444
457
  const watchdog = attemptWatchdog(generation.signal, this.requestTimeoutMs, this.idleTimeoutMs);
445
458
  let response;
446
459
  let credential;
@@ -459,6 +472,7 @@ export class NativeCodexHttpTransport {
459
472
  throw fixedFailure('native Codex Fast capability authority changed before request', 'FAST_CAPABILITY_UNAVAILABLE');
460
473
  }
461
474
  fetching = true;
475
+ telemetry.attempt('http');
462
476
  response = await this.fetchImpl(this.endpoint, {
463
477
  method: 'POST',
464
478
  redirect: 'error',
@@ -491,16 +505,18 @@ export class NativeCodexHttpTransport {
491
505
  : connection ?? mappedFailure(error, watchdog, generation.signal);
492
506
  watchdog.stop();
493
507
  if (connection !== undefined) {
494
- await this.waitForConnection(connectionRetryDelayMs, generation.signal);
508
+ await this.waitForConnection(connectionRetryDelayMs, generation.signal, telemetry);
495
509
  connectionRetryDelayMs = Math.min(connectionRetryDelayMs * 2, MAX_CONNECTION_RETRY_DELAY_MS);
496
510
  continue;
497
511
  }
498
512
  if (retryable(failure) && transientRetries < this.maxRetries) {
499
- await this.wait(transientRetries++, failure, generation.signal);
513
+ await this.wait(transientRetries++, failure, generation.signal, telemetry);
500
514
  continue;
501
515
  }
502
516
  throw failure;
503
517
  }
518
+ telemetry.activity();
519
+ telemetry.state('waiting');
504
520
  publishCodexRateLimits(credential.accountId, parseCodexRateLimitHeaders(response.headers), this.options.onRateLimits, this.options.warn);
505
521
  if (activeTurnState === undefined) {
506
522
  const headerTurnState = boundedCodexTurnState(response.headers.get('x-codex-turn-state'));
@@ -515,8 +531,10 @@ export class NativeCodexHttpTransport {
515
531
  const changed = await this.options.recoverCredential(credential, watchdog.signal);
516
532
  recovered = true;
517
533
  watchdog.stop();
518
- if (changed)
534
+ if (changed) {
535
+ telemetry.retry(0);
519
536
  continue;
537
+ }
520
538
  throw fixedFailure('native Codex rejected the configured credential', 'AUTH', errorFacts(response, this.maxDelayMs));
521
539
  }
522
540
  catch (error) {
@@ -524,7 +542,7 @@ export class NativeCodexHttpTransport {
524
542
  const failure = connection ?? mappedFailure(error, watchdog, generation.signal);
525
543
  watchdog.stop();
526
544
  if (connection !== undefined) {
527
- await this.waitForConnection(connectionRetryDelayMs, generation.signal);
545
+ await this.waitForConnection(connectionRetryDelayMs, generation.signal, telemetry);
528
546
  connectionRetryDelayMs = Math.min(connectionRetryDelayMs * 2, MAX_CONNECTION_RETRY_DELAY_MS);
529
547
  continue;
530
548
  }
@@ -532,13 +550,13 @@ export class NativeCodexHttpTransport {
532
550
  }
533
551
  }
534
552
  if (!response.ok) {
535
- let failure = await httpFailure(response, this.maxDelayMs, watchdog.signal);
553
+ let failure = await httpFailure(response, this.maxDelayMs, watchdog.signal, telemetry);
536
554
  if (generation.signal?.aborted || watchdog.timedOut()) {
537
555
  failure = mappedFailure(failure, watchdog, generation.signal);
538
556
  }
539
557
  watchdog.stop();
540
558
  if (retryable(failure) && transientRetries < this.maxRetries) {
541
- await this.wait(transientRetries++, failure, generation.signal);
559
+ await this.wait(transientRetries++, failure, generation.signal, telemetry);
542
560
  continue;
543
561
  }
544
562
  throw failure;
@@ -554,6 +572,7 @@ export class NativeCodexHttpTransport {
554
572
  for await (const chunk of streamResponses(response.body, {
555
573
  signal: watchdog.signal,
556
574
  onActivity: watchdog.pulse,
575
+ onBytes: bytes => telemetry.activity(bytes),
557
576
  onMalformedEvent: () => {
558
577
  this.options.warn?.('native Codex ignored a malformed SSE event');
559
578
  },
@@ -576,6 +595,7 @@ export class NativeCodexHttpTransport {
576
595
  },
577
596
  })) {
578
597
  emitted = true;
598
+ observeNetworkOutput(telemetry, chunk);
579
599
  if (chunk.type === 'finish'
580
600
  && ['stop', 'tool-calls', 'max-tokens'].includes(chunk.reason.kind))
581
601
  completed = true;
@@ -594,7 +614,7 @@ export class NativeCodexHttpTransport {
594
614
  catch (error) {
595
615
  const failure = mappedFailure(error, watchdog, generation.signal);
596
616
  if (!emitted && retryable(failure) && transientRetries < this.maxRetries) {
597
- await this.wait(transientRetries++, failure, generation.signal);
617
+ await this.wait(transientRetries++, failure, generation.signal, telemetry);
598
618
  continue;
599
619
  }
600
620
  if (emitted && retryable(failure))
@@ -10,6 +10,8 @@ export interface NativeCodexWebSocket {
10
10
  readonly responseHeaders: Readonly<Record<string, string>>;
11
11
  send(text: string, signal?: AbortSignal): Promise<void>;
12
12
  receive(signal?: AbortSignal): Promise<NativeCodexWebSocketFrame>;
13
+ /** Observe incoming payload bytes while a logical inference owns the socket. */
14
+ observeActivity?(observer: (bytes: number) => void): () => void;
13
15
  close(): void;
14
16
  }
15
17
  export interface NativeCodexWebSocketConnectOptions {
@@ -55,10 +55,19 @@ class NodeNativeCodexWebSocket {
55
55
  queue = [];
56
56
  waiters = [];
57
57
  ended = false;
58
+ activityObserver;
59
+ observeActivity(observer) {
60
+ this.activityObserver = observer;
61
+ return () => { if (this.activityObserver === observer)
62
+ this.activityObserver = undefined; };
63
+ }
58
64
  constructor(socket, responseHeaders) {
59
65
  this.socket = socket;
60
66
  this.responseHeaders = responseHeaders;
67
+ socket.on('ping', data => { this.activityObserver?.(data.byteLength); });
68
+ socket.on('pong', data => { this.activityObserver?.(data.byteLength); });
61
69
  socket.on('message', (data, isBinary) => {
70
+ this.activityObserver?.(Array.isArray(data) ? data.reduce((sum, part) => sum + part.byteLength, 0) : data.byteLength);
62
71
  if (isBinary) {
63
72
  this.fail(failure('native Codex WebSocket returned a binary frame', 'WS_PROTOCOL_ERROR'));
64
73
  return;
@@ -40,5 +40,6 @@ export declare class NativeCodexWebSocketTransport implements NativeCodexTranspo
40
40
  private exchange;
41
41
  private attempt;
42
42
  stream(generation: GenerateOptions, mode?: NativeCodexTransportMode): AsyncIterable<StreamChunk>;
43
+ private streamTracked;
43
44
  dispose(): void;
44
45
  }
@@ -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 { networkTelemetry, trackNetwork, observeNetworkOutput } from './network-telemetry.js';
3
4
  import { LlmError, ProviderRequestId, attributionHeaders, } from '@deepseek-ai/dsh-llm';
4
5
  import { nativeCodexAuthorityHash } from './catalog.js';
5
6
  import { NATIVE_CODEX_CONNECTION_FAILED_CODE, NATIVE_CODEX_STREAM_INTERRUPTED_CODE, isNativeCodexConnectionFailure, } from './native-adapter.js';
@@ -238,11 +239,13 @@ export class NativeCodexWebSocketTransport {
238
239
  const jitter = 0.9 + Math.max(0, Math.min(1, random)) * 0.2;
239
240
  return Math.max(1, Math.round(exponential * jitter));
240
241
  }
241
- async wait(retry, error, signal) {
242
+ async wait(retry, error, signal, telemetry) {
242
243
  const delay = this.retryDelay(retry, error);
244
+ telemetry.retry(delay);
243
245
  await (this.options.sleep ?? sleep)(delay, signal);
244
246
  }
245
- async waitForConnection(delayMs, signal) {
247
+ async waitForConnection(delayMs, signal, telemetry) {
248
+ telemetry.retry(delayMs, 'connection');
246
249
  this.options.warn?.(`native Codex network is unavailable; reconnecting in ${delayMs}ms`);
247
250
  await (this.options.sleep ?? sleep)(delayMs, signal);
248
251
  }
@@ -324,7 +327,7 @@ export class NativeCodexWebSocketTransport {
324
327
  ...attributionHeaders(),
325
328
  };
326
329
  }
327
- async ensureSocket(entry, prepared, credential, signal) {
330
+ async ensureSocket(entry, prepared, credential, signal, telemetry) {
328
331
  const fingerprint = socketCredential(credential);
329
332
  if (entry.socket !== undefined && entry.socketCredential === fingerprint)
330
333
  return;
@@ -339,6 +342,7 @@ export class NativeCodexWebSocketTransport {
339
342
  socket.close();
340
343
  throw failure('native Codex WebSocket transport was disposed', 'DISPOSED');
341
344
  }
345
+ telemetry.activity();
342
346
  entry.socket = socket;
343
347
  entry.socketCredential = fingerprint;
344
348
  publishCodexRateLimits(credential.accountId, parseCodexRateLimitHeaders(socket.responseHeaders), this.options.onRateLimits, this.options.warn);
@@ -372,11 +376,12 @@ export class NativeCodexWebSocketTransport {
372
376
  clearTimeout(timer);
373
377
  }
374
378
  }
375
- async *exchange(entry, payload, generation, mode, accountId, prewarm, signal) {
379
+ async *exchange(entry, payload, generation, mode, accountId, prewarm, signal, telemetry) {
376
380
  if (entry.socket === undefined)
377
381
  throw failure('native Codex WebSocket is unavailable', 'WS_RETRYABLE');
378
382
  const encoded = JSON.stringify(payload);
379
383
  await entry.socket.send(encoded, signal);
384
+ telemetry.state('waiting');
380
385
  const translator = new ResponsesStreamTranslator(prewarm ? undefined : {
381
386
  provider: generation.provider,
382
387
  model: mode.publicModel ?? generation.model,
@@ -384,6 +389,8 @@ export class NativeCodexWebSocketTransport {
384
389
  const outputItems = [];
385
390
  while (true) {
386
391
  const text = await this.receive(entry, signal);
392
+ if (entry.socket?.observeActivity === undefined)
393
+ telemetry.activity(Buffer.byteLength(text));
387
394
  let event;
388
395
  try {
389
396
  event = JSON.parse(text);
@@ -432,28 +439,38 @@ export class NativeCodexWebSocketTransport {
432
439
  }
433
440
  }
434
441
  }
435
- async *attempt(entry, prepared, credential, signal) {
436
- await this.ensureSocket(entry, prepared, credential, signal);
437
- let justPrewarmed = false;
438
- if (!entry.prewarmAttempted) {
439
- entry.prewarmAttempted = true;
440
- const warm = entry.protocol.prewarm(withRequestMetadata(prepared.request, entry.turnState, prepared.mode.responsesLite !== undefined));
441
- for await (const _chunk of this.exchange(entry, warm.payload, prepared.generation, prepared.mode, credential.accountId, true, signal)) { /* prewarm is invisible */ }
442
- entry.prewarmSucceeded = true;
443
- justPrewarmed = true;
444
- }
445
- const plan = entry.protocol.plan(withRequestMetadata(prepared.request, entry.turnState, prepared.mode.responsesLite !== undefined), justPrewarmed);
446
- yield* this.exchange(entry, plan.payload, prepared.generation, prepared.mode, credential.accountId, false, signal);
447
- if (this.options.onCompleted !== undefined) {
448
- try {
449
- this.options.onCompleted();
442
+ async *attempt(entry, prepared, credential, signal, telemetry) {
443
+ telemetry.attempt('websocket');
444
+ await this.ensureSocket(entry, prepared, credential, signal, telemetry);
445
+ const stopActivity = entry.socket?.observeActivity?.(bytes => telemetry.activity(bytes));
446
+ try {
447
+ let justPrewarmed = false;
448
+ if (!entry.prewarmAttempted) {
449
+ entry.prewarmAttempted = true;
450
+ const warm = entry.protocol.prewarm(withRequestMetadata(prepared.request, entry.turnState, prepared.mode.responsesLite !== undefined));
451
+ for await (const _chunk of this.exchange(entry, warm.payload, prepared.generation, prepared.mode, credential.accountId, true, signal, telemetry)) { /* prewarm is invisible */ }
452
+ entry.prewarmSucceeded = true;
453
+ justPrewarmed = true;
450
454
  }
451
- catch {
452
- this.options.warn?.('native Codex usage refresh could not be scheduled');
455
+ const plan = entry.protocol.plan(withRequestMetadata(prepared.request, entry.turnState, prepared.mode.responsesLite !== undefined), justPrewarmed);
456
+ yield* this.exchange(entry, plan.payload, prepared.generation, prepared.mode, credential.accountId, false, signal, telemetry);
457
+ if (this.options.onCompleted !== undefined) {
458
+ try {
459
+ this.options.onCompleted();
460
+ }
461
+ catch {
462
+ this.options.warn?.('native Codex usage refresh could not be scheduled');
463
+ }
453
464
  }
454
465
  }
466
+ finally {
467
+ stopActivity?.();
468
+ }
455
469
  }
456
470
  async *stream(generation, mode = {}) {
471
+ yield* trackNetwork(this.options.telemetry ?? networkTelemetry, 'websocket', generation.signal, handle => this.streamTracked(generation, mode, handle));
472
+ }
473
+ async *streamTracked(generation, mode, telemetry) {
457
474
  if (this.disposed)
458
475
  throw failure('native Codex WebSocket transport was disposed', 'DISPOSED');
459
476
  const lifecycle = new AbortController();
@@ -500,7 +517,7 @@ export class NativeCodexWebSocketTransport {
500
517
  let requestCompleted = false;
501
518
  try {
502
519
  if (entry.disabled) {
503
- yield* this.http.stream(activeGeneration, fallbackMode());
520
+ yield* this.http.stream(activeGeneration, fallbackMode(), telemetry);
504
521
  requestCompleted = true;
505
522
  return;
506
523
  }
@@ -509,6 +526,7 @@ export class NativeCodexWebSocketTransport {
509
526
  let attemptedCredential;
510
527
  const enteringPrewarm = !entry.prewarmAttempted;
511
528
  try {
529
+ telemetry.state('preparing');
512
530
  const credential = await this.options.resolveCredential(signal);
513
531
  attemptedCredential = credential;
514
532
  if (pinnedAccountId === undefined)
@@ -517,8 +535,9 @@ export class NativeCodexWebSocketTransport {
517
535
  throw failure('native Codex account changed during request', 'AUTH');
518
536
  }
519
537
  this.assertFastAuthority(credential, mode);
520
- for await (const chunk of this.attempt(entry, prepared, credential, signal)) {
538
+ for await (const chunk of this.attempt(entry, prepared, credential, signal, telemetry)) {
521
539
  emitted = true;
540
+ observeNetworkOutput(telemetry, chunk);
522
541
  yield chunk;
523
542
  }
524
543
  requestCompleted = true;
@@ -543,7 +562,7 @@ export class NativeCodexWebSocketTransport {
543
562
  // the DSH turn in an invisible infinite reconnect loop.
544
563
  if (attemptedCredential === undefined
545
564
  && isNativeCodexConnectionFailure(failureValue)) {
546
- await this.waitForConnection(connectionRetryDelayMs, signal);
565
+ await this.waitForConnection(connectionRetryDelayMs, signal, telemetry);
547
566
  connectionRetryDelayMs = Math.min(connectionRetryDelayMs * 2, MAX_CONNECTION_RETRY_DELAY_MS);
548
567
  continue;
549
568
  }
@@ -555,8 +574,10 @@ export class NativeCodexWebSocketTransport {
555
574
  try {
556
575
  const changed = await this.options.recoverCredential(attemptedCredential, signal);
557
576
  recovered = true;
558
- if (changed)
577
+ if (changed) {
578
+ telemetry.retry(0);
559
579
  continue;
580
+ }
560
581
  throw failure('native Codex rejected the configured credential', 'AUTH');
561
582
  }
562
583
  catch (recoveryError) {
@@ -566,7 +587,7 @@ export class NativeCodexWebSocketTransport {
566
587
  if (generation.signal?.aborted)
567
588
  throw recoveryError;
568
589
  if (isNativeCodexConnectionFailure(recoveryError)) {
569
- await this.waitForConnection(connectionRetryDelayMs, signal);
590
+ await this.waitForConnection(connectionRetryDelayMs, signal, telemetry);
570
591
  connectionRetryDelayMs = Math.min(connectionRetryDelayMs * 2, MAX_CONNECTION_RETRY_DELAY_MS);
571
592
  continue;
572
593
  }
@@ -577,18 +598,19 @@ export class NativeCodexWebSocketTransport {
577
598
  if (failureValue.code !== 'WS_UPGRADE_REQUIRED' && retryable
578
599
  && enteringPrewarm && !entry.prewarmSucceeded) {
579
600
  entry.prewarmAttempted = true;
580
- await this.wait(0, failureValue, signal);
601
+ await this.wait(0, failureValue, signal, telemetry);
581
602
  continue;
582
603
  }
583
604
  if (failureValue.code !== 'WS_UPGRADE_REQUIRED' && retryable
584
605
  && reconnects < this.maxReconnects) {
585
- await this.wait(reconnects, failureValue, signal);
606
+ await this.wait(reconnects, failureValue, signal, telemetry);
586
607
  reconnects++;
587
608
  continue;
588
609
  }
589
610
  if (failureValue.code === 'WS_UPGRADE_REQUIRED' || retryable) {
590
611
  entry.disabled = true;
591
- yield* this.http.stream(activeGeneration, fallbackMode());
612
+ telemetry.retry(0);
613
+ yield* this.http.stream(activeGeneration, fallbackMode(), telemetry);
592
614
  requestCompleted = true;
593
615
  return;
594
616
  }
@@ -0,0 +1,61 @@
1
+ /** Process-wide, content-free inference telemetry. All mutations are synchronous. */
2
+ import type { StreamChunk } from '@deepseek-ai/dsh-llm';
3
+ export type NetworkState = 'preparing' | 'connecting' | 'waiting' | 'streaming' | 'retrying' | 'completed' | 'failed' | 'cancelled';
4
+ export type NetworkTransport = 'http' | 'websocket';
5
+ export interface NetworkRequest {
6
+ id: string;
7
+ state: NetworkState;
8
+ transport: NetworkTransport;
9
+ startedAt: number;
10
+ lastActivityAt: number | null;
11
+ lastOutputAt: number | null;
12
+ retryAt: number | null;
13
+ retryKind: 'transient' | 'connection' | null;
14
+ retries: number;
15
+ receivedBytes: number;
16
+ attempts: number;
17
+ }
18
+ export interface NetworkSnapshot {
19
+ scope: 'process';
20
+ startedAt: number;
21
+ updatedAt: number;
22
+ activeCount: number;
23
+ totals: {
24
+ requests: number;
25
+ succeeded: number;
26
+ failed: number;
27
+ cancelled: number;
28
+ retries: number;
29
+ receivedBytes: number;
30
+ };
31
+ requests: NetworkRequest[];
32
+ truncated: boolean;
33
+ }
34
+ export interface NetworkRequestHandle {
35
+ state(state: 'preparing' | 'waiting'): void;
36
+ attempt(transport: NetworkTransport): void;
37
+ activity(bytes?: number): void;
38
+ output(): void;
39
+ retry(delayMs: number, kind?: 'transient' | 'connection'): void;
40
+ finish(state: 'completed' | 'failed' | 'cancelled'): void;
41
+ }
42
+ /** Keeps at most limit records, preferentially evicting terminal records. Handles
43
+ * remain owned by running generators; overflow never loses aggregate accounting.
44
+ * No timers, request content, external identifiers, or errors are retained. */
45
+ export declare class NetworkTelemetry {
46
+ private readonly limit;
47
+ private readonly now;
48
+ private readonly rows;
49
+ private readonly startedAt;
50
+ private activeCount;
51
+ private sequence;
52
+ private evicted;
53
+ private readonly totals;
54
+ constructor(limit?: number, now?: () => number);
55
+ begin(transport: NetworkTransport): NetworkRequestHandle;
56
+ snapshot(): NetworkSnapshot;
57
+ }
58
+ export declare const networkTelemetry: NetworkTelemetry;
59
+ export declare function observeNetworkOutput(handle: NetworkRequestHandle, chunk: StreamChunk): void;
60
+ /** Generator return/break is cancellation; fallback delegates with the same handle. */
61
+ export declare function trackNetwork<T>(telemetry: NetworkTelemetry, transport: NetworkTransport, signal: AbortSignal | undefined, run: (handle: NetworkRequestHandle) => AsyncIterable<T>): AsyncIterable<T>;
@@ -0,0 +1,104 @@
1
+ /** Keeps at most limit records, preferentially evicting terminal records. Handles
2
+ * remain owned by running generators; overflow never loses aggregate accounting.
3
+ * No timers, request content, external identifiers, or errors are retained. */
4
+ export class NetworkTelemetry {
5
+ limit;
6
+ now;
7
+ rows = new Map();
8
+ startedAt;
9
+ activeCount = 0;
10
+ sequence = 0;
11
+ evicted = false;
12
+ totals = { requests: 0, succeeded: 0, failed: 0, cancelled: 0, retries: 0, receivedBytes: 0 };
13
+ constructor(limit = 128, now = Date.now) {
14
+ this.limit = limit;
15
+ this.now = now;
16
+ if (!Number.isSafeInteger(limit) || limit < 1)
17
+ throw new RangeError('Invalid telemetry limit');
18
+ this.startedAt = now();
19
+ }
20
+ begin(transport) {
21
+ const row = {
22
+ id: String(++this.sequence), state: 'preparing', transport, startedAt: this.now(),
23
+ lastActivityAt: null, lastOutputAt: null, retryAt: null, retryKind: null, retries: 0, receivedBytes: 0, attempts: 0,
24
+ };
25
+ if (this.rows.size >= this.limit) {
26
+ const terminal = [...this.rows.values()].find(item => ['completed', 'failed', 'cancelled'].includes(item.state));
27
+ this.rows.delete(terminal?.id ?? this.rows.keys().next().value);
28
+ this.evicted = true;
29
+ }
30
+ this.rows.set(row.id, row);
31
+ this.activeCount++;
32
+ this.totals.requests++;
33
+ let finished = false;
34
+ const mutate = (fn) => {
35
+ if (finished)
36
+ return;
37
+ fn();
38
+ };
39
+ return {
40
+ state: state => mutate(() => { row.state = state; row.retryAt = null; if (state === 'waiting')
41
+ row.retryKind = null; }),
42
+ attempt: transport => mutate(() => {
43
+ row.transport = transport;
44
+ row.state = 'connecting';
45
+ row.retryAt = null;
46
+ row.attempts++;
47
+ }),
48
+ activity: (bytes = 0) => mutate(() => {
49
+ row.lastActivityAt = this.now();
50
+ if (Number.isSafeInteger(bytes) && bytes > 0) {
51
+ row.receivedBytes += bytes;
52
+ this.totals.receivedBytes += bytes;
53
+ }
54
+ }),
55
+ output: () => mutate(() => { row.state = 'streaming'; row.retryKind = null; row.lastOutputAt = this.now(); }),
56
+ retry: (delayMs, kind = 'transient') => mutate(() => {
57
+ row.retryKind = kind;
58
+ row.state = 'retrying';
59
+ row.retryAt = this.now() + Math.max(0, delayMs);
60
+ row.retries++;
61
+ this.totals.retries++;
62
+ }),
63
+ finish: state => mutate(() => {
64
+ row.state = state;
65
+ row.retryAt = null;
66
+ row.retryKind = null;
67
+ this.activeCount--;
68
+ this.totals[state === 'completed' ? 'succeeded' : state]++;
69
+ finished = true;
70
+ }),
71
+ };
72
+ }
73
+ snapshot() {
74
+ return {
75
+ scope: 'process', startedAt: this.startedAt, updatedAt: this.now(),
76
+ activeCount: this.activeCount, totals: { ...this.totals },
77
+ requests: [...this.rows.values()].reverse().map(row => ({ ...row })), truncated: this.evicted,
78
+ };
79
+ }
80
+ }
81
+ export const networkTelemetry = new NetworkTelemetry();
82
+ export function observeNetworkOutput(handle, chunk) {
83
+ if ((chunk.type === 'text-delta' || chunk.type === 'reasoning-delta') && chunk.text.length > 0
84
+ || chunk.type === 'tool-call-delta')
85
+ handle.output();
86
+ if (chunk.type === 'finish' && chunk.reason.kind === 'error')
87
+ handle.finish('failed');
88
+ }
89
+ /** Generator return/break is cancellation; fallback delegates with the same handle. */
90
+ export async function* trackNetwork(telemetry, transport, signal, run) {
91
+ const handle = telemetry.begin(transport);
92
+ try {
93
+ yield* run(handle);
94
+ handle.finish('completed');
95
+ }
96
+ catch (error) {
97
+ const code = typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined;
98
+ handle.finish(signal?.aborted || code === 'ABORTED' || code === 'DISPOSED' ? 'cancelled' : 'failed');
99
+ throw error;
100
+ }
101
+ finally {
102
+ handle.finish('cancelled');
103
+ }
104
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pure01fx/dsh-openai-codex-auth",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
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": {
@@ -41,6 +41,8 @@
41
41
  "lib/native-adapter.d.ts",
42
42
  "lib/native-http.js",
43
43
  "lib/native-http.d.ts",
44
+ "lib/network-telemetry.js",
45
+ "lib/network-telemetry.d.ts",
44
46
  "lib/native-websocket.js",
45
47
  "lib/native-websocket.d.ts",
46
48
  "lib/native-websocket-session.js",