dsh-codex-subscription 1.7.4 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -6,7 +6,7 @@ Use this guide when a user asks an Agent to install, update, verify, or remove
6
6
  ## Safety
7
7
 
8
8
  - Confirm the target DSH installation and profile. Use `web` only when it is the user's target.
9
- - Use the exact `1.7.4` package below; do not install a moving branch.
9
+ - Use the exact `1.8.0` package below; do not install a moving branch.
10
10
  - Never print OAuth credentials, account IDs, authorization callbacks, or credential-store contents.
11
11
  - Preserve the DSH profile, unrelated plugins, sessions, and saved sign-in.
12
12
  - Do not start, stop, or restart DSH without explicit permission.
@@ -34,13 +34,13 @@ For an official DSH installation run through npm, keep the complete `npx` prefix
34
34
  run command does not create a global `dsh` command:
35
35
 
36
36
  ```sh
37
- npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web add dsh-codex-subscription@1.7.4
37
+ npx -y @deepseek-ai/dsh@0.1.1-rc.2 plugin --profile web add dsh-codex-subscription@1.8.0
38
38
  ```
39
39
 
40
40
  With an existing global `dsh` command:
41
41
 
42
42
  ```sh
43
- dsh plugin --profile web add dsh-codex-subscription@1.7.4
43
+ dsh plugin --profile web add dsh-codex-subscription@1.8.0
44
44
  ```
45
45
 
46
46
  Use the same `add` command to update or repair. This is the complete package-changing operation.
@@ -58,7 +58,7 @@ dsh --profile web --dump-config
58
58
 
59
59
  For the official npm route, run the same checks with `npx -y @deepseek-ai/dsh@0.1.1-rc.2` in place of `dsh`.
60
60
 
61
- 1. `dsh-codex-subscription` version `1.7.4` appears exactly once.
61
+ 1. `dsh-codex-subscription` version `1.8.0` appears exactly once.
62
62
  2. `codex-subscription` appears exactly once in the composed config.
63
63
  3. No unrelated plugin or profile was changed and DSH was not restarted.
64
64
 
package/DIRECTORY.md CHANGED
@@ -18,7 +18,7 @@ Use a ChatGPT subscription in DeepSeek Harness through independent OAuth sign-in
18
18
  - Codex CLI required: no
19
19
  - OpenAI API key required: no
20
20
  - Platforms: official DSH plus DSH-Portable product forms for Windows, macOS, and Linux
21
- - Main capabilities: Codex models, quota and expiring resets, web search, Beta image generation/editing with explicit references and native preview/download, optional composer quota, Fast mode, secret-free support diagnostics and direct feedback
21
+ - Main capabilities: Codex models, model-aware context windows, quota and expiring resets, web search, Beta image generation/editing with explicit references and native preview/download, optional composer quota, Fast mode, existing-proxy adaptation, secret-free actionable support diagnostics, and direct feedback
22
22
  - Safety boundary: subscription failures remain visible and never silently fall back to another paid provider
23
23
 
24
24
  ## Install
package/README.md CHANGED
@@ -51,6 +51,7 @@ DSH-Portable exposes the same standard plugin command, so the command above also
51
51
  | **Subscription search** | Explicitly choose DSH default search or Codex subscription search |
52
52
  | **Codex image generation and editing (Beta)** | Generate without references, or explicitly edit one selected image; preview, zoom, annotate regions, download the original, and continue in the same composer |
53
53
  | **Fast mode** | Switch between Standard and Fast directly in the composer |
54
+ | **Model-aware context** | Keep catalog defaults, use each model's supported extended window, or enter a full numeric token limit for each model |
54
55
 
55
56
  These capabilities reuse the same local ChatGPT sign-in. Subscription routing failures stay visible and never silently switch to another paid route.
56
57
 
@@ -96,12 +97,13 @@ Open PowerShell and paste this one line:
96
97
  irm 'https://github.com/WSL043/dsh-codex-subscription/releases/latest/download/dsh-codex-setup.ps1' | iex
97
98
  ```
98
99
 
99
- The lightweight setup checks the current folder, the system command, common locations, and any running official DSH or
100
- [DSH-Portable](https://github.com/WSL043/DSH-Portable), then invokes the official `plugin add`
101
- operation once. It does not recursively scan disks, install pnpm, create a resident command, snapshot a
102
- profile, or download the plugin twice. It needs no administrator access and never restarts DSH. Only
103
- when no existing DSH is found does it use the official npm route pinned to `0.1.1-rc.2`; the setup warns
104
- that first-time dependency resolution can take a while.
100
+ The lightweight setup checks the current folder, the system command, common locations, and running official DSH or
101
+ [DSH-Portable](https://github.com/WSL043/DSH-Portable). It asks before choosing when more than one target is available.
102
+ Portable keeps using its public launcher. For official DSH, setup uses the selected profile home and a checksum-verified
103
+ pnpm helper pinned to the accepted DSH release; the helper is cached under the current Windows user when no working pnpm
104
+ command is available. It does not recursively scan disks, create a resident command, snapshot a profile, require
105
+ administrator access, or restart DSH. A recognized release-age rejection may retry the same pinned add once for that
106
+ command only.
105
107
 
106
108
  <details>
107
109
  <summary>Official npm route (Node.js installed)</summary>
@@ -146,9 +148,12 @@ Restart DSH manually after installation, then:
146
148
  - Visible quota-reset count and earliest expiry, with deliberate early redemption, layered confirmation, and no automatic retry;
147
149
  - Optional percentage or progress bar for the selected Codex model (off by default);
148
150
  - Standard or Fast mode for supported Codex models directly in the composer;
149
- - A copyable support report and direct feedback link in Settings; the report excludes OAuth credentials, account identifiers, and authorization timestamps;
151
+ - Standard, Extended, and per-model Custom context windows; Custom accepts a full numeric token count, stays within each audited model capacity, and feeds DSH's native agent compaction policy;
152
+ - A copyable support report and direct feedback link in Settings; the report includes bounded request stages, HTTP/transport classes, elapsed ranges, and route source types while excluding OAuth credentials, account identifiers, proxy addresses, and authorization timestamps;
150
153
  - Visible errors when subscription routing is unavailable, with no silent paid fallback.
151
154
 
155
+ The plugin can follow an existing HTTPS proxy from the process environment or operating-system proxy settings for official OpenAI and ChatGPT requests. It does not provide a proxy, relay, node list, or system-proxy configuration.
156
+
152
157
  ### Composer quota
153
158
 
154
159
  <p align="center">
@@ -214,7 +219,7 @@ dsh plugin --profile web remove dsh-codex-subscription
214
219
  - **`dsh` is not recognized:** the official npm route does not create a global `dsh` command; use the complete `npx -y @deepseek-ai/dsh@0.1.1-rc.2 ...` command above;
215
220
  - **More than one DSH exists:** run the standard command from the intended DSH environment. For an Agent, specify the target or pass `-DshPath` explicitly;
216
221
  - **Setup still fails:** send the Agent guide above to an Agent. Do not delete the profile or change the system PATH to force an install.
217
- - **Need to report a problem:** generate a **Support diagnostics** report at the bottom of Settings, then open the [bug report form](https://github.com/WSL043/dsh-codex-subscription/issues/new?template=install-problem.yml). The report includes the OS/runtime and a bounded sign-in phase, but excludes credentials, account identifiers, raw errors, and full logs. Paste it into the required diagnostics field; never attach sign-in URLs, authorization codes, or browser callback addresses.
222
+ - **Need to report a problem:** generate a **Support diagnostics** report at the bottom of Settings, then open the [bug report form](https://github.com/WSL043/dsh-codex-subscription/issues/new?template=install-problem.yml). The report includes the OS/runtime, bounded sign-in phase, and safe request-failure categories, but excludes credentials, account identifiers, proxy addresses, raw responses, and full logs. Paste it into the required diagnostics field; never attach sign-in URLs, authorization codes, or browser callback addresses.
218
223
 
219
224
  The ChatGPT Codex backend and DSH can change independently. This community project is not affiliated with or endorsed by DeepSeek or OpenAI.
220
225
 
package/README.zh-CN.md CHANGED
@@ -52,6 +52,7 @@ DSH-Portable 也提供相同的标准插件命令,因此同样使用上面的
52
52
  | **订阅搜索** | 可在 DSH 默认搜索与 Codex 订阅搜索之间明确切换 |
53
53
  | **Codex 图片生成与编辑(Beta)** | 可无参考图全新生成,也可明确选择会话图片继续编辑;支持预览、缩放和下载原图 |
54
54
  | **高速模式** | 直接在输入框切换标准或高速,无需离开当前会话 |
55
+ | **模型感知上下文** | 可保留目录默认值、按模型启用扩展窗口,或为每个模型填写完整数字 Token 上限 |
55
56
 
56
57
  这些能力共用同一份本机 ChatGPT 登录。订阅路由失败时会明确报错,不会静默切换到其他付费路由。
57
58
 
@@ -98,10 +99,11 @@ irm 'https://github.com/WSL043/dsh-codex-subscription/releases/latest/download/d
98
99
  ```
99
100
 
100
101
  这个轻量助手会检查当前目录、系统命令、常见位置以及正在运行的官方 DSH 或
101
- [DSH-Portable](https://github.com/WSL043/DSH-Portable),然后调用一次官方
102
- `plugin add`。它不会递归扫盘、安装 pnpm、创建常驻命令、保存 profile 快照或重复下载插件。
103
- 无需管理员权限,也不会擅自重启 DSH。找不到现有 DSH 时才会使用固定为 `0.1.1-rc.2`
104
- 的官方 npm 运行方式;首次解析依赖可能较慢,安装器会先明确提示。
102
+ [DSH-Portable](https://github.com/WSL043/DSH-Portable);存在多个目标时会先让用户选择。
103
+ Portable 继续使用它的公开启动器。安装到官方 DSH 时,助手会使用选定的配置目录和固定到已验收
104
+ DSH 版本的 pnpm 助手;系统没有可用 pnpm 时,会在当前 Windows 用户目录下缓存经过校验的副本。
105
+ 它不会递归扫盘、创建常驻命令、保存 profile 快照、要求管理员权限或擅自重启 DSH。只有识别到
106
+ 发布时间策略拦截时,才会仅对同一个固定安装命令重试一次。
105
107
 
106
108
  <details>
107
109
  <summary>官方 npm 方式(已安装 Node.js)</summary>
@@ -146,9 +148,12 @@ dsh --profile web --dump-config
146
148
  - 显示重置卡数量与最早到期时间,也允许在额度未完全用尽时主动尝试,并经过分层确认且不会自动重试;
147
149
  - 输入框可用百分比或进度条显示当前 Codex 模型的剩余额度(默认关闭);
148
150
  - 输入框可为支持的 Codex 模型切换标准或高速模式;
149
- - 设置页可生成并复制无敏感信息的支持诊断,并直接打开反馈入口;报告不包含 OAuth 凭据、账号标识或授权时间;
151
+ - 上下文窗口提供标准、扩展和逐模型自定义;自定义直接填写完整 Token 数值,并在已审核的模型容量内交给 DSH 原生 Agent 压缩策略处理;
152
+ - 设置页可生成并复制无敏感信息的支持诊断,并直接打开反馈入口;报告包含有限的请求阶段、HTTP/网络分类、耗时区间和路由来源类型,但不包含 OAuth 凭据、账号标识、代理地址或授权时间;
150
153
  - 订阅路由不可用时明确报错,不会静默切换到其他付费路由。
151
154
 
155
+ 对于 OpenAI 与 ChatGPT 官方请求,插件可以沿用进程环境或操作系统中已有的 HTTPS 代理。插件本身不提供代理、转发服务、节点列表,也不会修改系统代理设置。
156
+
152
157
  ### 输入框额度
153
158
 
154
159
  <p align="center">
@@ -210,7 +215,7 @@ dsh plugin --profile web remove dsh-codex-subscription
210
215
  - **`dsh` 无法识别**:官方 npm 方式本来就不会创建全局 `dsh` 命令,请使用上面的完整 `npx -y @deepseek-ai/dsh@0.1.1-rc.2 ...` 命令;
211
216
  - **电脑上有多个 DSH**:请在目标 DSH 环境中运行标准命令;交给 Agent 时明确目标,或传入 `-DshPath`;
212
217
  - **安装仍然失败**:把上面的 Agent 文档链接发给 Agent,不要删除 profile 或随意修改系统 PATH。
213
- - **需要提交问题**:在设置页底部生成“支持诊断”,然后打开[使用问题表单](https://github.com/WSL043/dsh-codex-subscription/issues/new?template=install-problem.yml)。报告包含系统/运行时和有限的登录阶段,但不含凭据、账号标识、原始错误或完整日志;请粘贴到必填诊断栏,且不要附上登录链接、授权码或浏览器回调地址。
218
+ - **需要提交问题**:在设置页底部生成“支持诊断”,然后打开[使用问题表单](https://github.com/WSL043/dsh-codex-subscription/issues/new?template=install-problem.yml)。报告包含系统/运行时、有限的登录阶段和安全的请求失败分类,但不含凭据、账号标识、代理地址、原始响应或完整日志;请粘贴到必填诊断栏,且不要附上登录链接、授权码或浏览器回调地址。
214
219
 
215
220
  ## 边界与支持
216
221
 
package/lib/client.js CHANGED
@@ -60,8 +60,50 @@ window.__ModuleLoader__.load({
60
60
  const SPEED_MODE_STANDARD = "standard";
61
61
  const SPEED_MODE_FAST = "fast";
62
62
  const DEFAULT_SPEED_MODE = SPEED_MODE_STANDARD;
63
+ const CONTEXT_MODE_FIELD = "contextMode";
64
+ const CONTEXT_MODE_STANDARD = "standard";
65
+ const CONTEXT_MODE_EXTENDED = "extended";
66
+ const CONTEXT_MODE_CUSTOM = "custom";
67
+ const DEFAULT_CONTEXT_MODE = CONTEXT_MODE_STANDARD;
68
+ const CUSTOM_CONTEXT_WINDOW_FIELD = "customContextWindow";
69
+ const DEFAULT_CUSTOM_CONTEXT_WINDOW = 272e3;
70
+ const MIN_CUSTOM_CONTEXT_WINDOW = 128e3;
71
+ const MAX_CUSTOM_CONTEXT_WINDOW = 1e6;
72
+ const CUSTOM_CONTEXT_MODEL_FIELDS = Object.freeze({
73
+ "gpt-5.4": "customContextGpt54",
74
+ "gpt-5.4-mini": "customContextGpt54Mini",
75
+ "gpt-5.5": "customContextGpt55",
76
+ "gpt-5.6": "customContextGpt56"
77
+ });
78
+ const CUSTOM_CONTEXT_MODEL_CAPS = Object.freeze({
79
+ "gpt-5.4": 1e6,
80
+ "gpt-5.4-mini": 4e5,
81
+ "gpt-5.5": 1e6,
82
+ "gpt-5.6": 1e6
83
+ });
84
+ const CUSTOM_CONTEXT_MODEL_DEFAULTS = Object.freeze({
85
+ "gpt-5.4": 272e3,
86
+ "gpt-5.4-mini": 272e3,
87
+ "gpt-5.5": 272e3,
88
+ "gpt-5.6": 272e3
89
+ });
63
90
  const normalizeSearchProvider = (value) => ["dsh", "codex"].includes(value) ? value : "dsh";
64
91
  const normalizeSpeedMode = (value) => ["standard", "fast"].includes(value) ? value : DEFAULT_SPEED_MODE;
92
+ const normalizeContextMode = (value) => [
93
+ "standard",
94
+ "extended",
95
+ "custom"
96
+ ].includes(value) ? value : DEFAULT_CONTEXT_MODE;
97
+ const normalizeCustomContextWindow = (value, maximum = MAX_CUSTOM_CONTEXT_WINDOW) => {
98
+ if (!Number.isInteger(value)) return DEFAULT_CUSTOM_CONTEXT_WINDOW;
99
+ return Math.min(Math.max(value, MIN_CUSTOM_CONTEXT_WINDOW), maximum);
100
+ };
101
+ const formatContextWindow = (value) => value === 1e6 ? "1M" : `${Math.round(value / 1e3)}K`;
102
+ const parseContextWindow = (value) => {
103
+ const match = /^\s*(\d+)\s*$/u.exec(String(value));
104
+ if (match === null) return NaN;
105
+ return Number(match[1]);
106
+ };
65
107
  const normalizeQuickQuotaMode = (value, legacyVisible = false) => [
66
108
  "off",
67
109
  "percent",
@@ -231,6 +273,16 @@ window.__ModuleLoader__.load({
231
273
  quickQuotaOff: "关闭",
232
274
  quickQuotaPercent: "百分比",
233
275
  quickQuotaBar: "进度条",
276
+ contextTitle: "上下文窗口",
277
+ contextStandard: "标准",
278
+ contextStandardHint: "使用模型目录默认值;官方 Agent 预设会自动管理上下文。",
279
+ contextExtended: "扩展",
280
+ contextExtendedHint: "按模型使用 400K 或 1M;超过 272K 后可能消耗更多额度。",
281
+ contextCustom: "自定义",
282
+ contextCustomHint: "输入完整 Token 数值;较低数值会让官方 Agent 预设更早压缩上下文。",
283
+ contextTokens: "Token 上限",
284
+ contextFixed: "固定 {value}",
285
+ contextMaximum: "范围 128000–{value}",
234
286
  quickQuotaStatus: "Codex 剩余额度 {value}%",
235
287
  speedTitle: "速度",
236
288
  speedStandard: "标准",
@@ -363,6 +415,16 @@ window.__ModuleLoader__.load({
363
415
  quickQuotaOff: "Off",
364
416
  quickQuotaPercent: "Percent",
365
417
  quickQuotaBar: "Progress bar",
418
+ contextTitle: "Context window",
419
+ contextStandard: "Standard",
420
+ contextStandardHint: "Use the model catalog default; official agent presets manage context automatically.",
421
+ contextExtended: "Extended",
422
+ contextExtendedHint: "Uses 400K or 1M by model; usage above 272K may consume more quota.",
423
+ contextCustom: "Custom",
424
+ contextCustomHint: "Enter the full token count; lower values make official agent presets compact sooner.",
425
+ contextTokens: "Token limit",
426
+ contextFixed: "Fixed {value}",
427
+ contextMaximum: "128000–{value}",
366
428
  quickQuotaStatus: "Codex quota: {value}% remaining",
367
429
  speedTitle: "Speed",
368
430
  speedStandard: "Standard",
@@ -417,6 +479,7 @@ window.__ModuleLoader__.load({
417
479
  .codexSubscriptionCard{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1);padding:14px 16px;display:flex;flex-direction:column;gap:12px}
418
480
  .codexSubscriptionUsageCard{padding:12px 14px;gap:9px}.codexSubscriptionPreferencesCard{padding:12px 14px;gap:10px}.codexSubscriptionPreference{min-height:32px;box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:12px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}.codexSubscriptionPreferenceCopy{display:flex;min-width:0;flex-direction:column;gap:2px}.codexSubscriptionPreferenceLabel{display:flex;align-items:center;gap:6px}
419
481
  .codexSubscriptionQuotaModes{display:flex;align-items:center;gap:3px;padding:2px;border-radius:9px;background:var(--dsw-alias-bg-module-platform)}.codexSubscriptionQuotaMode{position:relative;display:flex;align-items:center;justify-content:center;min-height:26px;padding:0 9px;border-radius:7px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;cursor:pointer}.codexSubscriptionQuotaMode:has(input:checked){background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);box-shadow:0 0 0 1px var(--dsw-alias-border-l3)}.codexSubscriptionQuotaMode:has(input:focus-visible){outline:2px solid var(--dsw-alias-border-l3);outline-offset:1px}.codexSubscriptionQuotaMode:has(input:disabled){cursor:not-allowed;opacity:.5}.codexSubscriptionQuotaMode input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
482
+ .codexSubscriptionContext{display:flex;flex-direction:column;gap:8px}.codexSubscriptionContextHead{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.codexSubscriptionContextCopy{display:flex;min-width:0;flex:1;flex-direction:column;gap:2px}.codexSubscriptionContextHint{font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionContextTrigger{height:32px;min-width:108px;display:inline-flex;align-items:center;justify-content:space-between;gap:10px;padding:0 10px 0 12px;border:0;border-radius:999px;outline:0;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;cursor:pointer}.codexSubscriptionContextTrigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.codexSubscriptionContextTrigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}.codexSubscriptionContextTrigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:not-allowed}.codexSubscriptionContextTrigger svg{color:var(--dsw-alias-label-tertiary);transition:transform 120ms var(--ds-ease-in-out)}.codexSubscriptionContextTrigger[aria-expanded=true] svg{transform:rotate(180deg)}.codexSubscriptionContextModels{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2)}.codexSubscriptionContextModel{min-height:42px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--dsw-alias-border-l2)}.codexSubscriptionContextModel:last-child{border-bottom:0}.codexSubscriptionContextModelCopy{display:flex;min-width:0;flex-direction:column}.codexSubscriptionContextModelCopy strong{font-size:12px;line-height:18px;font-weight:500}.codexSubscriptionContextModelCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionContextInput{width:116px}
420
483
  .codexSubscriptionSwitch{position:relative;flex:0 0 auto;width:32px;height:18px;padding:0;border:1px solid var(--dsw-alias-border-l3);border-radius:999px;background:var(--dsw-alias-bg-module-platform);cursor:pointer}.codexSubscriptionSwitch:disabled{cursor:not-allowed;opacity:.5}.codexSubscriptionSwitch[aria-checked=true]{background:var(--dsw-alias-label-secondary);border-color:var(--dsw-alias-label-secondary)}.codexSubscriptionSwitchKnob{position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:var(--dsw-alias-bg-layer-1);transition:transform 120ms var(--ds-ease-in-out)}.codexSubscriptionSwitch[aria-checked=true] .codexSubscriptionSwitchKnob{transform:translateX(14px)}
421
484
  .codexSubscriptionSearch{display:flex;flex-direction:column;gap:7px}.codexSubscriptionSearchChoices{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}.codexSubscriptionSearchChoice{display:grid;grid-template-columns:14px minmax(0,1fr);align-items:center;column-gap:8px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);padding:9px 10px;text-align:left;cursor:pointer}.codexSubscriptionSearchChoice:has(input:disabled){cursor:not-allowed;opacity:.5}.codexSubscriptionSearchChoice:has(input:checked){border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}.codexSubscriptionSearchChoice:has(input:focus-visible){outline:2px solid var(--dsw-alias-border-l3);outline-offset:2px}.codexSubscriptionSearchInput{width:14px;height:14px;margin:0;accent-color:var(--dsw-alias-label-primary);cursor:inherit}.codexSubscriptionSearchCopy{display:block;min-width:0;pointer-events:none}.codexSubscriptionSearchCopy strong,.codexSubscriptionSearchCopy span{display:block}.codexSubscriptionSearchCopy strong{font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-label-secondary)}.codexSubscriptionSearchChoice:has(input:checked) strong{color:var(--dsw-alias-label-primary)}.codexSubscriptionSearchCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionDivider{height:1px;background:var(--dsw-alias-border-l2)}
422
485
  .codexSubscriptionAccountRow,.codexSubscriptionSectionHead{display:flex;align-items:center;justify-content:space-between;gap:12px}.codexSubscriptionStatus{display:flex;align-items:center;gap:8px;font-size:14px;line-height:22px;font-weight:500}
@@ -823,6 +886,7 @@ window.__ModuleLoader__.load({
823
886
  let fallback;
824
887
  let failedPatch;
825
888
  let generation = 0;
889
+ let contextModels = [];
826
890
  const nativeSnapshot = () => scope.getSnapshot();
827
891
  const read = () => {
828
892
  const native = nativeSnapshot();
@@ -832,6 +896,10 @@ window.__ModuleLoader__.load({
832
896
  quickQuotaMode: normalizeQuickQuotaMode(current.value?.[QUICK_QUOTA_MODE_FIELD], current.value?.[LEGACY_QUICK_QUOTA_FIELD]),
833
897
  searchProvider: normalizeSearchProvider(current.value?.[SEARCH_PROVIDER_FIELD]),
834
898
  speedMode: normalizeSpeedMode(current.value?.[SPEED_MODE_FIELD]),
899
+ contextMode: normalizeContextMode(current.value?.[CONTEXT_MODE_FIELD]),
900
+ customContextWindow: normalizeCustomContextWindow(current.value?.[CUSTOM_CONTEXT_WINDOW_FIELD]),
901
+ customContextWindows: Object.fromEntries(Object.entries(CUSTOM_CONTEXT_MODEL_FIELDS).map(([modelKey, field]) => [modelKey, normalizeCustomContextWindow(current.value?.[field] ?? CUSTOM_CONTEXT_MODEL_DEFAULTS[modelKey], CUSTOM_CONTEXT_MODEL_CAPS[modelKey])])),
902
+ contextModels,
835
903
  writable: !updating && current.status === "ready" && current.writable === true,
836
904
  error
837
905
  });
@@ -848,13 +916,17 @@ window.__ModuleLoader__.load({
848
916
  publish();
849
917
  });
850
918
  const acceptFallback = (value) => {
919
+ contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
851
920
  fallbackStatus = "ready";
852
921
  fallback = {
853
922
  status: "ready",
854
923
  value: {
855
924
  [QUICK_QUOTA_MODE_FIELD]: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
856
925
  [SEARCH_PROVIDER_FIELD]: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
857
- [SPEED_MODE_FIELD]: normalizeSpeedMode(value?.[SPEED_MODE_FIELD])
926
+ [SPEED_MODE_FIELD]: normalizeSpeedMode(value?.[SPEED_MODE_FIELD]),
927
+ [CONTEXT_MODE_FIELD]: normalizeContextMode(value?.[CONTEXT_MODE_FIELD]),
928
+ [CUSTOM_CONTEXT_WINDOW_FIELD]: normalizeCustomContextWindow(value?.[CUSTOM_CONTEXT_WINDOW_FIELD]),
929
+ ...Object.fromEntries(Object.entries(CUSTOM_CONTEXT_MODEL_FIELDS).map(([modelKey, field]) => [field, normalizeCustomContextWindow(value?.[field] ?? CUSTOM_CONTEXT_MODEL_DEFAULTS[modelKey], CUSTOM_CONTEXT_MODEL_CAPS[modelKey])]))
858
930
  },
859
931
  writable: value?.writable === true
860
932
  };
@@ -866,11 +938,12 @@ window.__ModuleLoader__.load({
866
938
  fallback = void 0;
867
939
  error = false;
868
940
  publish();
869
- if (nativeSnapshot().status === "ready") return;
941
+ nativeSnapshot();
870
942
  try {
871
943
  const value = unwrap(await rpc.call(CHANNEL, "preferences/status", {}));
872
- if (current !== generation || nativeSnapshot().status === "ready") return;
873
- acceptFallback(value);
944
+ if (current !== generation) return;
945
+ if (nativeSnapshot().status === "ready") contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
946
+ else acceptFallback(value);
874
947
  publish();
875
948
  } catch {
876
949
  if (current !== generation || nativeSnapshot().status === "ready") return;
@@ -1035,6 +1108,125 @@ window.__ModuleLoader__.load({
1035
1108
  })]
1036
1109
  });
1037
1110
  }
1111
+ function ContextWindowPreference({ preference, t }) {
1112
+ const snapshot = usePreferenceSnapshot(preference);
1113
+ const writable = snapshot.status === "ready" && snapshot.writable === true;
1114
+ const [menuOpen, setMenuOpen] = (0, react.useState)(false);
1115
+ const modelRows = snapshot.contextModels.filter((model) => model.fixed !== true);
1116
+ const fixedRows = snapshot.contextModels.filter((model) => model.fixed === true);
1117
+ const [drafts, setDrafts] = (0, react.useState)({});
1118
+ (0, react.useEffect)(() => setDrafts(Object.fromEntries(modelRows.map((model) => [model.key, String(snapshot.customContextWindows[model.key])]))), [snapshot.customContextWindows, snapshot.contextModels]);
1119
+ const hint = snapshot.contextMode === "extended" ? t("contextExtendedHint") : snapshot.contextMode === "custom" ? t("contextCustomHint") : t("contextStandardHint");
1120
+ const commit = (modelKey) => {
1121
+ const parsed = parseContextWindow(drafts[modelKey]);
1122
+ if (!Number.isInteger(parsed)) {
1123
+ setDrafts((current) => ({
1124
+ ...current,
1125
+ [modelKey]: String(snapshot.customContextWindows[modelKey])
1126
+ }));
1127
+ return;
1128
+ }
1129
+ const value = normalizeCustomContextWindow(parsed, CUSTOM_CONTEXT_MODEL_CAPS[modelKey]);
1130
+ setDrafts((current) => ({
1131
+ ...current,
1132
+ [modelKey]: String(value)
1133
+ }));
1134
+ if (value !== snapshot.customContextWindows[modelKey]) preference.set({ [CUSTOM_CONTEXT_MODEL_FIELDS[modelKey]]: value });
1135
+ };
1136
+ const contextModeItems = [
1137
+ {
1138
+ id: CONTEXT_MODE_STANDARD,
1139
+ label: t("contextStandard")
1140
+ },
1141
+ {
1142
+ id: CONTEXT_MODE_EXTENDED,
1143
+ label: t("contextExtended")
1144
+ },
1145
+ {
1146
+ id: CONTEXT_MODE_CUSTOM,
1147
+ label: t("contextCustom")
1148
+ }
1149
+ ];
1150
+ const selectedMode = contextModeItems.find((item) => item.id === snapshot.contextMode)?.label ?? t("contextStandard");
1151
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1152
+ className: "codexSubscriptionContext",
1153
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1154
+ className: "codexSubscriptionContextHead",
1155
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1156
+ className: "codexSubscriptionContextCopy",
1157
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1158
+ className: "codexSubscriptionPreferenceLabel",
1159
+ children: t("contextTitle")
1160
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1161
+ className: "codexSubscriptionContextHint",
1162
+ children: hint
1163
+ })]
1164
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
1165
+ open: menuOpen,
1166
+ items: contextModeItems,
1167
+ selectedId: snapshot.contextMode,
1168
+ onSelect: (value) => {
1169
+ setMenuOpen(false);
1170
+ preference.set({ [CONTEXT_MODE_FIELD]: value });
1171
+ },
1172
+ onClose: () => setMenuOpen(false),
1173
+ align: "end",
1174
+ side: "bottom",
1175
+ portal: true,
1176
+ compact: true,
1177
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1178
+ className: "codexSubscriptionContextTrigger",
1179
+ type: "button",
1180
+ "aria-label": t("contextTitle"),
1181
+ "aria-haspopup": "menu",
1182
+ "aria-expanded": menuOpen,
1183
+ disabled: !writable,
1184
+ onClick: () => setMenuOpen((value) => !value),
1185
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: selectedMode }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})]
1186
+ })
1187
+ })]
1188
+ }), snapshot.contextMode === "custom" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1189
+ className: "codexSubscriptionContextModels",
1190
+ children: [modelRows.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1191
+ className: "codexSubscriptionContextModel",
1192
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1193
+ className: "codexSubscriptionContextModelCopy",
1194
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: model.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fill(t("contextMaximum"), { value: String(model.maximum) }) })]
1195
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1196
+ "aria-label": `${model.label} ${t("contextTokens")}`,
1197
+ className: "codexSubscriptionContextInput",
1198
+ type: "number",
1199
+ inputMode: "numeric",
1200
+ min: MIN_CUSTOM_CONTEXT_WINDOW,
1201
+ max: model.maximum,
1202
+ step: 1,
1203
+ value: drafts[model.key] ?? "",
1204
+ disabled: !writable,
1205
+ onChange: (event) => {
1206
+ const nextValue = event.currentTarget.value;
1207
+ setDrafts((current) => ({
1208
+ ...current,
1209
+ [model.key]: nextValue
1210
+ }));
1211
+ },
1212
+ onBlur: () => commit(model.key),
1213
+ onKeyDown: (event) => {
1214
+ if (event.key === "Enter") event.currentTarget.blur();
1215
+ }
1216
+ })]
1217
+ }, model.key)), fixedRows.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1218
+ className: "codexSubscriptionContextModel",
1219
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1220
+ className: "codexSubscriptionContextModelCopy",
1221
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: model.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: fill(t("contextFixed"), { value: formatContextWindow(model.maximum) }) })]
1222
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1223
+ className: "codexSubscriptionContextHint",
1224
+ children: formatContextWindow(model.maximum)
1225
+ })]
1226
+ }, model.key))]
1227
+ }) : null]
1228
+ });
1229
+ }
1038
1230
  function PreferencesCard({ preference, t }) {
1039
1231
  const snapshot = usePreferenceSnapshot(preference);
1040
1232
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1045,6 +1237,11 @@ window.__ModuleLoader__.load({
1045
1237
  t
1046
1238
  }),
1047
1239
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "codexSubscriptionDivider" }),
1240
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextWindowPreference, {
1241
+ preference,
1242
+ t
1243
+ }),
1244
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "codexSubscriptionDivider" }),
1048
1245
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickQuotaPreference, {
1049
1246
  preference,
1050
1247
  t
package/lib/index.js CHANGED
@@ -5,6 +5,8 @@ import { settingsNamespace } from "@deepseek-ai/dsh-settings";
5
5
  import z from "@deepseek-ai/schemastery";
6
6
  import { execFile, spawn } from "node:child_process";
7
7
  import { request } from "node:https";
8
+ import { AsyncLocalStorage } from "node:async_hooks";
9
+ import { Readable } from "node:stream";
8
10
  import { promisify } from "node:util";
9
11
  import { HttpsProxyAgent } from "https-proxy-agent";
10
12
  import { openaiCodexProvider as createOpenAICodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
@@ -477,8 +479,11 @@ function createCodexRpcHandler(coordinator, options = {}) {
477
479
  //#region src/oauth-network.js
478
480
  const execFileAsync = promisify(execFile);
479
481
  const CODEX_AUTH_HOST = "auth.openai.com";
480
- const MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
481
- let patchQueue = Promise.resolve();
482
+ const CODEX_HOSTS = /* @__PURE__ */ new Set([CODEX_AUTH_HOST, "chatgpt.com"]);
483
+ const networkScope = new AsyncLocalStorage();
484
+ let activeScopes = 0;
485
+ let baseFetch;
486
+ let scopedFetch;
482
487
  function normalizeProxy(raw) {
483
488
  if (typeof raw !== "string" || raw.trim() === "") return void 0;
484
489
  const value = raw.trim().includes("://") ? raw.trim() : `http://${raw.trim()}`;
@@ -556,12 +561,27 @@ async function macSystemProxy(options = {}) {
556
561
  return;
557
562
  }
558
563
  }
559
- async function resolveCodexOAuthProxy(options = {}) {
560
- const envProxy = proxyFromEnvironment(options.env);
561
- if (envProxy) return envProxy;
564
+ async function resolveCodexProxy(options = {}) {
565
+ const target = options.target ?? new URL(`https://${CODEX_AUTH_HOST}/`);
566
+ const env = options.env ?? process.env;
567
+ if (bypassesProxy(target.hostname.toLowerCase(), target.port || "443", env.NO_PROXY ?? env.no_proxy)) return {
568
+ url: void 0,
569
+ source: "bypass"
570
+ };
571
+ const envProxy = proxyFromEnvironment(env, target);
572
+ if (envProxy) return {
573
+ url: envProxy,
574
+ source: "environment"
575
+ };
562
576
  const platform = options.platform ?? process.platform;
563
- if (platform === "win32") return windowsSystemProxy(options);
564
- if (platform === "darwin") return macSystemProxy(options);
577
+ const system = platform === "win32" ? await windowsSystemProxy(options) : platform === "darwin" ? await macSystemProxy(options) : void 0;
578
+ return system ? {
579
+ url: system,
580
+ source: "system"
581
+ } : {
582
+ url: void 0,
583
+ source: "direct"
584
+ };
565
585
  }
566
586
  function bodyBytes(body) {
567
587
  if (body === void 0 || body === null) return void 0;
@@ -582,49 +602,123 @@ function fetchThroughProxy(input, init, proxyUrl) {
582
602
  agent: new HttpsProxyAgent(proxyUrl),
583
603
  signal: init?.signal
584
604
  }, (response) => {
585
- const chunks = [];
586
- let total = 0;
587
- response.on("data", (chunk) => {
588
- total += chunk.length;
589
- if (total > MAX_RESPONSE_BYTES) {
590
- request$1.destroy(/* @__PURE__ */ new Error("Codex OAuth response exceeded the safety limit"));
591
- return;
592
- }
593
- chunks.push(chunk);
594
- });
595
- response.on("end", () => resolve(new Response(Buffer.concat(chunks), {
596
- status: response.statusCode ?? 500,
605
+ const responseHeaders = new Headers();
606
+ for (const [name, value] of Object.entries(response.headers)) if (Array.isArray(value)) value.forEach((item) => responseHeaders.append(name, item));
607
+ else if (value !== void 0) responseHeaders.set(name, value);
608
+ const status = response.statusCode ?? 500;
609
+ const empty = init?.method === "HEAD" || [
610
+ 204,
611
+ 205,
612
+ 304
613
+ ].includes(status);
614
+ resolve(new Response(empty ? null : Readable.toWeb(response), {
615
+ status,
597
616
  statusText: response.statusMessage,
598
- headers: response.headers
599
- })));
617
+ headers: responseHeaders
618
+ }));
600
619
  });
601
620
  request$1.on("error", reject);
602
621
  if (body) request$1.write(body);
603
622
  request$1.end();
604
623
  });
605
624
  }
606
- async function withCodexOAuthNetwork(run, options = {}) {
607
- const previous = patchQueue;
608
- let release;
609
- patchQueue = new Promise((resolve) => {
610
- release = resolve;
611
- });
612
- await previous;
613
- const proxyUrl = await resolveCodexOAuthProxy(options);
614
- const original = globalThis.fetch;
615
- const proxyFetch = options.fetchThroughProxy ?? fetchThroughProxy;
616
- const wrapper = proxyUrl ? (input, init) => {
617
- const target = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
618
- return target.protocol === "https:" && target.hostname === CODEX_AUTH_HOST ? proxyFetch(input, init, proxyUrl) : original(input, init);
619
- } : original;
620
- globalThis.fetch = wrapper;
625
+ async function withCodexNetwork(run, options = {}) {
626
+ if (activeScopes === 0) {
627
+ baseFetch = globalThis.fetch;
628
+ scopedFetch = async (input, init) => {
629
+ const scope = networkScope.getStore();
630
+ if (scope === void 0) return baseFetch(input, init);
631
+ const { options: scopedOptions, allowedHosts, resolved } = scope;
632
+ const proxyFetch = scopedOptions.fetchThroughProxy ?? fetchThroughProxy;
633
+ const target = new URL(typeof input === "string" || input instanceof URL ? input : input.url);
634
+ if (target.protocol !== "https:" || !allowedHosts.has(target.hostname)) return baseFetch(input, init);
635
+ let proxy = resolved.get(target.hostname);
636
+ if (proxy === void 0) {
637
+ proxy = resolveCodexProxy({
638
+ ...scopedOptions,
639
+ target
640
+ });
641
+ resolved.set(target.hostname, proxy);
642
+ }
643
+ const route = await proxy;
644
+ scopedOptions.onRoute?.(route.source);
645
+ return route.url === void 0 ? baseFetch(input, init) : proxyFetch(input, init, route.url);
646
+ };
647
+ globalThis.fetch = scopedFetch;
648
+ }
649
+ activeScopes += 1;
650
+ const scope = {
651
+ options,
652
+ allowedHosts: options.hosts ?? CODEX_HOSTS,
653
+ resolved: /* @__PURE__ */ new Map()
654
+ };
621
655
  try {
622
- return await run();
656
+ return await networkScope.run(scope, run);
623
657
  } finally {
624
- if (globalThis.fetch === wrapper) globalThis.fetch = original;
625
- release();
658
+ activeScopes -= 1;
659
+ if (activeScopes === 0) {
660
+ if (globalThis.fetch === scopedFetch) globalThis.fetch = baseFetch;
661
+ baseFetch = void 0;
662
+ scopedFetch = void 0;
663
+ }
626
664
  }
627
665
  }
666
+ function classifyTransportError(error) {
667
+ const name = error?.name;
668
+ const code = String(error?.code ?? error?.cause?.code ?? "");
669
+ if (name === "AbortError" || name === "TimeoutError" || /ETIMEDOUT|UND_ERR_CONNECT_TIMEOUT/u.test(code)) return "timeout";
670
+ if (/ENOTFOUND|EAI_AGAIN/u.test(code)) return "dns";
671
+ if (/CERT_|TLS|SSL/u.test(code)) return "tls";
672
+ if (/ECONN|EPIPE|UND_ERR_SOCKET/u.test(code)) return "connection";
673
+ return "network";
674
+ }
675
+ const elapsedBucket = (elapsed) => elapsed < 1e3 ? "under-1s" : elapsed < 5e3 ? "1-5s" : elapsed < 15e3 ? "5-15s" : "over-15s";
676
+ function createCodexNetworkTransport(options = {}) {
677
+ const attempts = /* @__PURE__ */ new Map();
678
+ const now = options.now ?? Date.now;
679
+ const run = async (area, operation) => {
680
+ const startedAt = now();
681
+ let route = attempts.get(area)?.route ?? "direct";
682
+ let routed = false;
683
+ try {
684
+ const value = await withCodexNetwork(operation, {
685
+ ...options,
686
+ onRoute: (source) => {
687
+ route = source;
688
+ routed = true;
689
+ }
690
+ });
691
+ if (value instanceof Response && !value.ok) attempts.set(area, {
692
+ status: "failed",
693
+ stage: "http",
694
+ code: "http-error",
695
+ httpStatus: value.status,
696
+ route,
697
+ elapsed: elapsedBucket(now() - startedAt)
698
+ });
699
+ else if (routed || value instanceof Response) attempts.set(area, {
700
+ status: "ok",
701
+ route,
702
+ elapsed: elapsedBucket(now() - startedAt)
703
+ });
704
+ return value;
705
+ } catch (error) {
706
+ if (routed) attempts.set(area, {
707
+ status: "failed",
708
+ stage: "transport",
709
+ code: classifyTransportError(error),
710
+ route,
711
+ elapsed: elapsedBucket(now() - startedAt)
712
+ });
713
+ throw error;
714
+ }
715
+ };
716
+ return Object.freeze({
717
+ run,
718
+ fetch: (area, input, init) => run(area, () => globalThis.fetch(input, init)),
719
+ snapshot: () => Object.fromEntries([...attempts].map(([area, value]) => [area, { ...value }]))
720
+ });
721
+ }
628
722
  //#endregion
629
723
  //#region src/settings-contract.js
630
724
  const SETTINGS_NAMESPACE = "codex-subscription";
@@ -637,6 +731,75 @@ const SPEED_MODE_FIELD = "speedMode";
637
731
  const SPEED_MODE_STANDARD = "standard";
638
732
  const SPEED_MODE_FAST = "fast";
639
733
  const DEFAULT_SPEED_MODE = SPEED_MODE_STANDARD;
734
+ const CONTEXT_MODE_FIELD = "contextMode";
735
+ const CONTEXT_MODE_STANDARD = "standard";
736
+ const CONTEXT_MODE_EXTENDED = "extended";
737
+ const CONTEXT_MODE_CUSTOM = "custom";
738
+ const DEFAULT_CONTEXT_MODE = CONTEXT_MODE_STANDARD;
739
+ const CUSTOM_CONTEXT_WINDOW_FIELD = "customContextWindow";
740
+ const DEFAULT_CUSTOM_CONTEXT_WINDOW = 272e3;
741
+ const MIN_CUSTOM_CONTEXT_WINDOW = 128e3;
742
+ const MAX_CUSTOM_CONTEXT_WINDOW = 1e6;
743
+ const CUSTOM_CONTEXT_MODEL_FIELDS = Object.freeze({
744
+ "gpt-5.4": "customContextGpt54",
745
+ "gpt-5.4-mini": "customContextGpt54Mini",
746
+ "gpt-5.5": "customContextGpt55",
747
+ "gpt-5.6": "customContextGpt56"
748
+ });
749
+ const CUSTOM_CONTEXT_MODEL_CAPS = Object.freeze({
750
+ "gpt-5.4": 1e6,
751
+ "gpt-5.4-mini": 4e5,
752
+ "gpt-5.5": 1e6,
753
+ "gpt-5.6": 1e6
754
+ });
755
+ const CUSTOM_CONTEXT_MODEL_DEFAULTS = Object.freeze({
756
+ "gpt-5.4": 272e3,
757
+ "gpt-5.4-mini": 272e3,
758
+ "gpt-5.5": 272e3,
759
+ "gpt-5.6": 272e3
760
+ });
761
+ const normalizeContextMode = (value) => [
762
+ "standard",
763
+ "extended",
764
+ "custom"
765
+ ].includes(value) ? value : DEFAULT_CONTEXT_MODE;
766
+ const normalizeCustomContextWindow = (value, maximum = MAX_CUSTOM_CONTEXT_WINDOW) => {
767
+ if (!Number.isInteger(value)) return DEFAULT_CUSTOM_CONTEXT_WINDOW;
768
+ return Math.min(Math.max(value, MIN_CUSTOM_CONTEXT_WINDOW), maximum);
769
+ };
770
+ const customContextModelKey = (modelId) => modelId?.startsWith("gpt-5.6-") ? "gpt-5.6" : modelId;
771
+ function contextModelGroups(models) {
772
+ const groups = /* @__PURE__ */ new Map();
773
+ for (const model of models ?? []) {
774
+ if (model?.id === "gpt-5.3-codex-spark") {
775
+ groups.set(model.id, {
776
+ key: model.id,
777
+ label: model.name ?? model.id,
778
+ maximum: 128e3,
779
+ fixed: true
780
+ });
781
+ continue;
782
+ }
783
+ const key = customContextModelKey(model?.id);
784
+ if (!Object.hasOwn(CUSTOM_CONTEXT_MODEL_FIELDS, key)) continue;
785
+ if (key !== "gpt-5.6") {
786
+ groups.set(key, {
787
+ key,
788
+ label: model.name ?? model.id,
789
+ maximum: CUSTOM_CONTEXT_MODEL_CAPS[key]
790
+ });
791
+ continue;
792
+ }
793
+ const variant = String(model.name ?? model.id).replace(/^GPT-5\.6[ -]/iu, "");
794
+ const current = groups.get(key);
795
+ groups.set(key, {
796
+ key,
797
+ label: `GPT-5.6 ${current === void 0 ? variant : `${current.label.replace(/^GPT-5\.6 /u, "")} / ${variant}`}`,
798
+ maximum: CUSTOM_CONTEXT_MODEL_CAPS[key]
799
+ });
800
+ }
801
+ return [...groups.values()];
802
+ }
640
803
  const normalizeQuickQuotaMode = (value, legacyVisible = false) => [
641
804
  "off",
642
805
  "percent",
@@ -658,7 +821,15 @@ const FAST_SERVICE_TIER = "priority";
658
821
  * persistence, headers, transport, and model behavior remain owned by the
659
822
  * original provider.
660
823
  */
661
- function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0 } = {}) {
824
+ const EXTENDED_CONTEXT_WINDOWS = Object.freeze({
825
+ "gpt-5.4": 1e6,
826
+ "gpt-5.4-mini": 4e5,
827
+ "gpt-5.5": 1e6,
828
+ "gpt-5.6-luna": 1e6,
829
+ "gpt-5.6-sol": 1e6,
830
+ "gpt-5.6-terra": 1e6
831
+ });
832
+ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, resolveContextMode = () => void 0, resolveCustomContextWindow = () => void 0, runNetwork = (_area, operation) => operation() } = {}) {
662
833
  const provider = createOpenAICodexProvider();
663
834
  const requestToken = Object.freeze({
664
835
  name: "DSH-managed Codex OAuth request token",
@@ -689,19 +860,49 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0 } = {
689
860
  }
690
861
  };
691
862
  };
863
+ const getModels = () => provider.getModels().map((model) => {
864
+ const maximum = EXTENDED_CONTEXT_WINDOWS[model.id];
865
+ const mode = resolveContextMode();
866
+ if (maximum === void 0 || !["extended", "custom"].includes(mode)) return model;
867
+ if (mode === "extended") return {
868
+ ...model,
869
+ contextWindow: Math.max(model.contextWindow, maximum)
870
+ };
871
+ const requested = normalizeCustomContextWindow(resolveCustomContextWindow(customContextModelKey(model.id)), maximum);
872
+ return {
873
+ ...model,
874
+ contextWindow: requested
875
+ };
876
+ });
877
+ const networkIterable = (factory) => {
878
+ let iterator;
879
+ const getIterator = () => iterator ??= factory()[Symbol.asyncIterator]();
880
+ return {
881
+ [Symbol.asyncIterator]() {
882
+ return this;
883
+ },
884
+ next: (value) => runNetwork("model", () => getIterator().next(value)),
885
+ return: (value) => runNetwork("model", () => getIterator().return?.(value) ?? Promise.resolve({
886
+ done: true,
887
+ value
888
+ })),
889
+ throw: (error) => runNetwork("model", () => getIterator().throw?.(error) ?? Promise.reject(error))
890
+ };
891
+ };
692
892
  return Object.freeze({
693
893
  ...provider,
694
894
  auth: Object.freeze({
695
895
  ...provider.auth,
696
896
  apiKey: requestToken
697
897
  }),
698
- stream: (model, context, options) => provider.stream(model, context, withSpeed(model, options)),
699
- streamSimple: (model, context, options) => provider.streamSimple(model, context, withSpeed(model, options))
898
+ getModels,
899
+ stream: (model, context, options) => networkIterable(() => provider.stream(model, context, withSpeed(model, options))),
900
+ streamSimple: (model, context, options) => networkIterable(() => provider.streamSimple(model, context, withSpeed(model, options)))
700
901
  });
701
902
  }
702
903
  //#endregion
703
904
  //#region src/version.js
704
- const PACKAGE_VERSION = "1.7.4";
905
+ const PACKAGE_VERSION = "1.8.0";
705
906
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
706
907
  //#endregion
707
908
  //#region src/codex-search.js
@@ -1094,8 +1295,55 @@ function createCodexImageTool(options) {
1094
1295
  }
1095
1296
  //#endregion
1096
1297
  //#region src/diagnostics.js
1298
+ const requestAreas = /* @__PURE__ */ new Set([
1299
+ "login",
1300
+ "model",
1301
+ "quota",
1302
+ "quota-reset",
1303
+ "search",
1304
+ "image"
1305
+ ]);
1306
+ const statuses = /* @__PURE__ */ new Set(["ok", "failed"]);
1307
+ const stages = /* @__PURE__ */ new Set(["transport", "http"]);
1308
+ const codes = /* @__PURE__ */ new Set([
1309
+ "timeout",
1310
+ "dns",
1311
+ "tls",
1312
+ "connection",
1313
+ "network",
1314
+ "http-error"
1315
+ ]);
1316
+ const routes = /* @__PURE__ */ new Set([
1317
+ "direct",
1318
+ "environment",
1319
+ "system",
1320
+ "bypass"
1321
+ ]);
1322
+ const elapsedBuckets = /* @__PURE__ */ new Set([
1323
+ "under-1s",
1324
+ "1-5s",
1325
+ "5-15s",
1326
+ "over-15s"
1327
+ ]);
1328
+ function safeRequests(network) {
1329
+ const raw = network?.snapshot?.() ?? {};
1330
+ const result = {};
1331
+ for (const [area, value] of Object.entries(raw)) {
1332
+ if (!requestAreas.has(area) || value === null || typeof value !== "object") continue;
1333
+ if (!statuses.has(value.status) || !routes.has(value.route) || !elapsedBuckets.has(value.elapsed)) continue;
1334
+ result[area] = {
1335
+ status: value.status,
1336
+ ...stages.has(value.stage) ? { stage: value.stage } : {},
1337
+ ...codes.has(value.code) ? { code: value.code } : {},
1338
+ ...Number.isInteger(value.httpStatus) && value.httpStatus >= 100 && value.httpStatus <= 599 ? { httpStatus: value.httpStatus } : {},
1339
+ route: value.route,
1340
+ elapsed: value.elapsed
1341
+ };
1342
+ }
1343
+ return result;
1344
+ }
1097
1345
  /** Build a support report that deliberately excludes OAuth and account metadata. */
1098
- async function createSubscriptionDiagnostics({ auth, preferences, login = { phase: "idle" } }) {
1346
+ async function createSubscriptionDiagnostics({ auth, preferences, login = { phase: "idle" }, network }) {
1099
1347
  let account = { status: "unknown" };
1100
1348
  const issues = [];
1101
1349
  try {
@@ -1103,8 +1351,9 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
1103
1351
  } catch {
1104
1352
  issues.push({ code: "account-status-unavailable" });
1105
1353
  }
1354
+ const preference = preferences.status();
1106
1355
  return {
1107
- schemaVersion: 2,
1356
+ schemaVersion: 3,
1108
1357
  package: "dsh-codex-subscription",
1109
1358
  version: PACKAGE_VERSION,
1110
1359
  runtime: {
@@ -1114,23 +1363,14 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
1114
1363
  },
1115
1364
  account,
1116
1365
  login,
1117
- provider: {
1118
- id: "openai-codex",
1119
- transport: "sse",
1120
- cache: {
1121
- owner: "dsh/pi-ai",
1122
- retention: "short"
1123
- }
1124
- },
1125
- capabilities: {
1126
- models: true,
1127
- quota: true,
1128
- search: true,
1129
- imageGeneration: true,
1130
- composerQuota: true,
1131
- speedMode: true
1366
+ requests: safeRequests(network),
1367
+ configuration: {
1368
+ contextMode: preference.contextMode,
1369
+ quickQuotaMode: preference.quickQuotaMode,
1370
+ searchProvider: preference.searchProvider,
1371
+ speedMode: preference.speedMode,
1372
+ writable: preference.writable === true
1132
1373
  },
1133
- configuration: preferences.status(),
1134
1374
  issues
1135
1375
  };
1136
1376
  }
@@ -1575,6 +1815,23 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
1575
1815
  if (!["standard", "fast"].includes(payload["speedMode"])) return publicError("internal", "Invalid speed mode preference");
1576
1816
  patch[SPEED_MODE_FIELD] = payload[SPEED_MODE_FIELD];
1577
1817
  }
1818
+ if (Object.hasOwn(payload ?? {}, "contextMode")) {
1819
+ if (![
1820
+ "standard",
1821
+ "extended",
1822
+ "custom"
1823
+ ].includes(payload["contextMode"])) return publicError("internal", "Invalid context mode preference");
1824
+ patch[CONTEXT_MODE_FIELD] = payload[CONTEXT_MODE_FIELD];
1825
+ }
1826
+ if (Object.hasOwn(payload ?? {}, "customContextWindow")) {
1827
+ if (normalizeCustomContextWindow(payload["customContextWindow"]) !== payload["customContextWindow"]) return publicError("internal", "Invalid custom context window");
1828
+ patch[CUSTOM_CONTEXT_WINDOW_FIELD] = payload[CUSTOM_CONTEXT_WINDOW_FIELD];
1829
+ }
1830
+ for (const [modelKey, field] of Object.entries(CUSTOM_CONTEXT_MODEL_FIELDS)) {
1831
+ if (!Object.hasOwn(payload ?? {}, field)) continue;
1832
+ if (normalizeCustomContextWindow(payload[field], CUSTOM_CONTEXT_MODEL_CAPS[modelKey]) !== payload[field]) return publicError("internal", "Invalid custom model context window");
1833
+ patch[field] = payload[field];
1834
+ }
1578
1835
  if (Object.keys(patch).length === 0) return publicError("internal", "Invalid preference update");
1579
1836
  await preferences.update(patch);
1580
1837
  }
@@ -1663,20 +1920,40 @@ function apply(ctx) {
1663
1920
  ]),
1664
1921
  [LEGACY_QUICK_QUOTA_FIELD]: z.boolean(),
1665
1922
  [SEARCH_PROVIDER_FIELD]: z.union(["dsh", SEARCH_PROVIDER_CODEX]).default("dsh"),
1666
- [SPEED_MODE_FIELD]: z.union([SPEED_MODE_STANDARD, SPEED_MODE_FAST]).default(DEFAULT_SPEED_MODE)
1923
+ [SPEED_MODE_FIELD]: z.union([SPEED_MODE_STANDARD, SPEED_MODE_FAST]).default(DEFAULT_SPEED_MODE),
1924
+ [CONTEXT_MODE_FIELD]: z.union([
1925
+ CONTEXT_MODE_STANDARD,
1926
+ CONTEXT_MODE_EXTENDED,
1927
+ CONTEXT_MODE_CUSTOM
1928
+ ]).default(DEFAULT_CONTEXT_MODE),
1929
+ [CUSTOM_CONTEXT_WINDOW_FIELD]: z.number().step(1).min(128e3).max(1e6).default(DEFAULT_CUSTOM_CONTEXT_WINDOW),
1930
+ ...Object.fromEntries(Object.entries(CUSTOM_CONTEXT_MODEL_FIELDS).map(([modelKey, field]) => [field, z.number().step(1).min(128e3).max(CUSTOM_CONTEXT_MODEL_CAPS[modelKey]).default(CUSTOM_CONTEXT_MODEL_DEFAULTS[modelKey])]))
1667
1931
  }));
1668
1932
  const searchProvider = createSearchProviderSwitcher(ctx.loader);
1933
+ const network = createCodexNetworkTransport();
1934
+ const provider = openaiCodexSubscriptionProvider({
1935
+ resolveSpeedMode: () => settings.get()[SPEED_MODE_FIELD],
1936
+ resolveContextMode: () => normalizeContextMode(settings.get()[CONTEXT_MODE_FIELD]),
1937
+ resolveCustomContextWindow: (modelKey) => {
1938
+ const field = CUSTOM_CONTEXT_MODEL_FIELDS[modelKey];
1939
+ return normalizeCustomContextWindow(settings.get()[field] ?? CUSTOM_CONTEXT_MODEL_DEFAULTS[modelKey], CUSTOM_CONTEXT_MODEL_CAPS[modelKey]);
1940
+ },
1941
+ runNetwork: network.run
1942
+ });
1669
1943
  const preferences = {
1670
1944
  status: () => ({
1671
1945
  [QUICK_QUOTA_MODE_FIELD]: normalizeQuickQuotaMode(settings.get()[QUICK_QUOTA_MODE_FIELD], settings.get()[LEGACY_QUICK_QUOTA_FIELD]),
1672
1946
  [SEARCH_PROVIDER_FIELD]: settings.get()[SEARCH_PROVIDER_FIELD],
1673
1947
  [SPEED_MODE_FIELD]: settings.get()[SPEED_MODE_FIELD],
1948
+ [CONTEXT_MODE_FIELD]: normalizeContextMode(settings.get()[CONTEXT_MODE_FIELD]),
1949
+ [CUSTOM_CONTEXT_WINDOW_FIELD]: normalizeCustomContextWindow(settings.get()[CUSTOM_CONTEXT_WINDOW_FIELD]),
1950
+ ...Object.fromEntries(Object.entries(CUSTOM_CONTEXT_MODEL_FIELDS).map(([modelKey, field]) => [field, normalizeCustomContextWindow(settings.get()[field] ?? CUSTOM_CONTEXT_MODEL_DEFAULTS[modelKey], CUSTOM_CONTEXT_MODEL_CAPS[modelKey])])),
1951
+ contextModels: contextModelGroups(provider.getModels()),
1674
1952
  writable: ctx.settings.writable
1675
1953
  }),
1676
1954
  update: (patch) => settings.update(patch)
1677
1955
  };
1678
1956
  const store = new DshOAuthCredentialStore(ctx.credentials, CREDENTIAL_REF, [LEGACY_CREDENTIAL_REF]);
1679
- const provider = openaiCodexSubscriptionProvider({ resolveSpeedMode: () => settings.get()[SPEED_MODE_FIELD] });
1680
1957
  const authModels = createModels({ credentials: store });
1681
1958
  authModels.setProvider(provider);
1682
1959
  const profile = Object.freeze({
@@ -1691,7 +1968,16 @@ function apply(ctx) {
1691
1968
  cacheRetention: "short",
1692
1969
  transport: "sse"
1693
1970
  });
1694
- const profiles = /* @__PURE__ */ new Map([[PROVIDER, profile]]);
1971
+ let profileKey;
1972
+ let profileSnapshot;
1973
+ const profiles = () => {
1974
+ const key = [normalizeContextMode(settings.get()[CONTEXT_MODE_FIELD]), ...Object.values(CUSTOM_CONTEXT_MODEL_FIELDS).map((field) => settings.get()[field])].join(":");
1975
+ if (key !== profileKey) {
1976
+ profileKey = key;
1977
+ profileSnapshot = /* @__PURE__ */ new Map([[PROVIDER, profile]]);
1978
+ }
1979
+ return profileSnapshot;
1980
+ };
1695
1981
  const resolveAuth = () => authModels.getAuth(PROVIDER);
1696
1982
  const adapterAuth = Object.freeze({
1697
1983
  credentials: store,
@@ -1703,10 +1989,11 @@ function apply(ctx) {
1703
1989
  ctx.tools.register(createCodexImageTool({
1704
1990
  getAuth: resolveAuth,
1705
1991
  readCredential: (options) => store.read(PROVIDER, options),
1706
- attachments: ctx.attachments
1992
+ attachments: ctx.attachments,
1993
+ fetch: (input, init) => network.fetch("image", input, init)
1707
1994
  }));
1708
1995
  const adapter = new PiAiAdapter({
1709
- profiles: () => profiles,
1996
+ profiles,
1710
1997
  resolveApiKey: async () => {
1711
1998
  let resolved;
1712
1999
  try {
@@ -1729,7 +2016,8 @@ function apply(ctx) {
1729
2016
  const request = currentAgent()?.session.requestContext?.();
1730
2017
  return request?.provider === PROVIDER ? request.model : void 0;
1731
2018
  },
1732
- resolveSessionId: () => currentAgent()?.session.id
2019
+ resolveSessionId: () => currentAgent()?.session.id,
2020
+ fetch: (input, init) => network.fetch("search", input, init)
1733
2021
  }));
1734
2022
  ctx.effect(() => {
1735
2023
  const select = async (value) => {
@@ -1742,16 +2030,18 @@ function apply(ctx) {
1742
2030
  select(settings.get());
1743
2031
  return settings.watch(select);
1744
2032
  }, "codex-subscription: search provider selection");
1745
- const auth = createCodexAuthService(authModels, store, { runLogin: withCodexOAuthNetwork });
2033
+ const auth = createCodexAuthService(authModels, store, { runLogin: (operation) => network.run("login", operation) });
1746
2034
  const coordinator = new CodexLoginCoordinator(auth);
1747
2035
  const usageReader = createCodexUsageReader({
1748
2036
  getAuth: resolveAuth,
1749
- readCredential: (options) => store.read(PROVIDER, options)
2037
+ readCredential: (options) => store.read(PROVIDER, options),
2038
+ fetch: (input, init) => network.fetch("quota", input, init)
1750
2039
  });
1751
2040
  const resetCreditService = createCodexResetCreditService({
1752
2041
  getAuth: resolveAuth,
1753
2042
  readCredential: (options) => store.read(PROVIDER, options),
1754
- usageReader
2043
+ usageReader,
2044
+ fetch: (input, init) => network.fetch("quota-reset", input, init)
1755
2045
  });
1756
2046
  const handler = createSubscriptionRpcHandler({
1757
2047
  authHandler: createCodexRpcHandler(coordinator, { openExternal: openCodexAuthUrl }),
@@ -1761,10 +2051,11 @@ function apply(ctx) {
1761
2051
  diagnosticsReader: () => createSubscriptionDiagnostics({
1762
2052
  auth,
1763
2053
  preferences,
1764
- login: coordinator.supportState()
2054
+ login: coordinator.supportState(),
2055
+ network
1765
2056
  })
1766
2057
  });
1767
2058
  ctx.effect(() => ctx.connection.rpc.handle(CHANNEL, handler, { authority: "loopback" }), "codex-subscription: loopback account RPC");
1768
2059
  }
1769
2060
  //#endregion
1770
- export { CODEX_IMAGE_GENERATION_URL, CODEX_IMAGE_TOOL_NAME, CODEX_RESET_CONSUME_URL, CODEX_RESET_CREDITS_URL, CODEX_USAGE_URL, CodexLoginCoordinator, DshOAuthCredentialStore, apply, assertCodexAuthUrl, commandForCodexAuthUrl, createCodexAuthService, createCodexImageTool, createCodexResetCreditService, createCodexRpcHandler, createCodexUsageReader, createSearchProviderSwitcher, createSubscriptionDiagnostics, createSubscriptionRpcHandler, decodeCodexPng, inject, name, openCodexAuthUrl, parseCodexUsage };
2061
+ export { CODEX_IMAGE_GENERATION_URL, CODEX_IMAGE_TOOL_NAME, CODEX_RESET_CONSUME_URL, CODEX_RESET_CREDITS_URL, CODEX_USAGE_URL, CodexLoginCoordinator, DshOAuthCredentialStore, apply, assertCodexAuthUrl, commandForCodexAuthUrl, createCodexAuthService, createCodexImageTool, createCodexResetCreditService, createCodexRpcHandler, createCodexUsageReader, createSearchProviderSwitcher, createSubscriptionDiagnostics, createSubscriptionRpcHandler, decodeCodexPng, inject, name, normalizeContextMode, normalizeCustomContextWindow, openCodexAuthUrl, parseCodexUsage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "1.7.4",
3
+ "version": "1.8.0",
4
4
  "description": "Use ChatGPT and Codex subscriptions in DeepSeek Harness with OAuth, quota, safe resets, web search, images, and Fast mode",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",