@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 +6 -0
- package/README.md +1 -1
- package/client.js +115 -2
- package/lib/index.d.ts +1 -0
- package/lib/index.js +11 -0
- package/lib/native-http.d.ts +4 -1
- package/lib/native-http.js +33 -13
- package/lib/native-websocket-socket.d.ts +2 -0
- package/lib/native-websocket-socket.js +9 -0
- package/lib/native-websocket.d.ts +1 -0
- package/lib/native-websocket.js +51 -29
- package/lib/network-telemetry.d.ts +61 -0
- package/lib/network-telemetry.js +104 -0
- package/package.json +3 -1
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.
|
|
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;
|
package/lib/native-http.d.ts
CHANGED
|
@@ -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 {};
|
package/lib/native-http.js
CHANGED
|
@@ -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
|
}
|
package/lib/native-websocket.js
CHANGED
|
@@ -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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
452
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|