dsh-codex-subscription 1.7.4 → 1.9.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.9.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.9.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.9.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.9.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
@@ -48,30 +48,26 @@ DSH-Portable exposes the same standard plugin command, so the command above also
48
48
  | **Visible quota** | Keep backend-provided standard Codex, Spark, and other limits separate |
49
49
  | **Composer quota** | Choose a compact percentage, progress bar, or no inline quota display |
50
50
  | **Safe quota reset** | See the next expiry and deliberately try a reset with a cooldown and acknowledgement |
51
- | **Subscription search** | Explicitly choose DSH default search or Codex subscription search |
51
+ | **Subscription search** | Explicitly route search globally through DSH default search or the signed-in Codex subscription |
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
 
57
58
  ## Product screen
58
59
 
59
60
  <p align="center">
60
- <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/settings-focus-en.png" width="820" alt="Codex subscription settings in DeepSeek Harness">
61
+ <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/context-settings-en.png" width="820" alt="Current Codex subscription settings in DeepSeek Harness with search, model-aware context, composer quota, and support diagnostics">
61
62
  </p>
62
63
 
63
- <details>
64
- <summary>View the complete settings screen</summary>
65
-
66
- ![Complete Codex subscription settings in DeepSeek Harness](https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/settings-en.png)
67
-
68
- </details>
64
+ Captured from the installed official DeepSeek Harness `0.1.1-rc.2` product with the current plugin build.
69
65
 
70
66
  ## Prepare DSH
71
67
 
72
68
  This plugin is currently compatible through DeepSeek Harness `0.1.1-rc.2` and requires a ChatGPT account that currently has Codex access.
73
69
 
74
- - Do not want to configure Node.js? Use [DSH-Portable](https://github.com/WSL043/DSH-Portable), a community desktop distribution with portable and installed editions for Windows plus desktop packages for macOS and Linux.
70
+ - Do not want to configure Node.js? Use [DSH-Portable](https://github.com/WSL043/DSH-Portable), a community portable desktop distribution for Windows, macOS, and Linux.
75
71
  - Prefer the official route? Follow the [DeepSeek Harness run guide](https://github.com/deepseek-ai/deepseek-harness#run).
76
72
 
77
73
  ## Install
@@ -96,12 +92,13 @@ Open PowerShell and paste this one line:
96
92
  irm 'https://github.com/WSL043/dsh-codex-subscription/releases/latest/download/dsh-codex-setup.ps1' | iex
97
93
  ```
98
94
 
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.
95
+ The lightweight setup checks the current folder, the system command, common locations, and running official DSH or
96
+ [DSH-Portable](https://github.com/WSL043/DSH-Portable). It asks before choosing when more than one target is available.
97
+ Portable keeps using its public launcher. For official DSH, setup uses the selected profile home and a checksum-verified
98
+ pnpm helper pinned to the accepted DSH release; the helper is cached under the current Windows user when no working pnpm
99
+ command is available. It does not recursively scan disks, create a resident command, snapshot a profile, require
100
+ administrator access, or restart DSH. A recognized release-age rejection may retry the same pinned add once for that
101
+ command only.
105
102
 
106
103
  <details>
107
104
  <summary>Official npm route (Node.js installed)</summary>
@@ -140,15 +137,18 @@ Restart DSH manually after installation, then:
140
137
 
141
138
  - ChatGPT OAuth sign-in with credentials kept on the host;
142
139
  - Codex models and Beta image generation/editing directly inside DSH conversations;
143
- - A clear choice between DSH default search and Codex subscription search;
140
+ - A clear global choice between DSH default search and Codex subscription search; it applies to every model and session rather than following the selected model;
144
141
  - Actual backend-provided quota, reset time, and freshness;
145
142
  - Separate standard Codex, Codex-Spark, Credits, and other independent limits;
146
143
  - Visible quota-reset count and earliest expiry, with deliberate early redemption, layered confirmation, and no automatic retry;
147
144
  - Optional percentage or progress bar for the selected Codex model (off by default);
148
145
  - 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;
146
+ - 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;
147
+ - 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
148
  - Visible errors when subscription routing is unavailable, with no silent paid fallback.
151
149
 
150
+ 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.
151
+
152
152
  ### Composer quota
153
153
 
154
154
  <p align="center">
@@ -214,7 +214,7 @@ dsh plugin --profile web remove dsh-codex-subscription
214
214
  - **`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
215
  - **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
216
  - **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.
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, 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
218
 
219
219
  The ChatGPT Codex backend and DSH can change independently. This community project is not affiliated with or endorsed by DeepSeek or OpenAI.
220
220
 
package/README.zh-CN.md CHANGED
@@ -49,30 +49,26 @@ DSH-Portable 也提供相同的标准插件命令,因此同样使用上面的
49
49
  | **额度可见** | 普通 Codex、Spark 等服务端实际返回的额度分开显示 |
50
50
  | **输入框额度** | 可选择紧凑百分比、进度条或完全关闭输入框额度显示 |
51
51
  | **安全额度重置** | 直接查看最早到期时间,并通过冷静期和知情确认主动尝试重置 |
52
- | **订阅搜索** | 可在 DSH 默认搜索与 Codex 订阅搜索之间明确切换 |
52
+ | **订阅搜索** | 可将全部模型的搜索明确路由到 DSH 默认搜索或已登录的 Codex 订阅 |
53
53
  | **Codex 图片生成与编辑(Beta)** | 可无参考图全新生成,也可明确选择会话图片继续编辑;支持预览、缩放和下载原图 |
54
54
  | **高速模式** | 直接在输入框切换标准或高速,无需离开当前会话 |
55
+ | **模型感知上下文** | 可保留目录默认值、按模型启用扩展窗口,或为每个模型填写完整数字 Token 上限 |
55
56
 
56
57
  这些能力共用同一份本机 ChatGPT 登录。订阅路由失败时会明确报错,不会静默切换到其他付费路由。
57
58
 
58
59
  ## 实际界面
59
60
 
60
61
  <p align="center">
61
- <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/settings-focus.png" width="820" alt="DeepSeek Harness 中的 Codex 订阅设置">
62
+ <img src="https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/context-settings.png" width="820" alt="当前 DeepSeek Harness Codex 订阅设置,包含搜索来源、模型感知上下文、输入框额度和支持诊断">
62
63
  </p>
63
64
 
64
- <details>
65
- <summary>查看完整设置界面</summary>
66
-
67
- ![DeepSeek Harness 的完整 Codex 订阅设置](https://raw.githubusercontent.com/WSL043/dsh-codex-subscription/main/docs/assets/settings.png)
68
-
69
- </details>
65
+ 图片来自已安装的官方 DeepSeek Harness `0.1.1-rc.2` 与当前插件构建。
70
66
 
71
67
  ## 准备 DSH
72
68
 
73
69
  本插件当前适配到 DeepSeek Harness `0.1.1-rc.2`,并需要一个当前具有 Codex 使用资格的 ChatGPT 账户。
74
70
 
75
- - 不想配置 Node.js:使用 [DSH-Portable](https://github.com/WSL043/DSH-Portable)。这是社区桌面分发,提供 Windows 便携版和安装版,以及 macOS、Linux 桌面包;
71
+ - 不想配置 Node.js:使用 [DSH-Portable](https://github.com/WSL043/DSH-Portable)。这是面向 Windows、macOS Linux 的社区便携桌面分发;
76
72
  - 想按官方方式运行:查看 [DeepSeek Harness 官方说明](https://github.com/deepseek-ai/deepseek-harness#run)。
77
73
 
78
74
  ## 安装
@@ -98,10 +94,11 @@ irm 'https://github.com/WSL043/dsh-codex-subscription/releases/latest/download/d
98
94
  ```
99
95
 
100
96
  这个轻量助手会检查当前目录、系统命令、常见位置以及正在运行的官方 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 运行方式;首次解析依赖可能较慢,安装器会先明确提示。
97
+ [DSH-Portable](https://github.com/WSL043/DSH-Portable);存在多个目标时会先让用户选择。
98
+ Portable 继续使用它的公开启动器。安装到官方 DSH 时,助手会使用选定的配置目录和固定到已验收
99
+ DSH 版本的 pnpm 助手;系统没有可用 pnpm 时,会在当前 Windows 用户目录下缓存经过校验的副本。
100
+ 它不会递归扫盘、创建常驻命令、保存 profile 快照、要求管理员权限或擅自重启 DSH。只有识别到
101
+ 发布时间策略拦截时,才会仅对同一个固定安装命令重试一次。
105
102
 
106
103
  <details>
107
104
  <summary>官方 npm 方式(已安装 Node.js)</summary>
@@ -140,15 +137,18 @@ dsh --profile web --dump-config
140
137
 
141
138
  - ChatGPT OAuth 登录,凭据保留在本机;
142
139
  - Codex 模型和 Beta 图片生成与编辑直接出现在 DSH 会话中;
143
- - DSH 默认搜索与 Codex 订阅搜索可随时切换;
140
+ - 搜索来源是全局设置,可在 DSH 默认搜索与 Codex 订阅搜索之间切换;它对所有模型和会话生效,不会随当前模型自动切换;
144
141
  - 设置页显示服务端返回的额度、重置时间和更新时间;
145
142
  - 普通 Codex、Codex-Spark、Credits 等独立额度分开显示;
146
143
  - 显示重置卡数量与最早到期时间,也允许在额度未完全用尽时主动尝试,并经过分层确认且不会自动重试;
147
144
  - 输入框可用百分比或进度条显示当前 Codex 模型的剩余额度(默认关闭);
148
145
  - 输入框可为支持的 Codex 模型切换标准或高速模式;
149
- - 设置页可生成并复制无敏感信息的支持诊断,并直接打开反馈入口;报告不包含 OAuth 凭据、账号标识或授权时间;
146
+ - 上下文窗口提供标准、扩展和逐模型自定义;自定义直接填写完整 Token 数值,并在已审核的模型容量内交给 DSH 原生 Agent 压缩策略处理;
147
+ - 设置页可生成并复制无敏感信息的支持诊断,并直接打开反馈入口;报告包含有限的请求阶段、HTTP/网络分类、耗时区间和路由来源类型,但不包含 OAuth 凭据、账号标识、代理地址或授权时间;
150
148
  - 订阅路由不可用时明确报错,不会静默切换到其他付费路由。
151
149
 
150
+ 对于 OpenAI 与 ChatGPT 官方请求,插件可以沿用进程环境或操作系统中已有的 HTTPS 代理。插件本身不提供代理、转发服务、节点列表,也不会修改系统代理设置。
151
+
152
152
  ### 输入框额度
153
153
 
154
154
  <p align="center">
@@ -210,7 +210,7 @@ dsh plugin --profile web remove dsh-codex-subscription
210
210
  - **`dsh` 无法识别**:官方 npm 方式本来就不会创建全局 `dsh` 命令,请使用上面的完整 `npx -y @deepseek-ai/dsh@0.1.1-rc.2 ...` 命令;
211
211
  - **电脑上有多个 DSH**:请在目标 DSH 环境中运行标准命令;交给 Agent 时明确目标,或传入 `-DshPath`;
212
212
  - **安装仍然失败**:把上面的 Agent 文档链接发给 Agent,不要删除 profile 或随意修改系统 PATH。
213
- - **需要提交问题**:在设置页底部生成“支持诊断”,然后打开[使用问题表单](https://github.com/WSL043/dsh-codex-subscription/issues/new?template=install-problem.yml)。报告包含系统/运行时和有限的登录阶段,但不含凭据、账号标识、原始错误或完整日志;请粘贴到必填诊断栏,且不要附上登录链接、授权码或浏览器回调地址。
213
+ - **需要提交问题**:在设置页底部生成“支持诊断”,然后打开[使用问题表单](https://github.com/WSL043/dsh-codex-subscription/issues/new?template=install-problem.yml)。报告包含系统/运行时、有限的登录阶段和安全的请求失败分类,但不含凭据、账号标识、代理地址、原始响应或完整日志;请粘贴到必填诊断栏,且不要附上登录链接、授权码或浏览器回调地址。
214
214
 
215
215
  ## 边界与支持
216
216
 
package/lib/client.js CHANGED
@@ -49,6 +49,17 @@ window.__ModuleLoader__.load({
49
49
  }
50
50
  const ForwardRef = /*#__PURE__*/ react.forwardRef(BoltIcon);
51
51
  //#endregion
52
+ //#region src/image-edit.js
53
+ function buildImageEditDraft({ prompt = "", annotations = [], translate }) {
54
+ if (typeof translate !== "function") throw new TypeError("translate must be a function");
55
+ const notes = annotations.map((annotation, index) => ({
56
+ number: index + 1,
57
+ note: typeof annotation?.note === "string" ? annotation.note.trim() : ""
58
+ })).filter((annotation) => annotation.note !== "").map((annotation) => String(annotation.number) + ". " + annotation.note);
59
+ const base = prompt.trim() === "" ? translate("imageEditDefault") : prompt.trim();
60
+ return notes.length === 0 ? base : base + "\n\n" + translate("imageRegionNotes") + "\n" + notes.join("\n");
61
+ }
62
+ //#endregion
52
63
  //#region src/settings-contract.js
53
64
  const SETTINGS_NAMESPACE = "codex-subscription";
54
65
  const QUICK_QUOTA_MODE_FIELD = "quickQuotaMode";
@@ -60,8 +71,50 @@ window.__ModuleLoader__.load({
60
71
  const SPEED_MODE_STANDARD = "standard";
61
72
  const SPEED_MODE_FAST = "fast";
62
73
  const DEFAULT_SPEED_MODE = SPEED_MODE_STANDARD;
74
+ const CONTEXT_MODE_FIELD = "contextMode";
75
+ const CONTEXT_MODE_STANDARD = "standard";
76
+ const CONTEXT_MODE_EXTENDED = "extended";
77
+ const CONTEXT_MODE_CUSTOM = "custom";
78
+ const DEFAULT_CONTEXT_MODE = CONTEXT_MODE_STANDARD;
79
+ const CUSTOM_CONTEXT_WINDOW_FIELD = "customContextWindow";
80
+ const DEFAULT_CUSTOM_CONTEXT_WINDOW = 272e3;
81
+ const MIN_CUSTOM_CONTEXT_WINDOW = 128e3;
82
+ const MAX_CUSTOM_CONTEXT_WINDOW = 1e6;
83
+ const CUSTOM_CONTEXT_MODEL_FIELDS = Object.freeze({
84
+ "gpt-5.4": "customContextGpt54",
85
+ "gpt-5.4-mini": "customContextGpt54Mini",
86
+ "gpt-5.5": "customContextGpt55",
87
+ "gpt-5.6": "customContextGpt56"
88
+ });
89
+ const CUSTOM_CONTEXT_MODEL_CAPS = Object.freeze({
90
+ "gpt-5.4": 1e6,
91
+ "gpt-5.4-mini": 4e5,
92
+ "gpt-5.5": 1e6,
93
+ "gpt-5.6": 1e6
94
+ });
95
+ const CUSTOM_CONTEXT_MODEL_DEFAULTS = Object.freeze({
96
+ "gpt-5.4": 272e3,
97
+ "gpt-5.4-mini": 272e3,
98
+ "gpt-5.5": 272e3,
99
+ "gpt-5.6": 272e3
100
+ });
63
101
  const normalizeSearchProvider = (value) => ["dsh", "codex"].includes(value) ? value : "dsh";
64
102
  const normalizeSpeedMode = (value) => ["standard", "fast"].includes(value) ? value : DEFAULT_SPEED_MODE;
103
+ const normalizeContextMode = (value) => [
104
+ "standard",
105
+ "extended",
106
+ "custom"
107
+ ].includes(value) ? value : DEFAULT_CONTEXT_MODE;
108
+ const normalizeCustomContextWindow = (value, maximum = MAX_CUSTOM_CONTEXT_WINDOW) => {
109
+ if (!Number.isInteger(value)) return DEFAULT_CUSTOM_CONTEXT_WINDOW;
110
+ return Math.min(Math.max(value, MIN_CUSTOM_CONTEXT_WINDOW), maximum);
111
+ };
112
+ const formatContextWindow = (value) => value === 1e6 ? "1M" : `${Math.round(value / 1e3)}K`;
113
+ const parseContextWindow = (value) => {
114
+ const match = /^\s*(\d+)\s*$/u.exec(String(value));
115
+ if (match === null) return NaN;
116
+ return Number(match[1]);
117
+ };
65
118
  const normalizeQuickQuotaMode = (value, legacyVisible = false) => [
66
119
  "off",
67
120
  "percent",
@@ -170,10 +223,11 @@ window.__ModuleLoader__.load({
170
223
  diagnosticsFailed: "无法生成诊断信息。",
171
224
  feedbackOpen: "反馈问题",
172
225
  searchTitle: "搜索来源",
226
+ searchScope: "此设置对所有模型和会话生效,不会随当前模型自动切换。",
173
227
  searchDsh: "DSH 默认",
174
- searchDshHint: "当前搜索服务",
228
+ searchDshHint: "所有模型使用 DSH 当前搜索服务",
175
229
  searchCodex: "Codex 订阅",
176
- searchCodexHint: "ChatGPT 订阅搜索",
230
+ searchCodexHint: "所有模型通过已登录的 ChatGPT 订阅搜索",
177
231
  preferenceFailed: "设置未保存。",
178
232
  preferenceRetry: "重试",
179
233
  usage: "订阅额度",
@@ -231,6 +285,16 @@ window.__ModuleLoader__.load({
231
285
  quickQuotaOff: "关闭",
232
286
  quickQuotaPercent: "百分比",
233
287
  quickQuotaBar: "进度条",
288
+ contextTitle: "上下文窗口",
289
+ contextStandard: "标准",
290
+ contextStandardHint: "使用模型目录默认值;官方 Agent 预设会自动管理上下文。",
291
+ contextExtended: "扩展",
292
+ contextExtendedHint: "按模型使用 400K 或 1M;超过 272K 后可能消耗更多额度。",
293
+ contextCustom: "自定义",
294
+ contextCustomHint: "输入完整 Token 数值;较低数值会让官方 Agent 预设更早压缩上下文。",
295
+ contextTokens: "Token 上限",
296
+ contextFixed: "固定 {value}",
297
+ contextMaximum: "范围 128000–{value}",
234
298
  quickQuotaStatus: "Codex 剩余额度 {value}%",
235
299
  speedTitle: "速度",
236
300
  speedStandard: "标准",
@@ -302,10 +366,11 @@ window.__ModuleLoader__.load({
302
366
  diagnosticsFailed: "Could not create diagnostics.",
303
367
  feedbackOpen: "Report a problem",
304
368
  searchTitle: "Search source",
369
+ searchScope: "Applies to every model and session; it does not switch with the selected model.",
305
370
  searchDsh: "DSH default",
306
- searchDshHint: "Current search service",
371
+ searchDshHint: "Use DSH's current search service for every model",
307
372
  searchCodex: "Codex subscription",
308
- searchCodexHint: "ChatGPT subscription search",
373
+ searchCodexHint: "Search through the signed-in ChatGPT subscription for every model",
309
374
  preferenceFailed: "The setting was not saved.",
310
375
  preferenceRetry: "Retry",
311
376
  usage: "Subscription quota",
@@ -363,6 +428,16 @@ window.__ModuleLoader__.load({
363
428
  quickQuotaOff: "Off",
364
429
  quickQuotaPercent: "Percent",
365
430
  quickQuotaBar: "Progress bar",
431
+ contextTitle: "Context window",
432
+ contextStandard: "Standard",
433
+ contextStandardHint: "Use the model catalog default; official agent presets manage context automatically.",
434
+ contextExtended: "Extended",
435
+ contextExtendedHint: "Uses 400K or 1M by model; usage above 272K may consume more quota.",
436
+ contextCustom: "Custom",
437
+ contextCustomHint: "Enter the full token count; lower values make official agent presets compact sooner.",
438
+ contextTokens: "Token limit",
439
+ contextFixed: "Fixed {value}",
440
+ contextMaximum: "128000–{value}",
366
441
  quickQuotaStatus: "Codex quota: {value}% remaining",
367
442
  speedTitle: "Speed",
368
443
  speedStandard: "Standard",
@@ -409,6 +484,7 @@ window.__ModuleLoader__.load({
409
484
  imageRemoveAnnotation: "Remove note"
410
485
  };
411
486
  const STYLE = `
487
+ .codexSubscriptionSearchHead{display:flex;flex-direction:column;gap:1px}.codexSubscriptionSearchScope{font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary)}
412
488
  .codexSubscription{display:flex;flex-direction:column;gap:10px;max-width:720px;color:var(--dsw-alias-label-primary);container-type:inline-size}
413
489
  .codexSubscription h2,.codexSubscription h3,.codexSubscription p{margin:0}.codexSubscriptionHead{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
414
490
  .codexSubscription h2{font-size:16px;line-height:24px;font-weight:500}.codexSubscription h3{font-size:14px;line-height:22px;font-weight:500}
@@ -417,6 +493,7 @@ window.__ModuleLoader__.load({
417
493
  .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
494
  .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
495
  .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}
496
+ .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
497
  .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
498
  .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
499
  .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}
@@ -489,15 +566,7 @@ window.__ModuleLoader__.load({
489
566
  return cleaned.toLowerCase().endsWith(".png") ? cleaned : `${cleaned}.png`;
490
567
  };
491
568
  const imageByteSize = (bytes) => bytes < 1024 * 1024 ? `${Math.max(.1, bytes / 1024).toLocaleString(void 0, { maximumFractionDigits: 1 })} KB` : `${(bytes / 1024 / 1024).toLocaleString(void 0, { maximumFractionDigits: 1 })} MB`;
492
- function editDraft(prompt, annotations, t) {
493
- const notes = annotations.map((annotation, index) => ({
494
- number: index + 1,
495
- note: annotation.note.trim()
496
- })).filter((annotation) => annotation.note !== "").map((annotation) => String(annotation.number) + ". " + annotation.note);
497
- const base = prompt.trim() === "" ? t("imageEditDefault") : prompt.trim();
498
- return notes.length === 0 ? base : base + "\n\n" + t("imageRegionNotes") + "\n" + notes.join("\n");
499
- }
500
- function CodexGeneratedImage({ attachment, loadImage, attachForEdit, t }) {
569
+ function CodexGeneratedImage({ attachment, loadImage, attachForEdit, getImageViewer, t }) {
501
570
  const [attempt, setAttempt] = (0, react.useState)(0);
502
571
  const [error, setError] = (0, react.useState)(false);
503
572
  const [open, setOpen] = (0, react.useState)(false);
@@ -583,7 +652,11 @@ window.__ModuleLoader__.load({
583
652
  setEditBusy(true);
584
653
  setEditError(false);
585
654
  try {
586
- await attachForEdit(src, downloadName, editDraft(prompt, annotations, t));
655
+ await attachForEdit(src, downloadName, buildImageEditDraft({
656
+ prompt,
657
+ annotations,
658
+ translate: t
659
+ }));
587
660
  close();
588
661
  } catch {
589
662
  setEditError(true);
@@ -591,6 +664,35 @@ window.__ModuleLoader__.load({
591
664
  setEditBusy(false);
592
665
  }
593
666
  };
667
+ const openImage = () => {
668
+ if (src === void 0) return;
669
+ if ((getImageViewer?.())?.open?.({
670
+ items: [{
671
+ id: attachment.attachmentId ?? downloadName,
672
+ src,
673
+ name: label,
674
+ width: attachment.width,
675
+ height: attachment.height,
676
+ bytes: attachment.bytes
677
+ }],
678
+ opener: triggerRef.current,
679
+ source: "codex-generated",
680
+ annotations: true,
681
+ editor: {
682
+ label: t("imageEdit"),
683
+ busyLabel: t("imageEditPreparing"),
684
+ errorLabel: t("imageEditFailed"),
685
+ placeholder: t("imageEditPrompt"),
686
+ onSubmit: ({ prompt: nextPrompt, annotations: nextAnnotations }) => attachForEdit(src, downloadName, buildImageEditDraft({
687
+ prompt: nextPrompt,
688
+ annotations: nextAnnotations,
689
+ translate: t
690
+ }))
691
+ }
692
+ }) === true) return;
693
+ setZoom(1);
694
+ setOpen(true);
695
+ };
594
696
  if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
595
697
  type: "button",
596
698
  className: "codexGeneratedImageRetry",
@@ -756,19 +858,14 @@ window.__ModuleLoader__.load({
756
858
  className: "codexGeneratedImageFrame",
757
859
  title: t("imageOpen"),
758
860
  "aria-label": fill(t("imageOpenNamed"), { value: label }),
759
- onClick: () => {
760
- if (src !== void 0) {
761
- setZoom(1);
762
- setOpen(true);
763
- }
764
- },
861
+ onClick: openImage,
765
862
  children: src === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("imageLoading") }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
766
863
  src,
767
864
  alt: label
768
865
  })
769
866
  }), lightbox] });
770
867
  }
771
- function CodexImageToolRow({ block, loadImage, attachForEdit, t }) {
868
+ function CodexImageToolRow({ block, loadImage, attachForEdit, getImageViewer, t }) {
772
869
  const settled = block?.kind === "tool-result";
773
870
  const image = settled ? block.content.find((item) => item?.type === "image" && item.attachment !== void 0) : void 0;
774
871
  const failed = settled && block.isError === true;
@@ -806,6 +903,7 @@ window.__ModuleLoader__.load({
806
903
  attachment: image.attachment,
807
904
  loadImage,
808
905
  attachForEdit,
906
+ getImageViewer,
809
907
  t
810
908
  })
811
909
  }),
@@ -823,6 +921,7 @@ window.__ModuleLoader__.load({
823
921
  let fallback;
824
922
  let failedPatch;
825
923
  let generation = 0;
924
+ let contextModels = [];
826
925
  const nativeSnapshot = () => scope.getSnapshot();
827
926
  const read = () => {
828
927
  const native = nativeSnapshot();
@@ -832,6 +931,10 @@ window.__ModuleLoader__.load({
832
931
  quickQuotaMode: normalizeQuickQuotaMode(current.value?.[QUICK_QUOTA_MODE_FIELD], current.value?.[LEGACY_QUICK_QUOTA_FIELD]),
833
932
  searchProvider: normalizeSearchProvider(current.value?.[SEARCH_PROVIDER_FIELD]),
834
933
  speedMode: normalizeSpeedMode(current.value?.[SPEED_MODE_FIELD]),
934
+ contextMode: normalizeContextMode(current.value?.[CONTEXT_MODE_FIELD]),
935
+ customContextWindow: normalizeCustomContextWindow(current.value?.[CUSTOM_CONTEXT_WINDOW_FIELD]),
936
+ 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])])),
937
+ contextModels,
835
938
  writable: !updating && current.status === "ready" && current.writable === true,
836
939
  error
837
940
  });
@@ -848,13 +951,17 @@ window.__ModuleLoader__.load({
848
951
  publish();
849
952
  });
850
953
  const acceptFallback = (value) => {
954
+ contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
851
955
  fallbackStatus = "ready";
852
956
  fallback = {
853
957
  status: "ready",
854
958
  value: {
855
959
  [QUICK_QUOTA_MODE_FIELD]: normalizeQuickQuotaMode(value?.[QUICK_QUOTA_MODE_FIELD], value?.[LEGACY_QUICK_QUOTA_FIELD]),
856
960
  [SEARCH_PROVIDER_FIELD]: normalizeSearchProvider(value?.[SEARCH_PROVIDER_FIELD]),
857
- [SPEED_MODE_FIELD]: normalizeSpeedMode(value?.[SPEED_MODE_FIELD])
961
+ [SPEED_MODE_FIELD]: normalizeSpeedMode(value?.[SPEED_MODE_FIELD]),
962
+ [CONTEXT_MODE_FIELD]: normalizeContextMode(value?.[CONTEXT_MODE_FIELD]),
963
+ [CUSTOM_CONTEXT_WINDOW_FIELD]: normalizeCustomContextWindow(value?.[CUSTOM_CONTEXT_WINDOW_FIELD]),
964
+ ...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
965
  },
859
966
  writable: value?.writable === true
860
967
  };
@@ -866,11 +973,12 @@ window.__ModuleLoader__.load({
866
973
  fallback = void 0;
867
974
  error = false;
868
975
  publish();
869
- if (nativeSnapshot().status === "ready") return;
976
+ nativeSnapshot();
870
977
  try {
871
978
  const value = unwrap(await rpc.call(CHANNEL, "preferences/status", {}));
872
- if (current !== generation || nativeSnapshot().status === "ready") return;
873
- acceptFallback(value);
979
+ if (current !== generation) return;
980
+ if (nativeSnapshot().status === "ready") contextModels = Array.isArray(value?.contextModels) ? value.contextModels : [];
981
+ else acceptFallback(value);
874
982
  publish();
875
983
  } catch {
876
984
  if (current !== generation || nativeSnapshot().status === "ready") return;
@@ -1027,7 +1135,13 @@ window.__ModuleLoader__.load({
1027
1135
  });
1028
1136
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1029
1137
  className: "codexSubscriptionSearch",
1030
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("searchTitle") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1138
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1139
+ className: "codexSubscriptionSearchHead",
1140
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("searchTitle") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1141
+ className: "codexSubscriptionSearchScope",
1142
+ children: t("searchScope")
1143
+ })]
1144
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1031
1145
  className: "codexSubscriptionSearchChoices",
1032
1146
  role: "radiogroup",
1033
1147
  "aria-label": t("searchTitle"),
@@ -1035,6 +1149,125 @@ window.__ModuleLoader__.load({
1035
1149
  })]
1036
1150
  });
1037
1151
  }
1152
+ function ContextWindowPreference({ preference, t }) {
1153
+ const snapshot = usePreferenceSnapshot(preference);
1154
+ const writable = snapshot.status === "ready" && snapshot.writable === true;
1155
+ const [menuOpen, setMenuOpen] = (0, react.useState)(false);
1156
+ const modelRows = snapshot.contextModels.filter((model) => model.fixed !== true);
1157
+ const fixedRows = snapshot.contextModels.filter((model) => model.fixed === true);
1158
+ const [drafts, setDrafts] = (0, react.useState)({});
1159
+ (0, react.useEffect)(() => setDrafts(Object.fromEntries(modelRows.map((model) => [model.key, String(snapshot.customContextWindows[model.key])]))), [snapshot.customContextWindows, snapshot.contextModels]);
1160
+ const hint = snapshot.contextMode === "extended" ? t("contextExtendedHint") : snapshot.contextMode === "custom" ? t("contextCustomHint") : t("contextStandardHint");
1161
+ const commit = (modelKey) => {
1162
+ const parsed = parseContextWindow(drafts[modelKey]);
1163
+ if (!Number.isInteger(parsed)) {
1164
+ setDrafts((current) => ({
1165
+ ...current,
1166
+ [modelKey]: String(snapshot.customContextWindows[modelKey])
1167
+ }));
1168
+ return;
1169
+ }
1170
+ const value = normalizeCustomContextWindow(parsed, CUSTOM_CONTEXT_MODEL_CAPS[modelKey]);
1171
+ setDrafts((current) => ({
1172
+ ...current,
1173
+ [modelKey]: String(value)
1174
+ }));
1175
+ if (value !== snapshot.customContextWindows[modelKey]) preference.set({ [CUSTOM_CONTEXT_MODEL_FIELDS[modelKey]]: value });
1176
+ };
1177
+ const contextModeItems = [
1178
+ {
1179
+ id: CONTEXT_MODE_STANDARD,
1180
+ label: t("contextStandard")
1181
+ },
1182
+ {
1183
+ id: CONTEXT_MODE_EXTENDED,
1184
+ label: t("contextExtended")
1185
+ },
1186
+ {
1187
+ id: CONTEXT_MODE_CUSTOM,
1188
+ label: t("contextCustom")
1189
+ }
1190
+ ];
1191
+ const selectedMode = contextModeItems.find((item) => item.id === snapshot.contextMode)?.label ?? t("contextStandard");
1192
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1193
+ className: "codexSubscriptionContext",
1194
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1195
+ className: "codexSubscriptionContextHead",
1196
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1197
+ className: "codexSubscriptionContextCopy",
1198
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1199
+ className: "codexSubscriptionPreferenceLabel",
1200
+ children: t("contextTitle")
1201
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1202
+ className: "codexSubscriptionContextHint",
1203
+ children: hint
1204
+ })]
1205
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
1206
+ open: menuOpen,
1207
+ items: contextModeItems,
1208
+ selectedId: snapshot.contextMode,
1209
+ onSelect: (value) => {
1210
+ setMenuOpen(false);
1211
+ preference.set({ [CONTEXT_MODE_FIELD]: value });
1212
+ },
1213
+ onClose: () => setMenuOpen(false),
1214
+ align: "end",
1215
+ side: "bottom",
1216
+ portal: true,
1217
+ compact: true,
1218
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1219
+ className: "codexSubscriptionContextTrigger",
1220
+ type: "button",
1221
+ "aria-label": t("contextTitle"),
1222
+ "aria-haspopup": "menu",
1223
+ "aria-expanded": menuOpen,
1224
+ disabled: !writable,
1225
+ onClick: () => setMenuOpen((value) => !value),
1226
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: selectedMode }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})]
1227
+ })
1228
+ })]
1229
+ }), snapshot.contextMode === "custom" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1230
+ className: "codexSubscriptionContextModels",
1231
+ children: [modelRows.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1232
+ className: "codexSubscriptionContextModel",
1233
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1234
+ className: "codexSubscriptionContextModelCopy",
1235
+ 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) }) })]
1236
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1237
+ "aria-label": `${model.label} ${t("contextTokens")}`,
1238
+ className: "codexSubscriptionContextInput",
1239
+ type: "number",
1240
+ inputMode: "numeric",
1241
+ min: MIN_CUSTOM_CONTEXT_WINDOW,
1242
+ max: model.maximum,
1243
+ step: 1,
1244
+ value: drafts[model.key] ?? "",
1245
+ disabled: !writable,
1246
+ onChange: (event) => {
1247
+ const nextValue = event.currentTarget.value;
1248
+ setDrafts((current) => ({
1249
+ ...current,
1250
+ [model.key]: nextValue
1251
+ }));
1252
+ },
1253
+ onBlur: () => commit(model.key),
1254
+ onKeyDown: (event) => {
1255
+ if (event.key === "Enter") event.currentTarget.blur();
1256
+ }
1257
+ })]
1258
+ }, model.key)), fixedRows.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1259
+ className: "codexSubscriptionContextModel",
1260
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1261
+ className: "codexSubscriptionContextModelCopy",
1262
+ 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) }) })]
1263
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1264
+ className: "codexSubscriptionContextHint",
1265
+ children: formatContextWindow(model.maximum)
1266
+ })]
1267
+ }, model.key))]
1268
+ }) : null]
1269
+ });
1270
+ }
1038
1271
  function PreferencesCard({ preference, t }) {
1039
1272
  const snapshot = usePreferenceSnapshot(preference);
1040
1273
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1045,6 +1278,11 @@ window.__ModuleLoader__.load({
1045
1278
  t
1046
1279
  }),
1047
1280
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "codexSubscriptionDivider" }),
1281
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextWindowPreference, {
1282
+ preference,
1283
+ t
1284
+ }),
1285
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "codexSubscriptionDivider" }),
1048
1286
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickQuotaPreference, {
1049
1287
  preference,
1050
1288
  t
@@ -2140,6 +2378,13 @@ window.__ModuleLoader__.load({
2140
2378
  inject: (sessionId) => ({
2141
2379
  t,
2142
2380
  loadImage: (attachment) => conversation.resolveImage(sessionId, attachment),
2381
+ getImageViewer: () => {
2382
+ try {
2383
+ return ctx.get("nativeImageViewer");
2384
+ } catch {
2385
+ return;
2386
+ }
2387
+ },
2143
2388
  attachForEdit: async (src, filename, draft) => {
2144
2389
  const actx = sessions.scope(sessionId);
2145
2390
  if (actx === void 0 || typeof conversation.createDraftImages !== "function" || conversation.input?.for === void 0) throw new Error("This DSH version does not provide the image composer bridge");
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.9.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.9.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",