@pure01fx/dsh-openai-codex-auth 0.11.0 → 0.11.2
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 +12 -0
- package/CODEX-COMPATIBILITY.md +54 -0
- package/README.md +1 -1
- package/client.js +115 -2
- package/lib/cloud-http.js +12 -7
- package/lib/index.d.ts +1 -0
- package/lib/index.js +11 -0
- package/lib/native-adapter.js +2 -2
- package/lib/native-http.d.ts +4 -1
- package/lib/native-http.js +43 -27
- 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 +61 -32
- package/lib/network-telemetry.d.ts +61 -0
- package/lib/network-telemetry.js +104 -0
- package/lib/responses.js +8 -3
- package/lib/upstream.d.ts +1 -1
- package/lib/upstream.js +1 -1
- package/package.json +9 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.11.2
|
|
6
|
+
|
|
7
|
+
- Requests the live model catalog as Codex 0.155.0 so version-gated GPT-6 Sol can be advertised; preserves dynamic account-specific discovery and routes exact Sol requests through Responses Lite when catalog metadata is unavailable. Fast remains catalog-authorized.
|
|
8
|
+
- Classifies `slow_down` as a rate limit with bounded provider wait hints, recognizes credit/organization/project quota exhaustion, and distinguishes `INVALID_PROMPT` across HTTP, SSE, and WebSocket errors without retrying permanent failures or switching transports.
|
|
9
|
+
- Preserves validated image request IDs and HTTP status on rejected responses and malformed JSON/UTF-8, without exposing credential echoes or retrying ambiguous image POSTs.
|
|
10
|
+
|
|
11
|
+
## 0.11.1
|
|
12
|
+
|
|
13
|
+
- Adds live network status and process-wide request, retry, and received-payload statistics to the quota panel, distinguishing network activity from model output.
|
|
14
|
+
- 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.
|
|
15
|
+
- Exposes bounded, content-free telemetry through the protected local network endpoint without additional upstream quota requests or changes to timeout and retry behavior.
|
|
16
|
+
|
|
5
17
|
## 0.11.0
|
|
6
18
|
|
|
7
19
|
- 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/CODEX-COMPATIBILITY.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
# Codex 云端能力兼容性跟踪
|
|
2
2
|
|
|
3
|
+
## 已实现:Sol 与错误协议增量适配(0.11.2)
|
|
4
|
+
|
|
5
|
+
参考上游 `c44deff7b1083e9660ac55d02122481f1cdf139b`,在既有 0.11.1 工作树上完成以下必要改动。此节取代下方历史审查中相应的“待适配”结论;不代表完整上游运行时对齐。
|
|
6
|
+
|
|
7
|
+
- 模型目录仍动态获取,查询 `client_version` 升至 `0.155.0`,满足上游 Sol 条目的最低版本。仅为缺失目录 metadata 的精确 `gpt-6-sol` 增加 Lite 路由后备;正常目录的 `use_responses_lite` 仍是依据,Fast 仍须目录授权,未知模型不按名称前缀猜测协议。
|
|
8
|
+
- HTTP 错误正文、SSE `response.failed` 与 WS `error` / `response.failed` 共用安全分类。`slow_down` 按限流处理并保留有界等待提示;新增三类 credit/organization/project 额度错误映射宿主 `QUOTA`;`invalid_prompt` 映射独立的 `INVALID_PROMPT`。这些失败不会在传输内重试、重连或回退 HTTP;原有 WS error 包装的限流代码 `RATE_LIMITED` 保持兼容。
|
|
9
|
+
- 图片生成/编辑 HTTP 失败及 JSON/UTF-8 解码失败,通过正式 LlmError failure 元数据保留状态码和校验后的 requestId。图片专用响应头优先;拒绝非法 ID 与 token/account 回显,不增加图片 POST 重试。
|
|
10
|
+
- 新增来自上游的精简 Sol fixture(含全部六档 reasoning effort),覆盖动态目录、最低版本过滤的模拟服务、metadata 优先、缺目录后备、Fast 权限;HTTP/SSE/WS 回归覆盖上述错误的终止行为和隐私边界。模拟版本过滤验证客户端请求条件,不证明真实账号一定获得 Sol。
|
|
11
|
+
|
|
12
|
+
验证:`pnpm build`、`pnpm test`(26 文件 / 467 项)、`pnpm typecheck`、`git diff --check` 全部通过;生成产物与新鲜编译一致。已更新相关 `lib/` 产物和 CHANGELOG,保留全部已有工作树改动。没有运行真实账号推理、查询线上目录或发布/替换常驻 GUI 插件;当前进程是否使用新构建未验证。Provider/cloud 原始跟踪提交仍分别保持 `ddf04ad` / `b348fc26`,为集合 0.3.7 准备插件版本 0.11.2;尚未发布。
|
|
13
|
+
|
|
14
|
+
## 上游增量审查:064e701b(插件 0.11.1 工作树)
|
|
15
|
+
|
|
16
|
+
本次 `../codex` 从 `b348fc26674189f758d5941cdab3f78f258b2aa7` fast-forward 到 `064e701b0fa4f5c0dcaf195a565d088f99bb6af8`,新增 596 个提交;更新后 HEAD 与本次 fetch 的 origin/main 一致,工作树干净。插件以当前已有未提交改动的 0.11.1 工作树为审查对象(Git HEAD 为 `188abd13430b3fff7782c1ee87e5809c4a2e8e96`),不是仅审查已提交版本。
|
|
17
|
+
|
|
18
|
+
结论:建议做针对性的兼容适配;目前证据不支持“必须整体重写”或“已完整对齐最新上游”。本次只更新上游并记录审查,没有修改插件运行时代码、跟踪常量或发布版本。
|
|
19
|
+
|
|
20
|
+
### 优先适配:失败响应分类
|
|
21
|
+
|
|
22
|
+
上游 `codex-rs/codex-api/src/sse/responses.rs` 的变化直接影响现有推理路径:
|
|
23
|
+
|
|
24
|
+
- `slow_down` 现在与 `rate_limit_exceeded` 一样表示限流,并解析服务端建议等待时间。插件 `src/responses.ts:333` 的 `responsesFailure` 仅对后者返回 `RATE_LIMIT`,前者落入 `SERVER`。
|
|
25
|
+
- `credit_balance_exhausted`、`organization_spend_limit_exceeded`、`project_spend_limit_exceeded` 现在明确归为额度耗尽。插件未按这些 code 分类;若 message 未命中宿主的额度文本识别,会落入 `SERVER`,影响不可重试额度错误的判断。
|
|
26
|
+
- 已用现有 `lib/responses.js` 直接调用复现:上述四个 code 搭配中性 message 均返回 `SERVER`,对照 `rate_limit_exceeded` 返回 `RATE_LIMIT`。这是本地确定性复现,不是线上故障记录。
|
|
27
|
+
- `bio_policy` 在上游已独立分类;插件仍返回不可重试的 `INVALID_REQUEST`。这是诊断粒度差异,优先级低于限流和额度分类。
|
|
28
|
+
|
|
29
|
+
建议首个适配单元覆盖错误分类、等待时间,以及 HTTP/SSE、WebSocket 两条传输中的终止/重试语义;仅修改版本号不能解决差异。
|
|
30
|
+
|
|
31
|
+
### 可按需要扩展的能力
|
|
32
|
+
|
|
33
|
+
- **WebSocket 握手稳定性**:本次上游将已有 HTTP 基础设施 cookie store 接入 WSS 握手,并从成功或拒绝的 upgrade 响应回收 Set-Cookie(`websocket-client/src/lib.rs:145`)。插件没有对应 cookie store。可作为下一优先级的稳定性适配,但未验证当前账号存在握手故障。HTTP cookie store 是上次基线已有能力,不能将整个缺口归为本次新增;实现时应遵循 host/TLS 和基础设施 cookie allowlist,不共享账号/会话 cookie。
|
|
34
|
+
- **WebSocket 空增量续传**:上游 `core/src/client.rs:1377` 放宽为允许与已完成上下文相等的 input;插件 `src/native-websocket-session.ts:83` 默认仍要求严格增长,仅预热例外。可减少无新增 input 时的全量重发,当前没有数据丢失证据。
|
|
35
|
+
- **目录 access programs**:新增可选 `available_access_programs`,插件未建模;需要对应模型访问计划选择时再接入,不能单凭字段新增判定目录解析失败。
|
|
36
|
+
|
|
37
|
+
- **工作区路由与 gateway OAuth**:上游增加账号工作区路由发现以及独立 gateway 登录。插件仍限制凭据端点为 chatgpt.com 或 loopback(`src/endpoint.ts`),没有对应发现流程。需要面向这类工作区或网关时再设计端点信任、账号绑定及切换后的缓存/连接隔离。上游 `login/src/auth/manager/workspace_routing.rs:63` 明确保留无 resolver 调用者的旧行为,不能据此认定普通 ChatGPT 登录已失效。
|
|
38
|
+
- **推理强度增量更新**:目录新增 `supports_reasoning_effort_updates`;上游以 `configuration_update` 历史项保留请求基线、减少切换 effort 的缓存损失。该路径受功能开关、provider 和模型能力共同控制(`core/src/client.rs:548`)。插件继续使用请求级参数;属于缓存与功能对齐项目,尚无证据表明普通请求因此不兼容。
|
|
39
|
+
- **图片文件引用**:`protocol/src/models.rs:897` 新增 `ImageReference::File { file_id }`,原有 `image_url` wire 形状仍保留。插件继续发送内联图片;搜索结果只提取 `image_url` 图片,不能将不认识的 file_id 视为已下载附件。可在明确上传、下载和账号作用域后单独接入。
|
|
40
|
+
|
|
41
|
+
### 深入评估:推理增量更新与 file_id
|
|
42
|
+
|
|
43
|
+
推理增量更新建议先做有开关的实验,暂不默认启用。插件当前 `responses.ts:245` 将 effort 放在请求属性中,`native-websocket-session.ts:57` 对请求属性做复用哈希,因此 effort 改变会使增量链不满足复用条件。保持请求基线并追加 configuration_update 有明确的客户端复用收益机制,但服务端缓存收益尚未实测。真正成本是持久化:现有 `replay.ts:7` 只记录 message/reasoning/function_call,不能直接表示宿主生成的配置更新及其历史位置。需先确认宿主能否提供稳定的轮次、分叉、压缩边界和持久化扩展点,再决定插件内扩展是否足够,不能只做进程内 sessionId→effort 缓存。验收应覆盖 medium→high→medium、断线/重启、分叉、失败重试、成功/失败压缩、模型切换以及能力缺失时保留请求级 effort。收益验证比较匹配历史的缓存输入量、请求字节数和首 token 延迟,不预设改善比例。
|
|
44
|
+
|
|
45
|
+
file_id 建议暂缓产品化,先验证后端契约。补充检查发现上游 `attachment-store/src/lib.rs:161` 提供的 InlineAttachmentStore 直接返回原字节;`core/src/thread_manager.rs:498` 默认图片存储仍使用它。`core/src/image_preparation.rs:363` 接受可注入存储返回 File,并在上传失败时回退内联。因而协议存在 file_id 不等于默认 Codex 已经提供现成可复制的图片上传闭环。`codex-api/src/files.rs` 的通用/连接器文件上传也不能未经验证就当成 Responses 图片上传。下一步应以一张合成小图验证上传→finalize→同账号 Responses 识图→引用失效/解析契约;若不能确认接受性与生命周期,继续内联。后续实现优先将远端 ID 作为插件内部、按账号与内容隔离的缓存,保留 DSH 本地附件作恢复依据;应对重新上传、账号切换、并发、取消、签名 URL 凭据隔离,并避免中途改变历史图片表示破坏 WS 前缀。现有 WS 增量已避免重复发送不变历史,因此收益主要在 HTTP、多次新连接或跨请求复用,不能直接宣称节省视觉 token。
|
|
46
|
+
|
|
47
|
+
本轮为源码评估,无线上请求和运行时代码修改;为读取上游接口追加了 `codex-rs/attachment-store` 稀疏检出目录。
|
|
48
|
+
|
|
49
|
+
### 当前无需随上游机械改动的接口
|
|
50
|
+
|
|
51
|
+
本次区间 `codex-api/src/search.rs`、`images.rs`、`endpoint/images.rs` 无变化;`endpoint/search.rs` 保留 `alpha/search`,主要是内部常量和测试类型调整。普通 Responses HTTP/WebSocket 仍使用 `/responses`,移除的是 Guardian 专用分支,不要求插件改路由。模型目录支持自定义 URL 和更多可选元数据,不等于默认目录的旧字段全部失效。
|
|
52
|
+
|
|
53
|
+
### 验证与边界
|
|
54
|
+
|
|
55
|
+
`pnpm test`:26 个文件、384 项测试全部通过;`pnpm typecheck` 通过;测试包括现有生成产物与新鲜 TypeScript 编译的一致性校验。未执行真实 OAuth、刷新或付费推理;现有回归通过不覆盖上游所有新增行为。原 Provider 基线 `ddf04ad` 与云能力基线 `b348fc26` 保持不变。下方历史章节中的旧缺口、旧测试数量和账号验证不能自动视为当前工作树结论。
|
|
56
|
+
|
|
3
57
|
## DSH 0.1.5-rc.1 版本适配
|
|
4
58
|
|
|
5
59
|
`0.10.1` 迁移 ToolCallId、精确 SDK 依赖和客户端服务提供包,隐藏策略在目录发布投影之前执行。`tests/target-directory.spec.ts` 执行官方 shell 的真实 store/Zustand/Immer 与目标目录/catalog 类,使用合成 Remote 响应和投影输入;这不是浏览器联合验收。`tests/target-services.spec.ts` 使用真实 Cordis/LlmRuntime/ToolRuntime、真实 loopback HTTP/SSE/WebSocket,验证工具 ID、工具执行、含加密 reasoning 的历史回放、永久错误和取消,并确认 WS 没有偷偷回退 HTTP。实际 socket 测试也修复了 ws 成功回调为 null 时的误判。
|
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/cloud-http.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Nonstreaming Codex cloud requests. Never retry ambiguous POST failures. */
|
|
2
|
-
import { attributionHeaders, LlmError } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { attributionHeaders, LlmError, ProviderRequestId } from '@deepseek-ai/dsh-llm';
|
|
3
3
|
import { nativeCodexEndpoint } from './endpoint.js';
|
|
4
4
|
const fail = (message, code = 'CODEX_CLOUD_FAILED') => new LlmError(message, code);
|
|
5
5
|
function positive(value, fallback) {
|
|
@@ -93,6 +93,14 @@ export class NativeCodexCloudClient {
|
|
|
93
93
|
catch {
|
|
94
94
|
throw fail(signal.aborted ? 'Codex cloud request cancelled or timed out' : 'Codex cloud request failed; it was not retried', signal.aborted ? 'ABORTED' : 'CODEX_CLOUD_FAILED');
|
|
95
95
|
}
|
|
96
|
+
const rawRequestId = path.startsWith('images/')
|
|
97
|
+
? response.headers.get('x-codex-imagegen-request-id') ?? response.headers.get('x-request-id')
|
|
98
|
+
: response.headers.get('x-request-id');
|
|
99
|
+
// Validate before branding: response headers are untrusted, including credential echoes.
|
|
100
|
+
const requestId = rawRequestId && /^[a-zA-Z0-9_.:-]{1,256}$/.test(rawRequestId)
|
|
101
|
+
&& ![options.credential.accessToken, credential.accessToken, accountId].some(secret => rawRequestId.includes(secret))
|
|
102
|
+
? ProviderRequestId(rawRequestId) : undefined;
|
|
103
|
+
const responseFail = (message, code = 'CODEX_CLOUD_FAILED') => new LlmError(message, code, { status: response.status, ...(requestId === undefined ? {} : { requestId }) });
|
|
96
104
|
if (!response.ok) {
|
|
97
105
|
void response.body?.cancel().catch(() => { });
|
|
98
106
|
if (response.status === 401 && attempt === 0 && this.options.recoverCredential) {
|
|
@@ -110,7 +118,7 @@ export class NativeCodexCloudClient {
|
|
|
110
118
|
continue;
|
|
111
119
|
}
|
|
112
120
|
}
|
|
113
|
-
throw
|
|
121
|
+
throw responseFail('Codex cloud service returned HTTP ' + response.status, response.status === 401 ? 'AUTH_REQUIRED' : 'CODEX_CLOUD_HTTP_ERROR');
|
|
114
122
|
}
|
|
115
123
|
const reader = response.body?.getReader();
|
|
116
124
|
if (!reader)
|
|
@@ -144,12 +152,9 @@ export class NativeCodexCloudClient {
|
|
|
144
152
|
parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
|
|
145
153
|
}
|
|
146
154
|
catch {
|
|
147
|
-
throw
|
|
155
|
+
throw responseFail('Codex cloud service returned invalid JSON');
|
|
148
156
|
}
|
|
149
|
-
|
|
150
|
-
? response.headers.get('x-codex-imagegen-request-id') ?? response.headers.get('x-request-id')
|
|
151
|
-
: response.headers.get('x-request-id');
|
|
152
|
-
return { body: parsed, ...(requestId && /^[a-zA-Z0-9_.:-]{1,256}$/.test(requestId) && !requestId.includes(credential.accessToken) ? { requestId } : {}) };
|
|
157
|
+
return { body: parsed, ...(requestId ? { requestId } : {}) };
|
|
153
158
|
}
|
|
154
159
|
throw fail('Codex cloud authentication failed', 'AUTH_REQUIRED');
|
|
155
160
|
}
|
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-adapter.js
CHANGED
|
@@ -56,8 +56,8 @@ function supportsFast(model) {
|
|
|
56
56
|
function responsesLiteMode(model, requestedModel = model?.slug) {
|
|
57
57
|
if (model !== undefined && !model.useResponsesLite)
|
|
58
58
|
return {};
|
|
59
|
-
// Never downgrade
|
|
60
|
-
if (model === undefined && requestedModel !== 'gpt-6-astra')
|
|
59
|
+
// Never downgrade known Lite-only slugs to Standard Responses during catalog outages.
|
|
60
|
+
if (model === undefined && requestedModel !== 'gpt-6-astra' && requestedModel !== 'gpt-6-sol')
|
|
61
61
|
return {};
|
|
62
62
|
return {
|
|
63
63
|
responsesLite: {
|
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,10 +1,11 @@
|
|
|
1
1
|
/** Native ChatGPT Codex HTTP/SSE transport with safe pre-output retries. */
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import {
|
|
3
|
+
import { networkTelemetry, trackNetwork, observeNetworkOutput } from './network-telemetry.js';
|
|
4
|
+
import { EMPTY_RESPONSE_CODE, LlmError, ProviderRequestId, attributionHeaders, } from '@deepseek-ai/dsh-llm';
|
|
4
5
|
import { nativeCodexAuthorityHash } from './catalog.js';
|
|
5
6
|
import { nativeCodexEndpoint } from './endpoint.js';
|
|
6
7
|
import { NATIVE_CODEX_CONNECTION_FAILED_CODE, NATIVE_CODEX_STREAM_INTERRUPTED_CODE, isNativeCodexConnectionFailure, } from './native-adapter.js';
|
|
7
|
-
import { boundedCodexTurnState, codexRequestBody, codexResponseTurnState, streamResponses, } from './responses.js';
|
|
8
|
+
import { boundedCodexTurnState, codexRequestBody, codexResponseTurnState, streamResponses, responsesFailure, } from './responses.js';
|
|
8
9
|
import { hasNativeCodexReplayKind } from './replay.js';
|
|
9
10
|
import { parseCodexResponseUsageMetadata, publishCodexResponseUsage, } from './response-usage.js';
|
|
10
11
|
import { parseCodexRateLimitEvent, parseCodexRateLimitHeaders, publishCodexRateLimits, } from './rate-limits.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,27 +277,23 @@ 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
|
-
const
|
|
283
|
+
const classified = responsesFailure(detail.code, detail.message);
|
|
282
284
|
if (response.status === 401 || response.status === 403) {
|
|
283
285
|
return fixedFailure('native Codex rejected the configured credential', 'AUTH', facts);
|
|
284
286
|
}
|
|
287
|
+
if (classified.code !== 'SERVER') {
|
|
288
|
+
const delay = facts?.providerRetryAfterMs ?? classified.failure.providerRetryAfterMs;
|
|
289
|
+
return fixedFailure(classified.message, classified.code, {
|
|
290
|
+
...facts,
|
|
291
|
+
...(delay === undefined ? {} : { providerRetryAfterMs: Math.min(delay, maxDelayMs) }),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
285
294
|
if (response.status === 429) {
|
|
286
|
-
if (isQuotaExceededError(classification)) {
|
|
287
|
-
return fixedFailure('native Codex account quota is exhausted', QUOTA_EXCEEDED_CODE, facts);
|
|
288
|
-
}
|
|
289
295
|
return fixedFailure('native Codex request was rate limited', 'RATE_LIMIT', facts);
|
|
290
296
|
}
|
|
291
|
-
if (detail.code === 'context_length_exceeded'
|
|
292
|
-
|| detail.code === 'context_window_exceeded'
|
|
293
|
-
|| isContextWindowExceededError(classification)) {
|
|
294
|
-
return fixedFailure('native Codex request exceeded the model context window', CONTEXT_WINDOW_EXCEEDED_CODE, facts);
|
|
295
|
-
}
|
|
296
|
-
if (isQuotaExceededError(classification)) {
|
|
297
|
-
return fixedFailure('native Codex account quota is exhausted', QUOTA_EXCEEDED_CODE, facts);
|
|
298
|
-
}
|
|
299
297
|
if (response.status === 408 || response.status === 504) {
|
|
300
298
|
return fixedFailure('native Codex request timed out', 'TIMEOUT', facts);
|
|
301
299
|
}
|
|
@@ -374,11 +372,13 @@ export class NativeCodexHttpTransport {
|
|
|
374
372
|
const jitter = 0.9 + Math.max(0, Math.min(1, random)) * 0.2;
|
|
375
373
|
return Math.max(1, Math.round(exponential * jitter));
|
|
376
374
|
}
|
|
377
|
-
async wait(retry, error, signal) {
|
|
375
|
+
async wait(retry, error, signal, telemetry) {
|
|
378
376
|
const delay = this.retryDelay(retry, error.failure.providerRetryAfterMs);
|
|
377
|
+
telemetry.retry(delay);
|
|
379
378
|
await (this.options.sleep ?? sleep)(delay, signal);
|
|
380
379
|
}
|
|
381
|
-
async waitForConnection(delayMs, signal) {
|
|
380
|
+
async waitForConnection(delayMs, signal, telemetry) {
|
|
381
|
+
telemetry.retry(delayMs, 'connection');
|
|
382
382
|
this.options.warn?.(`native Codex network is unavailable; reconnecting in ${delayMs}ms`);
|
|
383
383
|
await (this.options.sleep ?? sleep)(delayMs, signal);
|
|
384
384
|
}
|
|
@@ -419,7 +419,15 @@ export class NativeCodexHttpTransport {
|
|
|
419
419
|
// Serialize only in the selected transport.
|
|
420
420
|
return { generation, mode, request, routingId, routingHint };
|
|
421
421
|
}
|
|
422
|
-
async *stream(generation, mode = {}) {
|
|
422
|
+
async *stream(generation, mode = {}, telemetry) {
|
|
423
|
+
if (telemetry !== undefined) {
|
|
424
|
+
yield* this.streamTracked(generation, mode, telemetry);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
yield* trackNetwork(this.options.telemetry ?? networkTelemetry, 'http', generation.signal, handle => this.streamTracked(generation, mode, handle));
|
|
428
|
+
}
|
|
429
|
+
async *streamTracked(generation, mode, telemetry) {
|
|
430
|
+
telemetry.state('preparing');
|
|
423
431
|
const prepared = await this.prepare(generation, mode);
|
|
424
432
|
const { request, routingId, routingHint } = prepared;
|
|
425
433
|
let body;
|
|
@@ -441,6 +449,7 @@ export class NativeCodexHttpTransport {
|
|
|
441
449
|
let pinnedAccountId = mode.pinnedAccountId;
|
|
442
450
|
while (true) {
|
|
443
451
|
throwIfAborted(generation.signal);
|
|
452
|
+
telemetry.state('preparing');
|
|
444
453
|
const watchdog = attemptWatchdog(generation.signal, this.requestTimeoutMs, this.idleTimeoutMs);
|
|
445
454
|
let response;
|
|
446
455
|
let credential;
|
|
@@ -459,6 +468,7 @@ export class NativeCodexHttpTransport {
|
|
|
459
468
|
throw fixedFailure('native Codex Fast capability authority changed before request', 'FAST_CAPABILITY_UNAVAILABLE');
|
|
460
469
|
}
|
|
461
470
|
fetching = true;
|
|
471
|
+
telemetry.attempt('http');
|
|
462
472
|
response = await this.fetchImpl(this.endpoint, {
|
|
463
473
|
method: 'POST',
|
|
464
474
|
redirect: 'error',
|
|
@@ -491,16 +501,18 @@ export class NativeCodexHttpTransport {
|
|
|
491
501
|
: connection ?? mappedFailure(error, watchdog, generation.signal);
|
|
492
502
|
watchdog.stop();
|
|
493
503
|
if (connection !== undefined) {
|
|
494
|
-
await this.waitForConnection(connectionRetryDelayMs, generation.signal);
|
|
504
|
+
await this.waitForConnection(connectionRetryDelayMs, generation.signal, telemetry);
|
|
495
505
|
connectionRetryDelayMs = Math.min(connectionRetryDelayMs * 2, MAX_CONNECTION_RETRY_DELAY_MS);
|
|
496
506
|
continue;
|
|
497
507
|
}
|
|
498
508
|
if (retryable(failure) && transientRetries < this.maxRetries) {
|
|
499
|
-
await this.wait(transientRetries++, failure, generation.signal);
|
|
509
|
+
await this.wait(transientRetries++, failure, generation.signal, telemetry);
|
|
500
510
|
continue;
|
|
501
511
|
}
|
|
502
512
|
throw failure;
|
|
503
513
|
}
|
|
514
|
+
telemetry.activity();
|
|
515
|
+
telemetry.state('waiting');
|
|
504
516
|
publishCodexRateLimits(credential.accountId, parseCodexRateLimitHeaders(response.headers), this.options.onRateLimits, this.options.warn);
|
|
505
517
|
if (activeTurnState === undefined) {
|
|
506
518
|
const headerTurnState = boundedCodexTurnState(response.headers.get('x-codex-turn-state'));
|
|
@@ -515,8 +527,10 @@ export class NativeCodexHttpTransport {
|
|
|
515
527
|
const changed = await this.options.recoverCredential(credential, watchdog.signal);
|
|
516
528
|
recovered = true;
|
|
517
529
|
watchdog.stop();
|
|
518
|
-
if (changed)
|
|
530
|
+
if (changed) {
|
|
531
|
+
telemetry.retry(0);
|
|
519
532
|
continue;
|
|
533
|
+
}
|
|
520
534
|
throw fixedFailure('native Codex rejected the configured credential', 'AUTH', errorFacts(response, this.maxDelayMs));
|
|
521
535
|
}
|
|
522
536
|
catch (error) {
|
|
@@ -524,7 +538,7 @@ export class NativeCodexHttpTransport {
|
|
|
524
538
|
const failure = connection ?? mappedFailure(error, watchdog, generation.signal);
|
|
525
539
|
watchdog.stop();
|
|
526
540
|
if (connection !== undefined) {
|
|
527
|
-
await this.waitForConnection(connectionRetryDelayMs, generation.signal);
|
|
541
|
+
await this.waitForConnection(connectionRetryDelayMs, generation.signal, telemetry);
|
|
528
542
|
connectionRetryDelayMs = Math.min(connectionRetryDelayMs * 2, MAX_CONNECTION_RETRY_DELAY_MS);
|
|
529
543
|
continue;
|
|
530
544
|
}
|
|
@@ -532,13 +546,13 @@ export class NativeCodexHttpTransport {
|
|
|
532
546
|
}
|
|
533
547
|
}
|
|
534
548
|
if (!response.ok) {
|
|
535
|
-
let failure = await httpFailure(response, this.maxDelayMs, watchdog.signal);
|
|
549
|
+
let failure = await httpFailure(response, this.maxDelayMs, watchdog.signal, telemetry);
|
|
536
550
|
if (generation.signal?.aborted || watchdog.timedOut()) {
|
|
537
551
|
failure = mappedFailure(failure, watchdog, generation.signal);
|
|
538
552
|
}
|
|
539
553
|
watchdog.stop();
|
|
540
554
|
if (retryable(failure) && transientRetries < this.maxRetries) {
|
|
541
|
-
await this.wait(transientRetries++, failure, generation.signal);
|
|
555
|
+
await this.wait(transientRetries++, failure, generation.signal, telemetry);
|
|
542
556
|
continue;
|
|
543
557
|
}
|
|
544
558
|
throw failure;
|
|
@@ -554,6 +568,7 @@ export class NativeCodexHttpTransport {
|
|
|
554
568
|
for await (const chunk of streamResponses(response.body, {
|
|
555
569
|
signal: watchdog.signal,
|
|
556
570
|
onActivity: watchdog.pulse,
|
|
571
|
+
onBytes: bytes => telemetry.activity(bytes),
|
|
557
572
|
onMalformedEvent: () => {
|
|
558
573
|
this.options.warn?.('native Codex ignored a malformed SSE event');
|
|
559
574
|
},
|
|
@@ -576,6 +591,7 @@ export class NativeCodexHttpTransport {
|
|
|
576
591
|
},
|
|
577
592
|
})) {
|
|
578
593
|
emitted = true;
|
|
594
|
+
observeNetworkOutput(telemetry, chunk);
|
|
579
595
|
if (chunk.type === 'finish'
|
|
580
596
|
&& ['stop', 'tool-calls', 'max-tokens'].includes(chunk.reason.kind))
|
|
581
597
|
completed = true;
|
|
@@ -594,7 +610,7 @@ export class NativeCodexHttpTransport {
|
|
|
594
610
|
catch (error) {
|
|
595
611
|
const failure = mappedFailure(error, watchdog, generation.signal);
|
|
596
612
|
if (!emitted && retryable(failure) && transientRetries < this.maxRetries) {
|
|
597
|
-
await this.wait(transientRetries++, failure, generation.signal);
|
|
613
|
+
await this.wait(transientRetries++, failure, generation.signal, telemetry);
|
|
598
614
|
continue;
|
|
599
615
|
}
|
|
600
616
|
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';
|
|
@@ -7,7 +8,7 @@ import { NativeCodexHttpTransport, } from './native-http.js';
|
|
|
7
8
|
import { replayableItemId } from './replay.js';
|
|
8
9
|
import { parseCodexResponseUsageMetadata, publishCodexResponseUsage, } from './response-usage.js';
|
|
9
10
|
import { parseCodexRateLimitEvent, parseCodexRateLimitHeaders, publishCodexRateLimits, } from './rate-limits.js';
|
|
10
|
-
import { ResponsesStreamTranslator, codexResponseTurnState, } from './responses.js';
|
|
11
|
+
import { ResponsesStreamTranslator, responsesFailure, codexResponseTurnState, } from './responses.js';
|
|
11
12
|
import { NodeNativeCodexWebSocketFactory, } from './native-websocket-socket.js';
|
|
12
13
|
import { NativeCodexWebSocketSessionState } from './native-websocket-session.js';
|
|
13
14
|
const WS_BETA = 'responses_websockets=2026-02-06';
|
|
@@ -192,8 +193,15 @@ function eventFailure(event) {
|
|
|
192
193
|
}
|
|
193
194
|
if (status === 403)
|
|
194
195
|
return new LlmError('native Codex WebSocket authorization failed', 'AUTH', facts);
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
const classified = responsesFailure(providerCode, typeof error.message === 'string' ? error.message : undefined);
|
|
197
|
+
if (classified.code !== 'SERVER') {
|
|
198
|
+
const delay = facts?.providerRetryAfterMs ?? classified.failure.providerRetryAfterMs;
|
|
199
|
+
return new LlmError(classified.message, classified.code === 'RATE_LIMIT' ? 'RATE_LIMITED' : classified.code, {
|
|
200
|
+
...facts,
|
|
201
|
+
...(delay === undefined ? {} : { providerRetryAfterMs: Math.min(delay, 120_000) }),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (providerCode === 'usage_limit_reached' || status === 429) {
|
|
197
205
|
return new LlmError('native Codex WebSocket rate limit was reached', 'RATE_LIMITED', facts);
|
|
198
206
|
}
|
|
199
207
|
if (status !== undefined && status >= 500) {
|
|
@@ -238,11 +246,13 @@ export class NativeCodexWebSocketTransport {
|
|
|
238
246
|
const jitter = 0.9 + Math.max(0, Math.min(1, random)) * 0.2;
|
|
239
247
|
return Math.max(1, Math.round(exponential * jitter));
|
|
240
248
|
}
|
|
241
|
-
async wait(retry, error, signal) {
|
|
249
|
+
async wait(retry, error, signal, telemetry) {
|
|
242
250
|
const delay = this.retryDelay(retry, error);
|
|
251
|
+
telemetry.retry(delay);
|
|
243
252
|
await (this.options.sleep ?? sleep)(delay, signal);
|
|
244
253
|
}
|
|
245
|
-
async waitForConnection(delayMs, signal) {
|
|
254
|
+
async waitForConnection(delayMs, signal, telemetry) {
|
|
255
|
+
telemetry.retry(delayMs, 'connection');
|
|
246
256
|
this.options.warn?.(`native Codex network is unavailable; reconnecting in ${delayMs}ms`);
|
|
247
257
|
await (this.options.sleep ?? sleep)(delayMs, signal);
|
|
248
258
|
}
|
|
@@ -324,7 +334,7 @@ export class NativeCodexWebSocketTransport {
|
|
|
324
334
|
...attributionHeaders(),
|
|
325
335
|
};
|
|
326
336
|
}
|
|
327
|
-
async ensureSocket(entry, prepared, credential, signal) {
|
|
337
|
+
async ensureSocket(entry, prepared, credential, signal, telemetry) {
|
|
328
338
|
const fingerprint = socketCredential(credential);
|
|
329
339
|
if (entry.socket !== undefined && entry.socketCredential === fingerprint)
|
|
330
340
|
return;
|
|
@@ -339,6 +349,7 @@ export class NativeCodexWebSocketTransport {
|
|
|
339
349
|
socket.close();
|
|
340
350
|
throw failure('native Codex WebSocket transport was disposed', 'DISPOSED');
|
|
341
351
|
}
|
|
352
|
+
telemetry.activity();
|
|
342
353
|
entry.socket = socket;
|
|
343
354
|
entry.socketCredential = fingerprint;
|
|
344
355
|
publishCodexRateLimits(credential.accountId, parseCodexRateLimitHeaders(socket.responseHeaders), this.options.onRateLimits, this.options.warn);
|
|
@@ -372,11 +383,12 @@ export class NativeCodexWebSocketTransport {
|
|
|
372
383
|
clearTimeout(timer);
|
|
373
384
|
}
|
|
374
385
|
}
|
|
375
|
-
async *exchange(entry, payload, generation, mode, accountId, prewarm, signal) {
|
|
386
|
+
async *exchange(entry, payload, generation, mode, accountId, prewarm, signal, telemetry) {
|
|
376
387
|
if (entry.socket === undefined)
|
|
377
388
|
throw failure('native Codex WebSocket is unavailable', 'WS_RETRYABLE');
|
|
378
389
|
const encoded = JSON.stringify(payload);
|
|
379
390
|
await entry.socket.send(encoded, signal);
|
|
391
|
+
telemetry.state('waiting');
|
|
380
392
|
const translator = new ResponsesStreamTranslator(prewarm ? undefined : {
|
|
381
393
|
provider: generation.provider,
|
|
382
394
|
model: mode.publicModel ?? generation.model,
|
|
@@ -384,6 +396,8 @@ export class NativeCodexWebSocketTransport {
|
|
|
384
396
|
const outputItems = [];
|
|
385
397
|
while (true) {
|
|
386
398
|
const text = await this.receive(entry, signal);
|
|
399
|
+
if (entry.socket?.observeActivity === undefined)
|
|
400
|
+
telemetry.activity(Buffer.byteLength(text));
|
|
387
401
|
let event;
|
|
388
402
|
try {
|
|
389
403
|
event = JSON.parse(text);
|
|
@@ -432,28 +446,38 @@ export class NativeCodexWebSocketTransport {
|
|
|
432
446
|
}
|
|
433
447
|
}
|
|
434
448
|
}
|
|
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();
|
|
449
|
+
async *attempt(entry, prepared, credential, signal, telemetry) {
|
|
450
|
+
telemetry.attempt('websocket');
|
|
451
|
+
await this.ensureSocket(entry, prepared, credential, signal, telemetry);
|
|
452
|
+
const stopActivity = entry.socket?.observeActivity?.(bytes => telemetry.activity(bytes));
|
|
453
|
+
try {
|
|
454
|
+
let justPrewarmed = false;
|
|
455
|
+
if (!entry.prewarmAttempted) {
|
|
456
|
+
entry.prewarmAttempted = true;
|
|
457
|
+
const warm = entry.protocol.prewarm(withRequestMetadata(prepared.request, entry.turnState, prepared.mode.responsesLite !== undefined));
|
|
458
|
+
for await (const _chunk of this.exchange(entry, warm.payload, prepared.generation, prepared.mode, credential.accountId, true, signal, telemetry)) { /* prewarm is invisible */ }
|
|
459
|
+
entry.prewarmSucceeded = true;
|
|
460
|
+
justPrewarmed = true;
|
|
450
461
|
}
|
|
451
|
-
|
|
452
|
-
|
|
462
|
+
const plan = entry.protocol.plan(withRequestMetadata(prepared.request, entry.turnState, prepared.mode.responsesLite !== undefined), justPrewarmed);
|
|
463
|
+
yield* this.exchange(entry, plan.payload, prepared.generation, prepared.mode, credential.accountId, false, signal, telemetry);
|
|
464
|
+
if (this.options.onCompleted !== undefined) {
|
|
465
|
+
try {
|
|
466
|
+
this.options.onCompleted();
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
this.options.warn?.('native Codex usage refresh could not be scheduled');
|
|
470
|
+
}
|
|
453
471
|
}
|
|
454
472
|
}
|
|
473
|
+
finally {
|
|
474
|
+
stopActivity?.();
|
|
475
|
+
}
|
|
455
476
|
}
|
|
456
477
|
async *stream(generation, mode = {}) {
|
|
478
|
+
yield* trackNetwork(this.options.telemetry ?? networkTelemetry, 'websocket', generation.signal, handle => this.streamTracked(generation, mode, handle));
|
|
479
|
+
}
|
|
480
|
+
async *streamTracked(generation, mode, telemetry) {
|
|
457
481
|
if (this.disposed)
|
|
458
482
|
throw failure('native Codex WebSocket transport was disposed', 'DISPOSED');
|
|
459
483
|
const lifecycle = new AbortController();
|
|
@@ -500,7 +524,7 @@ export class NativeCodexWebSocketTransport {
|
|
|
500
524
|
let requestCompleted = false;
|
|
501
525
|
try {
|
|
502
526
|
if (entry.disabled) {
|
|
503
|
-
yield* this.http.stream(activeGeneration, fallbackMode());
|
|
527
|
+
yield* this.http.stream(activeGeneration, fallbackMode(), telemetry);
|
|
504
528
|
requestCompleted = true;
|
|
505
529
|
return;
|
|
506
530
|
}
|
|
@@ -509,6 +533,7 @@ export class NativeCodexWebSocketTransport {
|
|
|
509
533
|
let attemptedCredential;
|
|
510
534
|
const enteringPrewarm = !entry.prewarmAttempted;
|
|
511
535
|
try {
|
|
536
|
+
telemetry.state('preparing');
|
|
512
537
|
const credential = await this.options.resolveCredential(signal);
|
|
513
538
|
attemptedCredential = credential;
|
|
514
539
|
if (pinnedAccountId === undefined)
|
|
@@ -517,8 +542,9 @@ export class NativeCodexWebSocketTransport {
|
|
|
517
542
|
throw failure('native Codex account changed during request', 'AUTH');
|
|
518
543
|
}
|
|
519
544
|
this.assertFastAuthority(credential, mode);
|
|
520
|
-
for await (const chunk of this.attempt(entry, prepared, credential, signal)) {
|
|
545
|
+
for await (const chunk of this.attempt(entry, prepared, credential, signal, telemetry)) {
|
|
521
546
|
emitted = true;
|
|
547
|
+
observeNetworkOutput(telemetry, chunk);
|
|
522
548
|
yield chunk;
|
|
523
549
|
}
|
|
524
550
|
requestCompleted = true;
|
|
@@ -543,7 +569,7 @@ export class NativeCodexWebSocketTransport {
|
|
|
543
569
|
// the DSH turn in an invisible infinite reconnect loop.
|
|
544
570
|
if (attemptedCredential === undefined
|
|
545
571
|
&& isNativeCodexConnectionFailure(failureValue)) {
|
|
546
|
-
await this.waitForConnection(connectionRetryDelayMs, signal);
|
|
572
|
+
await this.waitForConnection(connectionRetryDelayMs, signal, telemetry);
|
|
547
573
|
connectionRetryDelayMs = Math.min(connectionRetryDelayMs * 2, MAX_CONNECTION_RETRY_DELAY_MS);
|
|
548
574
|
continue;
|
|
549
575
|
}
|
|
@@ -555,8 +581,10 @@ export class NativeCodexWebSocketTransport {
|
|
|
555
581
|
try {
|
|
556
582
|
const changed = await this.options.recoverCredential(attemptedCredential, signal);
|
|
557
583
|
recovered = true;
|
|
558
|
-
if (changed)
|
|
584
|
+
if (changed) {
|
|
585
|
+
telemetry.retry(0);
|
|
559
586
|
continue;
|
|
587
|
+
}
|
|
560
588
|
throw failure('native Codex rejected the configured credential', 'AUTH');
|
|
561
589
|
}
|
|
562
590
|
catch (recoveryError) {
|
|
@@ -566,7 +594,7 @@ export class NativeCodexWebSocketTransport {
|
|
|
566
594
|
if (generation.signal?.aborted)
|
|
567
595
|
throw recoveryError;
|
|
568
596
|
if (isNativeCodexConnectionFailure(recoveryError)) {
|
|
569
|
-
await this.waitForConnection(connectionRetryDelayMs, signal);
|
|
597
|
+
await this.waitForConnection(connectionRetryDelayMs, signal, telemetry);
|
|
570
598
|
connectionRetryDelayMs = Math.min(connectionRetryDelayMs * 2, MAX_CONNECTION_RETRY_DELAY_MS);
|
|
571
599
|
continue;
|
|
572
600
|
}
|
|
@@ -577,18 +605,19 @@ export class NativeCodexWebSocketTransport {
|
|
|
577
605
|
if (failureValue.code !== 'WS_UPGRADE_REQUIRED' && retryable
|
|
578
606
|
&& enteringPrewarm && !entry.prewarmSucceeded) {
|
|
579
607
|
entry.prewarmAttempted = true;
|
|
580
|
-
await this.wait(0, failureValue, signal);
|
|
608
|
+
await this.wait(0, failureValue, signal, telemetry);
|
|
581
609
|
continue;
|
|
582
610
|
}
|
|
583
611
|
if (failureValue.code !== 'WS_UPGRADE_REQUIRED' && retryable
|
|
584
612
|
&& reconnects < this.maxReconnects) {
|
|
585
|
-
await this.wait(reconnects, failureValue, signal);
|
|
613
|
+
await this.wait(reconnects, failureValue, signal, telemetry);
|
|
586
614
|
reconnects++;
|
|
587
615
|
continue;
|
|
588
616
|
}
|
|
589
617
|
if (failureValue.code === 'WS_UPGRADE_REQUIRED' || retryable) {
|
|
590
618
|
entry.disabled = true;
|
|
591
|
-
|
|
619
|
+
telemetry.retry(0);
|
|
620
|
+
yield* this.http.stream(activeGeneration, fallbackMode(), telemetry);
|
|
592
621
|
requestCompleted = true;
|
|
593
622
|
return;
|
|
594
623
|
}
|
|
@@ -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/lib/responses.js
CHANGED
|
@@ -282,16 +282,21 @@ export function responsesFailure(code, message) {
|
|
|
282
282
|
|| isContextWindowExceededError(detail)) {
|
|
283
283
|
return fixedError('native Codex request exceeded the model context window', CONTEXT_WINDOW_EXCEEDED_CODE);
|
|
284
284
|
}
|
|
285
|
-
if (code === 'insufficient_quota' ||
|
|
285
|
+
if (code === 'insufficient_quota' || code === 'credit_balance_exhausted'
|
|
286
|
+
|| code === 'organization_spend_limit_exceeded' || code === 'project_spend_limit_exceeded'
|
|
287
|
+
|| isQuotaExceededError(detail)) {
|
|
286
288
|
return fixedError('native Codex account quota is exhausted', QUOTA_EXCEEDED_CODE);
|
|
287
289
|
}
|
|
288
|
-
if (code === 'rate_limit_exceeded') {
|
|
290
|
+
if (code === 'rate_limit_exceeded' || code === 'slow_down') {
|
|
289
291
|
const delay = retryDelay(message);
|
|
290
292
|
return new LlmError('native Codex request was rate limited', 'RATE_LIMIT', {
|
|
291
293
|
...delay === undefined ? {} : { providerRetryAfterMs: delay },
|
|
292
294
|
});
|
|
293
295
|
}
|
|
294
|
-
if (code === 'invalid_prompt'
|
|
296
|
+
if (code === 'invalid_prompt') {
|
|
297
|
+
return fixedError('native Codex rejected the prompt', 'INVALID_PROMPT');
|
|
298
|
+
}
|
|
299
|
+
if (code === 'bio_policy') {
|
|
295
300
|
return fixedError('native Codex rejected the request', 'INVALID_REQUEST');
|
|
296
301
|
}
|
|
297
302
|
return fixedError('native Codex reported a failed response', 'SERVER');
|
package/lib/upstream.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ export declare const TRACKED_CODEX_RELEASE = "main@ddf04ad";
|
|
|
5
5
|
/** Independently audited cloud search/image contracts; not local Codex executor parity. */
|
|
6
6
|
export declare const TRACKED_CODEX_CLOUD_COMMIT = "b348fc26674189f758d5941cdab3f78f258b2aa7";
|
|
7
7
|
/** Whole stable release version sent to the Codex model-catalog endpoint. */
|
|
8
|
-
export declare const CODEX_CLIENT_VERSION = "0.
|
|
8
|
+
export declare const CODEX_CLIENT_VERSION = "0.155.0";
|
package/lib/upstream.js
CHANGED
|
@@ -5,4 +5,4 @@ export const TRACKED_CODEX_RELEASE = 'main@ddf04ad';
|
|
|
5
5
|
/** Independently audited cloud search/image contracts; not local Codex executor parity. */
|
|
6
6
|
export const TRACKED_CODEX_CLOUD_COMMIT = 'b348fc26674189f758d5941cdab3f78f258b2aa7';
|
|
7
7
|
/** Whole stable release version sent to the Codex model-catalog endpoint. */
|
|
8
|
-
export const CODEX_CLIENT_VERSION = '0.
|
|
8
|
+
export const CODEX_CLIENT_VERSION = '0.155.0';
|
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.2",
|
|
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",
|
|
@@ -86,12 +88,6 @@
|
|
|
86
88
|
"LICENSE",
|
|
87
89
|
"CODEX-COMPATIBILITY.md"
|
|
88
90
|
],
|
|
89
|
-
"scripts": {
|
|
90
|
-
"build": "tsc",
|
|
91
|
-
"typecheck": "tsc --noEmit",
|
|
92
|
-
"test": "vitest run",
|
|
93
|
-
"prepack": "pnpm build"
|
|
94
|
-
},
|
|
95
91
|
"dsh": {
|
|
96
92
|
"engines": {
|
|
97
93
|
"dsh": "0.1.5-rc.1"
|
|
@@ -174,5 +170,10 @@
|
|
|
174
170
|
"@deepseek-ai/dsh-sandbox-policy": {
|
|
175
171
|
"optional": true
|
|
176
172
|
}
|
|
173
|
+
},
|
|
174
|
+
"scripts": {
|
|
175
|
+
"build": "tsc",
|
|
176
|
+
"typecheck": "tsc --noEmit",
|
|
177
|
+
"test": "vitest run"
|
|
177
178
|
}
|
|
178
|
-
}
|
|
179
|
+
}
|