dsh-codex-subscription 1.14.3 → 2.0.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/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { clientRequestSchema } from "@deepseek-ai/dsh-client-connection";
1
2
  import * as dshCredentials from "@deepseek-ai/dsh-credentials";
2
3
  import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
3
4
  import { LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
@@ -17,6 +18,389 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
17
18
  import { constants } from "node:fs";
18
19
  import { lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
19
20
  import { dirname, join, resolve } from "node:path";
21
+ //#region src/image-models.js
22
+ const DEFAULT_IMAGE_MODEL = "gpt-image-2";
23
+ const IMAGE_MODELS = Object.freeze({
24
+ "gpt-image-2": Object.freeze([
25
+ "auto",
26
+ "low",
27
+ "medium",
28
+ "high"
29
+ ]),
30
+ "gpt-image-2.5-flare": Object.freeze([
31
+ "auto",
32
+ "low",
33
+ "medium",
34
+ "high",
35
+ "xhigh",
36
+ "max"
37
+ ]),
38
+ "gpt-image-2.5-sunburst": Object.freeze([
39
+ "auto",
40
+ "low",
41
+ "medium",
42
+ "high",
43
+ "xhigh",
44
+ "max"
45
+ ])
46
+ });
47
+ function resolveImageModel(model = DEFAULT_IMAGE_MODEL) {
48
+ if (typeof model !== "string" || !Object.hasOwn(IMAGE_MODELS, model)) throw new Error("Unknown image model");
49
+ return model;
50
+ }
51
+ function validateImageQuality(model, quality) {
52
+ if (!IMAGE_MODELS[resolveImageModel(model)].includes(quality)) throw new Error(`Unsupported quality for ${model}`);
53
+ }
54
+ //#endregion
55
+ //#region src/image-features.js
56
+ const IMAGE_FEATURE_DEFAULTS = Object.freeze({
57
+ imageGeneration: true,
58
+ imageShortcut: true,
59
+ imageEditing: true,
60
+ imageViewer: true,
61
+ imageAnnotations: true,
62
+ imageSketch: true
63
+ });
64
+ function readImageFeatures(value = {}) {
65
+ return Object.fromEntries(Object.entries(IMAGE_FEATURE_DEFAULTS).map(([key, fallback]) => [key, typeof value?.[key] === "boolean" ? value[key] : fallback]));
66
+ }
67
+ function readImageDefaults(value = {}) {
68
+ const imageModel = Object.hasOwn(IMAGE_MODELS, value?.imageModel) ? value.imageModel : DEFAULT_IMAGE_MODEL;
69
+ return {
70
+ imageModel,
71
+ imageQuality: IMAGE_MODELS[imageModel].includes(value?.imageQuality) ? value.imageQuality : "auto"
72
+ };
73
+ }
74
+ function imageFeaturePatch(value = {}) {
75
+ const patch = {};
76
+ if (Object.hasOwn(value, "imageModel")) patch.imageModel = resolveImageModel(value.imageModel);
77
+ if (Object.hasOwn(value, "imageQuality")) {
78
+ if (![
79
+ "auto",
80
+ "low",
81
+ "medium",
82
+ "high",
83
+ "xhigh",
84
+ "max"
85
+ ].includes(value.imageQuality)) throw new Error("Invalid image quality");
86
+ patch.imageQuality = value.imageQuality;
87
+ }
88
+ for (const key of Object.keys(IMAGE_FEATURE_DEFAULTS)) {
89
+ if (!Object.hasOwn(value, key)) continue;
90
+ if (typeof value[key] !== "boolean") throw new Error("Invalid image feature preference");
91
+ patch[key] = value[key];
92
+ }
93
+ return patch;
94
+ }
95
+ function assertImageOperation(features, editing) {
96
+ const current = readImageFeatures(features);
97
+ if (!(editing ? current.imageEditing : current.imageGeneration)) throw new Error(editing ? "Image editing is disabled in subscription settings" : "Image generation is disabled in subscription settings");
98
+ }
99
+ //#endregion
100
+ //#region src/capability-settings.js
101
+ const CUSTOM_CONTEXT_OVERRIDES_FIELD = "customContextModels";
102
+ const SEARCH_MODE_FIELD = "searchMode";
103
+ const SEARCH_DOMAINS_FIELD = "searchDomains";
104
+ const QUOTA_ALERTS_FIELD = "quotaAlerts";
105
+ const SEARCH_MODES = [
106
+ "live",
107
+ "cached",
108
+ "disabled"
109
+ ];
110
+ const QUOTA_ALERT_MODES = [
111
+ "off",
112
+ "important",
113
+ "early",
114
+ "custom"
115
+ ];
116
+ const QUOTA_THRESHOLD_FIELDS = ["quotaShortThreshold", "quotaLongThreshold"];
117
+ const validQuotaThreshold = (value) => Number.isInteger(value) && value >= 1 && value <= 100;
118
+ const MAX_CONTEXT_BUDGET = 16e6;
119
+ const validModelKey = (key) => typeof key === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,95}$/u.test(key) && ![
120
+ "constructor",
121
+ "prototype",
122
+ "__proto__"
123
+ ].includes(key);
124
+ function normalizeContextOverrides(value) {
125
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
126
+ return Object.fromEntries(Object.entries(value).filter(([key, size]) => validModelKey(key) && Number.isSafeInteger(size) && size > 0 && size <= 16e6).slice(0, 64));
127
+ }
128
+ function normalizeSearchDomains(value) {
129
+ if (!Array.isArray(value) || value.length > 20) throw new Error("Invalid search domains");
130
+ return [...new Set(value.map((item) => {
131
+ if (typeof item !== "string" || item.length > 253 || !/^[\p{L}\p{N}.-]+$/u.test(item)) throw new Error("Invalid search domain");
132
+ const hostname = new URL(`https://${item}`).hostname.toLowerCase();
133
+ if (!hostname.includes(".") || hostname.split(".").some((part) => !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(part))) throw new Error("Invalid search domain");
134
+ return hostname;
135
+ }))];
136
+ }
137
+ function readCapabilitySettings(value = {}) {
138
+ return {
139
+ ...Object.fromEntries(QUOTA_THRESHOLD_FIELDS.map((key) => [key, validQuotaThreshold(value[key]) ? value[key] : 20])),
140
+ ...readImageFeatures(value),
141
+ ...readImageDefaults(value),
142
+ [CUSTOM_CONTEXT_OVERRIDES_FIELD]: normalizeContextOverrides(value[CUSTOM_CONTEXT_OVERRIDES_FIELD]),
143
+ [SEARCH_MODE_FIELD]: SEARCH_MODES.includes(value["searchMode"]) ? value[SEARCH_MODE_FIELD] : "live",
144
+ [SEARCH_DOMAINS_FIELD]: normalizeSearchDomains(value["searchDomains"] ?? []),
145
+ [QUOTA_ALERTS_FIELD]: QUOTA_ALERT_MODES.includes(value["quotaAlerts"]) ? value[QUOTA_ALERTS_FIELD] : "important"
146
+ };
147
+ }
148
+ function capabilityPatch(payload) {
149
+ const patch = imageFeaturePatch(payload ?? {});
150
+ for (const field of QUOTA_THRESHOLD_FIELDS) {
151
+ if (!Object.hasOwn(payload ?? {}, field)) continue;
152
+ if (!validQuotaThreshold(payload[field])) throw new Error("Threshold must be an integer from 1 to 100");
153
+ patch[field] = payload[field];
154
+ }
155
+ for (const [key, choices] of [[SEARCH_MODE_FIELD, SEARCH_MODES], [QUOTA_ALERTS_FIELD, QUOTA_ALERT_MODES]]) {
156
+ if (!Object.hasOwn(payload ?? {}, key)) continue;
157
+ if (!choices.includes(payload[key])) throw new Error("Invalid capability preference");
158
+ patch[key] = payload[key];
159
+ }
160
+ if (Object.hasOwn(payload ?? {}, "searchDomains")) patch[SEARCH_DOMAINS_FIELD] = normalizeSearchDomains(payload[SEARCH_DOMAINS_FIELD]);
161
+ if (Object.hasOwn(payload ?? {}, "customContextModels")) {
162
+ const original = payload[CUSTOM_CONTEXT_OVERRIDES_FIELD];
163
+ const normalized = normalizeContextOverrides(original);
164
+ if (JSON.stringify(normalized) !== JSON.stringify(original)) throw new Error("Invalid model context preferences");
165
+ patch[CUSTOM_CONTEXT_OVERRIDES_FIELD] = normalized;
166
+ }
167
+ return patch;
168
+ }
169
+ //#endregion
170
+ //#region src/settings-contract.js
171
+ const SETTINGS_NAMESPACE = "codex-subscription";
172
+ const QUICK_QUOTA_MODE_FIELD = "quickQuotaMode";
173
+ const LEGACY_QUICK_QUOTA_FIELD = "quickQuotaVisible";
174
+ const QUICK_QUOTA_MODE_PERCENT = "percent";
175
+ const QUICK_QUOTA_MODE_FORECAST = "forecast";
176
+ const SEARCH_PROVIDER_FIELD = "searchProvider";
177
+ const SEARCH_PROVIDER_AUTO = "auto";
178
+ const SEARCH_PROVIDER_CODEX = "codex";
179
+ const DEFAULT_SEARCH_PROVIDER = SEARCH_PROVIDER_AUTO;
180
+ const SPEED_MODE_FIELD = "speedMode";
181
+ const SPEED_MODE_STANDARD = "standard";
182
+ const SPEED_MODE_FAST = "fast";
183
+ const DEFAULT_SPEED_MODE = SPEED_MODE_STANDARD;
184
+ const OUTPUT_VERBOSITY_FIELD = "outputVerbosity";
185
+ const OUTPUT_VERBOSITY_DEFAULT = "default";
186
+ const OUTPUT_VERBOSITY_MEDIUM = "medium";
187
+ const OUTPUT_VERBOSITY_HIGH = "high";
188
+ const DEFAULT_OUTPUT_VERBOSITY = OUTPUT_VERBOSITY_DEFAULT;
189
+ const CONTEXT_MODE_FIELD = "contextMode";
190
+ const CONTEXT_MODE_STANDARD = "standard";
191
+ const CONTEXT_MODE_EXTENDED = "extended";
192
+ const CONTEXT_MODE_CUSTOM = "custom";
193
+ const DEFAULT_CONTEXT_MODE = CONTEXT_MODE_STANDARD;
194
+ const CUSTOM_CONTEXT_WINDOW_FIELD = "customContextWindow";
195
+ const DEFAULT_CUSTOM_CONTEXT_WINDOW = 272e3;
196
+ const MIN_CUSTOM_CONTEXT_WINDOW = 128e3;
197
+ const MAX_CUSTOM_CONTEXT_WINDOW = 1e6;
198
+ const CUSTOM_CONTEXT_MODEL_FIELDS = Object.freeze({
199
+ "gpt-5.4": "customContextGpt54",
200
+ "gpt-5.4-mini": "customContextGpt54Mini",
201
+ "gpt-5.5": "customContextGpt55",
202
+ "gpt-5.6": "customContextGpt56",
203
+ "gpt-6-astra": "customContextGpt6Astra"
204
+ });
205
+ const CUSTOM_CONTEXT_MODEL_CAPS = Object.freeze({
206
+ "gpt-5.4": 1e6,
207
+ "gpt-5.4-mini": 4e5,
208
+ "gpt-5.5": 1e6,
209
+ "gpt-5.6": 1e6,
210
+ "gpt-6-astra": 872e3
211
+ });
212
+ const CUSTOM_CONTEXT_MODEL_DEFAULTS = Object.freeze({
213
+ "gpt-5.4": 272e3,
214
+ "gpt-5.4-mini": 272e3,
215
+ "gpt-5.5": 272e3,
216
+ "gpt-5.6": 272e3,
217
+ "gpt-6-astra": 272e3
218
+ });
219
+ const normalizeOutputVerbosity = (value) => [
220
+ "default",
221
+ "low",
222
+ "medium",
223
+ "high"
224
+ ].includes(value) ? value : DEFAULT_OUTPUT_VERBOSITY;
225
+ const normalizeContextMode = (value) => [
226
+ "standard",
227
+ "extended",
228
+ "custom"
229
+ ].includes(value) ? value : DEFAULT_CONTEXT_MODE;
230
+ const normalizeCustomContextWindow = (value, maximum = MAX_CUSTOM_CONTEXT_WINDOW) => {
231
+ if (!Number.isInteger(value)) return DEFAULT_CUSTOM_CONTEXT_WINDOW;
232
+ return Math.min(Math.max(value, MIN_CUSTOM_CONTEXT_WINDOW), maximum);
233
+ };
234
+ const customContextModelKey = (modelId) => modelId?.startsWith("gpt-5.6-") ? "gpt-5.6" : modelId;
235
+ function modelContextMaximum(model) {
236
+ const explicit = Number.isSafeInteger(model?.maxContextWindow) && model.maxContextWindow > 0 ? model.maxContextWindow : void 0;
237
+ const fallback = CUSTOM_CONTEXT_MODEL_CAPS[customContextModelKey(model?.id)] ?? model?.contextWindow;
238
+ return Math.min(MAX_CONTEXT_BUDGET, explicit ?? fallback ?? 272e3);
239
+ }
240
+ function clampModelContext(value, maximum, fallback = DEFAULT_CUSTOM_CONTEXT_WINDOW) {
241
+ return Math.max(Math.min(MIN_CUSTOM_CONTEXT_WINDOW, maximum), Math.min(Number.isSafeInteger(value) ? value : fallback, maximum));
242
+ }
243
+ function contextModelGroups(models) {
244
+ const groups = /* @__PURE__ */ new Map();
245
+ for (const model of models ?? []) {
246
+ if (model?.id === "gpt-5.3-codex-spark") {
247
+ groups.set(model.id, {
248
+ key: model.id,
249
+ label: model.name ?? model.id,
250
+ maximum: 128e3,
251
+ fixed: true
252
+ });
253
+ continue;
254
+ }
255
+ const key = customContextModelKey(model?.id);
256
+ if (!validModelKey(key)) continue;
257
+ const maximum = modelContextMaximum(model);
258
+ if (key !== "gpt-5.6") {
259
+ groups.set(key, {
260
+ key,
261
+ label: model.name ?? model.id,
262
+ maximum,
263
+ ...Object.hasOwn(CUSTOM_CONTEXT_MODEL_FIELDS, key) ? {} : { default: clampModelContext(model.contextWindow, maximum) }
264
+ });
265
+ continue;
266
+ }
267
+ const variant = String(model.name ?? model.id).replace(/^GPT-5\.6[ -]/iu, "");
268
+ const current = groups.get(key);
269
+ groups.set(key, {
270
+ key,
271
+ label: `GPT-5.6 ${current === void 0 ? variant : `${current.label.replace(/^GPT-5\.6 /u, "")} / ${variant}`}`,
272
+ maximum: Math.min(current?.maximum ?? maximum, maximum)
273
+ });
274
+ }
275
+ return [...groups.values()];
276
+ }
277
+ const normalizeQuickQuotaMode = (value, legacyVisible = false) => [
278
+ "off",
279
+ "percent",
280
+ "bar",
281
+ "forecast"
282
+ ].includes(value) ? value : legacyVisible === true ? QUICK_QUOTA_MODE_PERCENT : "off";
283
+ const supportsCodexFastMode = (modelId) => typeof modelId === "string" && (/^gpt-5\.(?:5|6)(?:$|-)/u.test(modelId) || modelId === "gpt-5.4" || modelId === "gpt-6-astra");
284
+ //#endregion
285
+ //#region src/preference-fields.js
286
+ const PREFERENCE_FIELDS = Object.freeze({
287
+ [QUICK_QUOTA_MODE_FIELD]: {
288
+ choices: [
289
+ "off",
290
+ QUICK_QUOTA_MODE_PERCENT,
291
+ "bar",
292
+ QUICK_QUOTA_MODE_FORECAST
293
+ ],
294
+ error: "Invalid quick quota preference"
295
+ },
296
+ [SEARCH_PROVIDER_FIELD]: {
297
+ choices: [
298
+ SEARCH_PROVIDER_AUTO,
299
+ "dsh",
300
+ SEARCH_PROVIDER_CODEX
301
+ ],
302
+ default: DEFAULT_SEARCH_PROVIDER,
303
+ error: "Invalid search provider preference"
304
+ },
305
+ [SPEED_MODE_FIELD]: {
306
+ choices: [SPEED_MODE_STANDARD, SPEED_MODE_FAST],
307
+ default: DEFAULT_SPEED_MODE,
308
+ error: "Invalid speed mode preference"
309
+ },
310
+ [OUTPUT_VERBOSITY_FIELD]: {
311
+ choices: [
312
+ OUTPUT_VERBOSITY_DEFAULT,
313
+ "low",
314
+ OUTPUT_VERBOSITY_MEDIUM,
315
+ OUTPUT_VERBOSITY_HIGH
316
+ ],
317
+ default: DEFAULT_OUTPUT_VERBOSITY,
318
+ error: "Invalid output verbosity preference"
319
+ },
320
+ [CONTEXT_MODE_FIELD]: {
321
+ choices: [
322
+ CONTEXT_MODE_STANDARD,
323
+ CONTEXT_MODE_EXTENDED,
324
+ CONTEXT_MODE_CUSTOM
325
+ ],
326
+ default: DEFAULT_CONTEXT_MODE,
327
+ error: "Invalid context mode preference"
328
+ }
329
+ });
330
+ //#endregion
331
+ //#region src/rpc-contract.js
332
+ const RPC_ENDPOINTS = Object.freeze([
333
+ "status",
334
+ "login/start",
335
+ "login/status",
336
+ "login/submit",
337
+ "login/cancel",
338
+ "logout",
339
+ "account/select",
340
+ "account/remove",
341
+ "usage",
342
+ "diagnostics",
343
+ "preferences/status",
344
+ "preferences/models",
345
+ "preferences/update",
346
+ "reset-credit/inspect",
347
+ "reset-credit/prepare",
348
+ "reset-credit/consume",
349
+ "image/original/chunk"
350
+ ]);
351
+ //#endregion
352
+ //#region src/subscription-transport.js
353
+ /** Exact routes stay inside DSH's authenticated /api bridge and body limit. */
354
+ function registerSubscriptionTransport(connection, handler) {
355
+ const disposers = [];
356
+ try {
357
+ for (const endpoint of RPC_ENDPOINTS) {
358
+ const method = `codex-subscription/${endpoint}`;
359
+ disposers.push(connection.fetch.register({
360
+ path: `/api/${method}`,
361
+ methods: ["POST"],
362
+ requestBody: "buffered",
363
+ async fetch(request) {
364
+ if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return new Response("content type must be application/json", { status: 415 });
365
+ let body;
366
+ try {
367
+ body = await request.json();
368
+ } catch {
369
+ return new Response("invalid JSON", { status: 400 });
370
+ }
371
+ const envelope = clientRequestSchema.safeParse(body);
372
+ if (!envelope.success || envelope.data.method !== method) return new Response("invalid RPC envelope", { status: 400 });
373
+ let result;
374
+ try {
375
+ request.signal.throwIfAborted();
376
+ result = await handler(endpoint, envelope.data.payload, request.signal);
377
+ } catch {
378
+ result = {
379
+ ok: false,
380
+ error: {
381
+ code: "internal",
382
+ message: "Subscription request failed",
383
+ details: { issues: [] }
384
+ }
385
+ };
386
+ }
387
+ return Response.json({
388
+ type: "server-response",
389
+ rpcId: envelope.data.rpcId,
390
+ result
391
+ });
392
+ }
393
+ }));
394
+ }
395
+ } catch (error) {
396
+ for (const dispose of disposers.reverse()) dispose();
397
+ throw error;
398
+ }
399
+ return () => {
400
+ for (const dispose of disposers.reverse()) dispose();
401
+ };
402
+ }
403
+ //#endregion
20
404
  //#region src/account-vault.js
21
405
  const VERSION = 1;
22
406
  const DEFAULT_LABEL = "Account 1";
@@ -1126,111 +1510,6 @@ function createCodexNetworkTransport(options = {}) {
1126
1510
  });
1127
1511
  }
1128
1512
  //#endregion
1129
- //#region src/settings-contract.js
1130
- const SETTINGS_NAMESPACE = "codex-subscription";
1131
- const QUICK_QUOTA_MODE_FIELD = "quickQuotaMode";
1132
- const LEGACY_QUICK_QUOTA_FIELD = "quickQuotaVisible";
1133
- const QUICK_QUOTA_MODE_PERCENT = "percent";
1134
- const QUICK_QUOTA_MODE_FORECAST = "forecast";
1135
- const SEARCH_PROVIDER_FIELD = "searchProvider";
1136
- const SEARCH_PROVIDER_AUTO = "auto";
1137
- const SEARCH_PROVIDER_CODEX = "codex";
1138
- const DEFAULT_SEARCH_PROVIDER = SEARCH_PROVIDER_AUTO;
1139
- const SPEED_MODE_FIELD = "speedMode";
1140
- const SPEED_MODE_STANDARD = "standard";
1141
- const SPEED_MODE_FAST = "fast";
1142
- const DEFAULT_SPEED_MODE = SPEED_MODE_STANDARD;
1143
- const OUTPUT_VERBOSITY_FIELD = "outputVerbosity";
1144
- const OUTPUT_VERBOSITY_DEFAULT = "default";
1145
- const OUTPUT_VERBOSITY_MEDIUM = "medium";
1146
- const OUTPUT_VERBOSITY_HIGH = "high";
1147
- const DEFAULT_OUTPUT_VERBOSITY = OUTPUT_VERBOSITY_DEFAULT;
1148
- const CONTEXT_MODE_FIELD = "contextMode";
1149
- const CONTEXT_MODE_STANDARD = "standard";
1150
- const CONTEXT_MODE_EXTENDED = "extended";
1151
- const CONTEXT_MODE_CUSTOM = "custom";
1152
- const DEFAULT_CONTEXT_MODE = CONTEXT_MODE_STANDARD;
1153
- const CUSTOM_CONTEXT_WINDOW_FIELD = "customContextWindow";
1154
- const DEFAULT_CUSTOM_CONTEXT_WINDOW = 272e3;
1155
- const MIN_CUSTOM_CONTEXT_WINDOW = 128e3;
1156
- const MAX_CUSTOM_CONTEXT_WINDOW = 1e6;
1157
- const CUSTOM_CONTEXT_MODEL_FIELDS = Object.freeze({
1158
- "gpt-5.4": "customContextGpt54",
1159
- "gpt-5.4-mini": "customContextGpt54Mini",
1160
- "gpt-5.5": "customContextGpt55",
1161
- "gpt-5.6": "customContextGpt56",
1162
- "gpt-6-astra": "customContextGpt6Astra"
1163
- });
1164
- const CUSTOM_CONTEXT_MODEL_CAPS = Object.freeze({
1165
- "gpt-5.4": 1e6,
1166
- "gpt-5.4-mini": 4e5,
1167
- "gpt-5.5": 1e6,
1168
- "gpt-5.6": 1e6,
1169
- "gpt-6-astra": 872e3
1170
- });
1171
- const CUSTOM_CONTEXT_MODEL_DEFAULTS = Object.freeze({
1172
- "gpt-5.4": 272e3,
1173
- "gpt-5.4-mini": 272e3,
1174
- "gpt-5.5": 272e3,
1175
- "gpt-5.6": 272e3,
1176
- "gpt-6-astra": 272e3
1177
- });
1178
- const normalizeOutputVerbosity = (value) => [
1179
- "default",
1180
- "low",
1181
- "medium",
1182
- "high"
1183
- ].includes(value) ? value : DEFAULT_OUTPUT_VERBOSITY;
1184
- const normalizeContextMode = (value) => [
1185
- "standard",
1186
- "extended",
1187
- "custom"
1188
- ].includes(value) ? value : DEFAULT_CONTEXT_MODE;
1189
- const normalizeCustomContextWindow = (value, maximum = MAX_CUSTOM_CONTEXT_WINDOW) => {
1190
- if (!Number.isInteger(value)) return DEFAULT_CUSTOM_CONTEXT_WINDOW;
1191
- return Math.min(Math.max(value, MIN_CUSTOM_CONTEXT_WINDOW), maximum);
1192
- };
1193
- const customContextModelKey = (modelId) => modelId?.startsWith("gpt-5.6-") ? "gpt-5.6" : modelId;
1194
- function contextModelGroups(models) {
1195
- const groups = /* @__PURE__ */ new Map();
1196
- for (const model of models ?? []) {
1197
- if (model?.id === "gpt-5.3-codex-spark") {
1198
- groups.set(model.id, {
1199
- key: model.id,
1200
- label: model.name ?? model.id,
1201
- maximum: 128e3,
1202
- fixed: true
1203
- });
1204
- continue;
1205
- }
1206
- const key = customContextModelKey(model?.id);
1207
- if (!Object.hasOwn(CUSTOM_CONTEXT_MODEL_FIELDS, key)) continue;
1208
- if (key !== "gpt-5.6") {
1209
- groups.set(key, {
1210
- key,
1211
- label: model.name ?? model.id,
1212
- maximum: CUSTOM_CONTEXT_MODEL_CAPS[key]
1213
- });
1214
- continue;
1215
- }
1216
- const variant = String(model.name ?? model.id).replace(/^GPT-5\.6[ -]/iu, "");
1217
- const current = groups.get(key);
1218
- groups.set(key, {
1219
- key,
1220
- label: `GPT-5.6 ${current === void 0 ? variant : `${current.label.replace(/^GPT-5\.6 /u, "")} / ${variant}`}`,
1221
- maximum: CUSTOM_CONTEXT_MODEL_CAPS[key]
1222
- });
1223
- }
1224
- return [...groups.values()];
1225
- }
1226
- const normalizeQuickQuotaMode = (value, legacyVisible = false) => [
1227
- "off",
1228
- "percent",
1229
- "bar",
1230
- "forecast"
1231
- ].includes(value) ? value : legacyVisible === true ? QUICK_QUOTA_MODE_PERCENT : "off";
1232
- const supportsCodexFastMode = (modelId) => typeof modelId === "string" && (/^gpt-5\.(?:5|6)(?:$|-)/u.test(modelId) || modelId === "gpt-5.4");
1233
- //#endregion
1234
1513
  //#region src/pi-ai-runtime.js
1235
1514
  const FAST_SERVICE_TIER = "priority";
1236
1515
  /**
@@ -1245,15 +1524,6 @@ const FAST_SERVICE_TIER = "priority";
1245
1524
  * persistence, headers, transport, and model behavior remain owned by the
1246
1525
  * original provider.
1247
1526
  */
1248
- const EXTENDED_CONTEXT_WINDOWS = Object.freeze({
1249
- "gpt-5.4": 1e6,
1250
- "gpt-5.4-mini": 4e5,
1251
- "gpt-5.5": 1e6,
1252
- "gpt-5.6-luna": 1e6,
1253
- "gpt-5.6-sol": 1e6,
1254
- "gpt-5.6-terra": 1e6,
1255
- "gpt-6-astra": CUSTOM_CONTEXT_MODEL_CAPS["gpt-6-astra"]
1256
- });
1257
1527
  function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, resolveOutputVerbosity = () => OUTPUT_VERBOSITY_DEFAULT, resolveContextMode = () => void 0, resolveCustomContextWindow = () => void 0, catalog, runNetwork = (_area, operation) => operation() } = {}) {
1258
1528
  const provider = createOpenAICodexProvider();
1259
1529
  const requestToken = Object.freeze({
@@ -1301,17 +1571,17 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
1301
1571
  };
1302
1572
  };
1303
1573
  const getModels = () => (catalog?.getModels(provider.getModels()) ?? provider.getModels()).map((model) => {
1304
- const maximum = EXTENDED_CONTEXT_WINDOWS[model.id];
1574
+ const maximum = modelContextMaximum(model);
1305
1575
  const mode = resolveContextMode();
1306
- if (maximum === void 0 || !["extended", "custom"].includes(mode)) return model;
1576
+ if (model.id === "gpt-5.3-codex-spark" || !["extended", "custom"].includes(mode)) return model;
1307
1577
  if (mode === "extended") {
1308
- const contextWindow = model.id === "gpt-6-astra" ? maximum : Math.max(model.contextWindow, maximum);
1578
+ const contextWindow = maximum;
1309
1579
  return {
1310
1580
  ...model,
1311
1581
  contextWindow
1312
1582
  };
1313
1583
  }
1314
- const requested = normalizeCustomContextWindow(resolveCustomContextWindow(customContextModelKey(model.id)), maximum);
1584
+ const requested = clampModelContext(resolveCustomContextWindow(customContextModelKey(model.id)), maximum, model.contextWindow);
1315
1585
  return {
1316
1586
  ...model,
1317
1587
  contextWindow: requested
@@ -1345,7 +1615,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
1345
1615
  }
1346
1616
  //#endregion
1347
1617
  //#region src/version.js
1348
- const PACKAGE_VERSION = "1.14.3";
1618
+ const PACKAGE_VERSION = "2.0.0";
1349
1619
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
1350
1620
  //#endregion
1351
1621
  //#region src/model-catalog.js
@@ -1383,6 +1653,7 @@ function visibleModel(value) {
1383
1653
  priority: Number.isFinite(value.priority) ? value.priority : 0,
1384
1654
  input: input.length > 0 ? input : ["text"],
1385
1655
  contextWindow: positiveInteger$1(value.context_window) ?? positiveInteger$1(value.max_context_window),
1656
+ ...positiveInteger$1(value.max_context_window) === void 0 ? {} : { maxContextWindow: value.max_context_window },
1386
1657
  reasoning: supported.length > 0,
1387
1658
  thinkingLevelMap: reasoningMap(supported),
1388
1659
  supportVerbosity: value.support_verbosity === true,
@@ -1410,6 +1681,7 @@ function mergeModel(baseModels, remote) {
1410
1681
  reasoning: remote.reasoning,
1411
1682
  thinkingLevelMap: remote.thinkingLevelMap,
1412
1683
  ...remote.contextWindow === void 0 ? {} : { contextWindow: remote.contextWindow },
1684
+ ...remote.maxContextWindow === void 0 ? {} : { maxContextWindow: remote.maxContextWindow },
1413
1685
  ...base.id === remote.id ? {} : { cost: {
1414
1686
  input: 0,
1415
1687
  output: 0,
@@ -1429,10 +1701,13 @@ function createOfficialModelCatalog(options = {}) {
1429
1701
  let revision = 0;
1430
1702
  let refreshing;
1431
1703
  let generation = 0;
1704
+ let refreshStatus = "idle";
1432
1705
  const refresh = ({ signal } = {}) => {
1433
1706
  if (signal?.aborted) return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
1434
1707
  if (refreshing?.generation === generation) return refreshing.promise;
1435
1708
  const currentGeneration = generation;
1709
+ refreshStatus = "refreshing";
1710
+ let outcome = "idle";
1436
1711
  const controller = new AbortController();
1437
1712
  const abort = () => {
1438
1713
  if (!controller.signal.aborted) controller.abort(signal?.reason ?? /* @__PURE__ */ new Error("Codex model catalog refresh aborted"));
@@ -1464,7 +1739,10 @@ function createOfficialModelCatalog(options = {}) {
1464
1739
  signal: requestSignal
1465
1740
  });
1466
1741
  if (currentGeneration !== generation || requestSignal.aborted) return false;
1467
- if (response.status === 304) return false;
1742
+ if (response.status === 304) {
1743
+ outcome = "ok";
1744
+ return false;
1745
+ }
1468
1746
  if (!response.ok) throw new Error(`Codex model catalog failed (HTTP ${response.status})`);
1469
1747
  const remote = parseOfficialModelCatalog(await response.json());
1470
1748
  if (currentGeneration !== generation || requestSignal.aborted) return false;
@@ -1477,6 +1755,7 @@ function createOfficialModelCatalog(options = {}) {
1477
1755
  metadata = new Map(remote.map((model) => [model.id, model]));
1478
1756
  etag = nonEmpty$2(response.headers.get("etag")) ?? etag;
1479
1757
  revision += 1;
1758
+ outcome = "ok";
1480
1759
  return true;
1481
1760
  })();
1482
1761
  let rejectAborted;
@@ -1487,11 +1766,17 @@ function createOfficialModelCatalog(options = {}) {
1487
1766
  });
1488
1767
  timer = scheduleTimeout(() => controller.abort(timeoutError), timeoutMs);
1489
1768
  timer.unref?.();
1490
- const promise = Promise.race([work, abortPromise]).finally(() => {
1769
+ const promise = Promise.race([work, abortPromise]).catch((error) => {
1770
+ outcome = "failed";
1771
+ throw error;
1772
+ }).finally(() => {
1491
1773
  cancelTimeout(timer);
1492
1774
  signal?.removeEventListener("abort", abort);
1493
1775
  requestSignal.removeEventListener("abort", rejectAborted);
1494
- if (refreshing?.promise === promise) refreshing = void 0;
1776
+ if (refreshing?.promise === promise) {
1777
+ refreshing = void 0;
1778
+ refreshStatus = outcome;
1779
+ }
1495
1780
  });
1496
1781
  refreshing = {
1497
1782
  generation: currentGeneration,
@@ -1505,6 +1790,10 @@ function createOfficialModelCatalog(options = {}) {
1505
1790
  getModels: (fallback) => models ?? fallback,
1506
1791
  metadata: (modelId) => metadata.get(modelId),
1507
1792
  revision: () => revision,
1793
+ status: () => ({
1794
+ source: models === void 0 ? "fallback" : "online",
1795
+ refresh: refreshStatus
1796
+ }),
1508
1797
  clear() {
1509
1798
  generation += 1;
1510
1799
  const flight = refreshing;
@@ -1513,6 +1802,7 @@ function createOfficialModelCatalog(options = {}) {
1513
1802
  models = void 0;
1514
1803
  metadata = /* @__PURE__ */ new Map();
1515
1804
  etag = void 0;
1805
+ refreshStatus = "idle";
1516
1806
  revision += 1;
1517
1807
  }
1518
1808
  });
@@ -1580,6 +1870,9 @@ function createCodexSearchProvider(options) {
1580
1870
  id: CODEX_SEARCH_PROVIDER_ID,
1581
1871
  available: () => true,
1582
1872
  async search(request, signal) {
1873
+ signal?.throwIfAborted();
1874
+ const preferences = readCapabilitySettings(options.resolvePreferences?.());
1875
+ if (preferences.searchMode === "disabled") throw new WebError("Codex search is disabled in subscription settings", "WEB_PROVIDER_UNAVAILABLE");
1583
1876
  const auth = await options.getAuth({ signal });
1584
1877
  const credential = await options.readCredential({ signal });
1585
1878
  const access = auth?.auth?.apiKey;
@@ -1610,7 +1903,7 @@ function createCodexSearchProvider(options) {
1610
1903
  },
1611
1904
  settings: {
1612
1905
  allowed_callers: ["direct"],
1613
- external_web_access: true
1906
+ external_web_access: preferences.searchMode === "live"
1614
1907
  },
1615
1908
  max_output_tokens: MAX_OUTPUT_TOKENS
1616
1909
  }),
@@ -1628,7 +1921,12 @@ function createCodexSearchProvider(options) {
1628
1921
  throw new WebError("Codex returned an unreadable search response", "WEB_PROVIDER_ERROR", { cause: error });
1629
1922
  }
1630
1923
  try {
1631
- return parseSearchResponse(value);
1924
+ const result = parseSearchResponse(value);
1925
+ if (preferences.searchDomains.length > 0) result.sources = result.sources.filter((source) => {
1926
+ const hostname = new URL(source.url).hostname.toLowerCase();
1927
+ return preferences.searchDomains.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
1928
+ });
1929
+ return result;
1632
1930
  } catch (error) {
1633
1931
  throw new WebError("Codex returned a malformed search response", "WEB_PROVIDER_ERROR", { cause: error });
1634
1932
  }
@@ -1696,15 +1994,8 @@ function inheritedOriginalImageRef(session, assetId) {
1696
1994
  const CODEX_IMAGE_TOOL_NAME = "codex_image_generate";
1697
1995
  const CODEX_IMAGE_GENERATION_URL = "https://chatgpt.com/backend-api/codex/images/generations";
1698
1996
  const CODEX_IMAGE_EDIT_URL = "https://chatgpt.com/backend-api/codex/images/edits";
1699
- const IMAGE_MODEL = "gpt-image-2";
1700
1997
  const MAX_REFERENCE_IMAGES = 5;
1701
1998
  const RESPONSE_ENVELOPE_BYTES = 1024 * 1024;
1702
- const IMAGE_QUALITIES = /* @__PURE__ */ new Set([
1703
- "auto",
1704
- "low",
1705
- "medium",
1706
- "high"
1707
- ]);
1708
1999
  const IMAGE_BACKGROUNDS = /* @__PURE__ */ new Set([
1709
2000
  "auto",
1710
2001
  "transparent",
@@ -1730,7 +2021,7 @@ function normalizeImageOptions(args) {
1730
2021
  const quality = nonEmpty(args?.quality) ?? "auto";
1731
2022
  const background = nonEmpty(args?.background) ?? "auto";
1732
2023
  const size = nonEmpty(args?.size) ?? "auto";
1733
- if (!IMAGE_QUALITIES.has(quality)) throw new Error("quality must be auto, low, medium, or high");
2024
+ validateImageQuality(resolveImageModel(args?.model), quality);
1734
2025
  if (!IMAGE_BACKGROUNDS.has(background)) throw new Error("background must be auto, transparent, or opaque");
1735
2026
  if (size !== "auto") {
1736
2027
  const match = /^(\d+)x(\d+)$/u.exec(size);
@@ -1956,6 +2247,9 @@ function imageOutputSchema() {
1956
2247
  }
1957
2248
  },
1958
2249
  background: { type: "string" },
2250
+ requestedModel: { type: "string" },
2251
+ reportedModel: { type: "string" },
2252
+ requestedSize: { type: "string" },
1959
2253
  localPath: {
1960
2254
  type: "string",
1961
2255
  required: true,
@@ -1974,7 +2268,8 @@ function responseMetadata(value) {
1974
2268
  encoded,
1975
2269
  background: nonEmpty(value.background),
1976
2270
  quality: nonEmpty(value.quality),
1977
- size: nonEmpty(value.size)
2271
+ size: nonEmpty(value.size),
2272
+ reportedModel: nonEmpty(value.model)
1978
2273
  };
1979
2274
  }
1980
2275
  /** Create the DSH-native image-generation tool backed only by the ChatGPT subscription. */
@@ -1983,8 +2278,13 @@ function createCodexImageTool(options) {
1983
2278
  const attachments = options.attachments;
1984
2279
  return defineTool({
1985
2280
  name: CODEX_IMAGE_TOOL_NAME,
1986
- description: "Create a new image or explicitly edit selected prior images using the signed-in Codex subscription. Omit referenceImages only for a completely new image. For an edit, copy each complete image reference from the session image block or read_image result, including attachmentId in the exact form sha256:<64 lowercase hex>; never use a workspace path, absolute path, or filename, and do not omit the references to turn an edit into text-to-image generation. A bare 64-character hex digest is accepted and normalized to sha256:<64 lowercase hex>. If a reference is unavailable, call read_image and retry with its returned reference. Include only the exact prior image references the user asked to edit; never assume every image in the conversation is a reference. For annotation-guided edits, include both the named clean source and its numbered location reference, and preserve the numbered coordinates and requested changes in the prompt. The location-reference markers are guidance only and must not appear in the result. If those references cannot be identified, do not substitute unrelated images or silently ignore the annotations.",
2281
+ description: "Generate or edit images only when the user asks for image output, not when merely discussing images. Uses the signed-in Codex subscription. For a new image, omit referenceImages. For edits, copy complete references only for the images the user selected; obtain missing references using read_image. Never substitute paths, unrelated images, or text-only generation for an edit. For numbered annotations, include the clean source and location-reference image, preserve the requested changes and coordinates in the prompt, and remove guidance markers from the result. If the intended references cannot be identified, ask rather than guessing.",
1987
2282
  parameters: {
2283
+ model: {
2284
+ type: "string",
2285
+ enum: Object.keys(IMAGE_MODELS),
2286
+ description: "Optional image engine, independent of the conversation model. When omitted, uses the user image setting (initially gpt-image-2). The 2.5 identifiers are experimental subscription candidates; override only when explicitly requested. Never silently retry with another model."
2287
+ },
1988
2288
  prompt: {
1989
2289
  type: "string",
1990
2290
  required: true,
@@ -2000,7 +2300,9 @@ function createCodexImageTool(options) {
2000
2300
  "auto",
2001
2301
  "low",
2002
2302
  "medium",
2003
- "high"
2303
+ "high",
2304
+ "xhigh",
2305
+ "max"
2004
2306
  ],
2005
2307
  description: "Optional rendering quality. Use auto unless the user requests draft speed or final quality."
2006
2308
  },
@@ -2066,12 +2368,22 @@ function createCodexImageTool(options) {
2066
2368
  presentationMeta: (_args, value) => ({
2067
2369
  kind: "codex-subscription-image",
2068
2370
  schemaVersion: 1,
2069
- original: value.original
2371
+ original: value.original,
2372
+ ...value.requestedModel === void 0 ? {} : { requestedModel: value.requestedModel },
2373
+ ...value.reportedModel === void 0 ? {} : { reportedModel: value.reportedModel },
2374
+ ...value.requestedSize === void 0 ? {} : { requestedSize: value.requestedSize }
2070
2375
  })
2071
2376
  },
2072
2377
  timeoutMs: 300 * 1e3,
2073
2378
  isConcurrencySafe: () => false,
2074
2379
  async execute(args, exec) {
2380
+ assertImageOperation(options.getFeatures?.(), args.referenceImages !== void 0);
2381
+ const defaults = readImageDefaults(options.getFeatures?.());
2382
+ args = {
2383
+ ...args,
2384
+ model: args.model ?? defaults.imageModel,
2385
+ quality: args.quality ?? defaults.imageQuality
2386
+ };
2075
2387
  const prompt = nonEmpty(args.prompt);
2076
2388
  if (prompt === void 0) throw new Error("prompt must be a non-empty string");
2077
2389
  const imageOptions = normalizeImageOptions(args);
@@ -2086,6 +2398,7 @@ function createCodexImageTool(options) {
2086
2398
  const sessionMessages = editing && typeof options.getSessionMessages === "function" ? await options.getSessionMessages(exec.agent?.id) : void 0;
2087
2399
  const images = editing ? await editImages(args.referenceImages, attachments, exec.signal, sessionMessages ?? (options.getSessionMessages ? [] : void 0)) : void 0;
2088
2400
  let response;
2401
+ assertImageOperation(options.getFeatures?.(), editing);
2089
2402
  try {
2090
2403
  response = await fetchImage(editing ? CODEX_IMAGE_EDIT_URL : CODEX_IMAGE_GENERATION_URL, {
2091
2404
  method: "POST",
@@ -2103,7 +2416,7 @@ function createCodexImageTool(options) {
2103
2416
  ...images === void 0 ? {} : { images },
2104
2417
  prompt,
2105
2418
  background: imageOptions.background,
2106
- model: IMAGE_MODEL,
2419
+ model: resolveImageModel(args.model),
2107
2420
  quality: imageOptions.quality,
2108
2421
  size: imageOptions.size
2109
2422
  }),
@@ -2135,6 +2448,9 @@ function createCodexImageTool(options) {
2135
2448
  throw error;
2136
2449
  }
2137
2450
  const result = {
2451
+ requestedModel: resolveImageModel(args.model),
2452
+ requestedSize: imageOptions.size,
2453
+ ...metadata.reportedModel === void 0 ? {} : { reportedModel: metadata.reportedModel },
2138
2454
  image: imageReference(ref),
2139
2455
  original,
2140
2456
  localPath: options.originalImages.originalPath(original.assetId),
@@ -2154,6 +2470,27 @@ function createCodexImageTool(options) {
2154
2470
  });
2155
2471
  }
2156
2472
  //#endregion
2473
+ //#region src/image-tool-registration.js
2474
+ /** Removing the tool also removes its schema from subsequent model requests. */
2475
+ function watchImageTool(settings, register) {
2476
+ let disposeTool;
2477
+ const sync = (value) => {
2478
+ const { imageGeneration, imageEditing } = readImageFeatures(value);
2479
+ if ((imageGeneration || imageEditing) && !disposeTool) disposeTool = register();
2480
+ else if (!imageGeneration && !imageEditing && disposeTool) {
2481
+ disposeTool();
2482
+ disposeTool = void 0;
2483
+ }
2484
+ };
2485
+ sync(settings.get());
2486
+ const unwatch = settings.watch(sync);
2487
+ return () => {
2488
+ unwatch();
2489
+ disposeTool?.();
2490
+ disposeTool = void 0;
2491
+ };
2492
+ }
2493
+ //#endregion
2157
2494
  //#region src/image-original-store.js
2158
2495
  const ORIGINAL_IMAGE_DIRECTORY = "dsh-codex-subscription/images/v1";
2159
2496
  const METADATA_VERSION = 1;
@@ -2268,7 +2605,8 @@ var OriginalImageStore = class {
2268
2605
  await Promise.all([assertPrivateFile(metadataFile), assertPrivateFile(originalFile)]);
2269
2606
  const metadata = parseMetadata(await readFile(metadataFile, "utf8"));
2270
2607
  if (metadata === void 0 || metadata.image.assetId !== assetId || metadata.sessionId !== sessionId && !originalImageRefsEqual(metadata.image, inherited)) return void 0;
2271
- const data = new Uint8Array(await readFile(originalFile));
2608
+ const buffer = await readFile(originalFile);
2609
+ const data = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
2272
2610
  const dimensions = pngDimensions(data);
2273
2611
  if (data.byteLength !== metadata.image.bytes || digest(data) !== metadata.image.sha256 || dimensions.width !== metadata.image.width || dimensions.height !== metadata.image.height) return void 0;
2274
2612
  return {
@@ -2284,10 +2622,11 @@ var OriginalImageStore = class {
2284
2622
  const stored = await this.read(sessionId, assetId, inherited);
2285
2623
  if (stored === void 0 || offset >= stored.data.byteLength || offset % 4194304 !== 0) return void 0;
2286
2624
  const end = Math.min(stored.data.byteLength, offset + ORIGINAL_IMAGE_CHUNK_BYTES);
2625
+ const chunk = Buffer.from(stored.data.buffer, stored.data.byteOffset + offset, end - offset);
2287
2626
  return {
2288
2627
  ref: stored.ref,
2289
2628
  offset,
2290
- encoded: Buffer.from(stored.data.subarray(offset, end)).toString("base64"),
2629
+ encoded: chunk.toString("base64"),
2291
2630
  done: end === stored.data.byteLength
2292
2631
  };
2293
2632
  }
@@ -2297,6 +2636,7 @@ var OriginalImageStore = class {
2297
2636
  const requestAreas = /* @__PURE__ */ new Set([
2298
2637
  "login",
2299
2638
  "model",
2639
+ "catalog",
2300
2640
  "quota",
2301
2641
  "quota-reset",
2302
2642
  "search",
@@ -2342,7 +2682,7 @@ function safeRequests(network) {
2342
2682
  return result;
2343
2683
  }
2344
2684
  /** Build a support report that deliberately excludes OAuth and account metadata. */
2345
- async function createSubscriptionDiagnostics({ auth, preferences, login = { phase: "idle" }, network }) {
2685
+ async function createSubscriptionDiagnostics({ auth, preferences, login = { phase: "idle" }, network, modelCatalog }) {
2346
2686
  let account = { status: "unknown" };
2347
2687
  const issues = [];
2348
2688
  try {
@@ -2351,6 +2691,7 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
2351
2691
  issues.push({ code: "account-status-unavailable" });
2352
2692
  }
2353
2693
  const preference = preferences.status();
2694
+ const catalog = modelCatalog?.status?.();
2354
2695
  return {
2355
2696
  schemaVersion: 3,
2356
2697
  package: "dsh-codex-subscription",
@@ -2363,6 +2704,15 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
2363
2704
  account,
2364
2705
  login,
2365
2706
  requests: safeRequests(network),
2707
+ ...catalog && ["fallback", "online"].includes(catalog.source) && [
2708
+ "idle",
2709
+ "refreshing",
2710
+ "ok",
2711
+ "failed"
2712
+ ].includes(catalog.refresh) ? { catalog: {
2713
+ source: catalog.source,
2714
+ refresh: catalog.refresh
2715
+ } } : {},
2366
2716
  configuration: {
2367
2717
  contextMode: preference.contextMode,
2368
2718
  quickQuotaMode: preference.quickQuotaMode,
@@ -2601,12 +2951,74 @@ function createCodexUsageReader(options) {
2601
2951
  });
2602
2952
  }
2603
2953
  //#endregion
2954
+ //#region src/quota-rate-interval.js
2955
+ function quotaRateInterval(samples, { quantum = 1, rounding = "unknown" } = {}) {
2956
+ if (!(quantum > 0) || !Number.isFinite(quantum)) throw new RangeError("Invalid quantum");
2957
+ const points = samples.map((sample) => {
2958
+ const used = 100 - sample.remainingPercent;
2959
+ const lower = rounding === "floor" ? used : used - quantum / (rounding === "nearest" ? 2 : 1);
2960
+ const upper = rounding === "floor" ? used + quantum : used + quantum / (rounding === "nearest" ? 2 : 1);
2961
+ return {
2962
+ at: sample.at,
2963
+ lower: Math.max(0, lower),
2964
+ upper: Math.min(100, upper)
2965
+ };
2966
+ });
2967
+ let min = 0, max = Infinity;
2968
+ for (let i = 0; i < points.length; i++) for (let j = i + 1; j < points.length; j++) {
2969
+ const minutes = (points[j].at - points[i].at) / 6e4;
2970
+ if (minutes <= 0) continue;
2971
+ min = Math.max(min, (points[j].lower - points[i].upper) / minutes);
2972
+ max = Math.min(max, (points[j].upper - points[i].lower) / minutes);
2973
+ }
2974
+ return {
2975
+ min,
2976
+ max,
2977
+ feasible: min <= max + 1e-10
2978
+ };
2979
+ }
2980
+ function quotaSharedOffsetInterval(samples, { quantum = 1, maxLagMs = 12e4 } = {}) {
2981
+ if (!(quantum > 0) || !Number.isFinite(quantum) || !Number.isFinite(maxLagMs) || maxLagMs < 0) throw new RangeError("Invalid interval options");
2982
+ let min = 0, max = Infinity;
2983
+ for (let i = 0; i < samples.length; i++) for (let j = i + 1; j < samples.length; j++) {
2984
+ const elapsed = (samples[j].at - samples[i].at) / 6e4, lag = maxLagMs / 6e4;
2985
+ if (elapsed <= 0) continue;
2986
+ const delta = samples[i].remainingPercent - samples[j].remainingPercent;
2987
+ min = Math.max(min, (delta - quantum) / (elapsed + lag));
2988
+ if (elapsed > lag) max = Math.min(max, (delta + quantum) / (elapsed - lag));
2989
+ }
2990
+ return {
2991
+ min,
2992
+ max,
2993
+ feasible: min <= max + 1e-10
2994
+ };
2995
+ }
2996
+ function refineQuotaRate(samples, conservative) {
2997
+ if (!conservative.feasible || samples.length < 4 || samples.at(-1).at - samples[0].at < 20 * 6e4) return conservative;
2998
+ let crossings = 0;
2999
+ for (let i = 0; i < samples.length; i++) {
3000
+ const value = samples[i].remainingPercent;
3001
+ if (!Number.isInteger(value) || value <= 0 || value >= 100) return conservative;
3002
+ if (i > 0) {
3003
+ if (value > samples[i - 1].remainingPercent || samples[i].at <= samples[i - 1].at) return conservative;
3004
+ if (value < samples[i - 1].remainingPercent) crossings++;
3005
+ }
3006
+ }
3007
+ if (crossings < 3) return conservative;
3008
+ const candidate = quotaSharedOffsetInterval(samples);
3009
+ if (!candidate.feasible || candidate.min <= 0) return conservative;
3010
+ if (candidate.max - candidate.min >= conservative.max - conservative.min) return conservative;
3011
+ return {
3012
+ ...candidate,
3013
+ method: "shared-offset",
3014
+ maxLagMs: 12e4
3015
+ };
3016
+ }
3017
+ //#endregion
2604
3018
  //#region src/quota-forecast.js
2605
3019
  const HOUR_MS = 3600 * 1e3;
2606
3020
  const HISTORY_MS = 24 * HOUR_MS;
2607
- const MIN_SAMPLES = 3;
2608
- const PLATEAU_SAMPLE_MS = 900 * 1e3;
2609
- const finite = (value) => Number.isFinite(Number(value));
3021
+ const finite = (value) => value !== null && value !== void 0 && Number.isFinite(Number(value));
2610
3022
  const clampPercent = (value) => Math.max(0, Math.min(100, Number(value)));
2611
3023
  const cleanSegment = (value) => String(value ?? "default").slice(0, 96);
2612
3024
  const keyFor = (window, context = {}) => JSON.stringify([
@@ -2614,17 +3026,6 @@ const keyFor = (window, context = {}) => JSON.stringify([
2614
3026
  cleanSegment(context.limitId ?? "codex"),
2615
3027
  Number(window.windowSeconds) || "limit"
2616
3028
  ]);
2617
- const median = (values) => {
2618
- const ordered = [...values].sort((a, b) => a - b);
2619
- const middle = Math.floor(ordered.length / 2);
2620
- return ordered.length % 2 === 0 ? (ordered[middle - 1] + ordered[middle]) / 2 : ordered[middle];
2621
- };
2622
- function requiredSpanMs(consumedPercent) {
2623
- if (consumedPercent >= 2) return 300 * 1e3;
2624
- if (consumedPercent >= 1) return 600 * 1e3;
2625
- if (consumedPercent >= .5) return 1200 * 1e3;
2626
- return 1800 * 1e3;
2627
- }
2628
3029
  function observeQuotaForecast(state, windows, now = Date.now(), context = {}) {
2629
3030
  const next = { windows: { ...state?.windows ?? {} } };
2630
3031
  let changed = false;
@@ -2632,12 +3033,13 @@ function observeQuotaForecast(state, windows, now = Date.now(), context = {}) {
2632
3033
  if (!finite(window?.remainingPercent)) continue;
2633
3034
  const key = keyFor(window, context);
2634
3035
  const resetsAt = finite(window.resetsAt) ? Number(window.resetsAt) : null;
2635
- const remainingPercent = Math.round(clampPercent(window.remainingPercent) * 1e4) / 1e4;
3036
+ const remainingPercent = clampPercent(window.remainingPercent);
2636
3037
  const previous = next.windows[key];
2637
3038
  const resetChanged = previous !== void 0 && (previous.resetsAt === null !== (resetsAt === null) || previous.resetsAt !== null && Math.abs(previous.resetsAt - resetsAt) > 300);
2638
3039
  const last = previous?.samples?.at(-1);
2639
3040
  const quotaIncreased = last !== void 0 && remainingPercent > last.remainingPercent + .5;
2640
- const record = resetChanged || quotaIncreased ? {
3041
+ const observationGap = last !== void 0 && now - last.at > 90 * 6e4;
3042
+ const record = resetChanged || quotaIncreased || observationGap ? {
2641
3043
  resetsAt,
2642
3044
  samples: []
2643
3045
  } : {
@@ -2645,7 +3047,7 @@ function observeQuotaForecast(state, windows, now = Date.now(), context = {}) {
2645
3047
  samples: [...previous?.samples ?? []]
2646
3048
  };
2647
3049
  const latest = record.samples.at(-1);
2648
- if (latest === void 0 || now > latest.at && (Math.abs(remainingPercent - latest.remainingPercent) >= .001 || now - latest.at >= PLATEAU_SAMPLE_MS)) {
3050
+ if (latest === void 0 || now > latest.at) {
2649
3051
  record.samples.push({
2650
3052
  at: now,
2651
3053
  remainingPercent
@@ -2666,59 +3068,98 @@ function estimateQuotaForecast(state, window, now = Date.now(), context = {}) {
2666
3068
  if (record === void 0) return { status: "calibrating" };
2667
3069
  const resetsAt = finite(window.resetsAt) ? Number(window.resetsAt) : null;
2668
3070
  if (record.resetsAt === null !== (resetsAt === null) || resetsAt !== null && Math.abs(record.resetsAt - resetsAt) > 300) return { status: "calibrating" };
2669
- const samples = record.samples.filter((sample) => sample.at >= now - HISTORY_MS && sample.at <= now + 6e4);
2670
- if (samples.length < MIN_SAMPLES) return {
3071
+ let samples = record.samples.filter((sample) => sample.at >= now - 2 * HOUR_MS && sample.at <= now);
3072
+ if (samples.length < 2) return {
2671
3073
  status: "calibrating",
2672
3074
  sampleCount: samples.length
2673
3075
  };
2674
- const first = samples[0];
2675
- const last = samples.at(-1);
2676
- const spanMs = last.at - first.at;
2677
- const consumedPercent = Math.max(0, first.remainingPercent - last.remainingPercent);
2678
- if (spanMs < requiredSpanMs(consumedPercent)) return {
3076
+ if (now - samples.at(-1).at > 20 * 6e4) return {
2679
3077
  status: "calibrating",
3078
+ reason: "stale"
3079
+ };
3080
+ let bounds = quotaRateInterval(samples);
3081
+ let changedIntensity = false;
3082
+ while (!bounds.feasible && samples.length > 3) {
3083
+ samples = samples.slice(1);
3084
+ bounds = quotaRateInterval(samples);
3085
+ changedIntensity = true;
3086
+ }
3087
+ bounds = refineQuotaRate(samples, bounds);
3088
+ const spanMs = samples.at(-1).at - samples[0].at;
3089
+ const common = {
2680
3090
  sampleCount: samples.length,
2681
3091
  observedSpanMs: spanMs,
2682
- consumedPercent
2683
- };
2684
- const slopes = [];
2685
- for (let left = 0; left < samples.length - 1; left += 1) for (let right = left + 1; right < samples.length; right += 1) {
2686
- const hours = (samples[right].at - samples[left].at) / HOUR_MS;
2687
- if (hours <= 0) continue;
2688
- slopes.push((samples[left].remainingPercent - samples[right].remainingPercent) / hours);
2689
- }
2690
- const positive = slopes.filter((value) => Number.isFinite(value) && value >= 0);
2691
- const pacePerHour = positive.length === 0 ? 0 : median(positive);
2692
- if (!Number.isFinite(pacePerHour) || pacePerHour < .02) return {
2693
- status: "idle",
2694
- pacePerHour: 0,
2695
- sampleCount: samples.length,
2696
- observedSpanMs: spanMs,
2697
- consumedPercent
3092
+ consumedPercent: samples[0].remainingPercent - samples.at(-1).remainingPercent,
3093
+ lowerPacePerHour: bounds.min * 60,
3094
+ upperPacePerHour: bounds.max * 60,
3095
+ changedIntensity,
3096
+ rateMethod: bounds.method ?? "conservative"
3097
+ };
3098
+ if (!bounds.feasible) return {
3099
+ ...common,
3100
+ status: "calibrating",
3101
+ reason: "changing-pace"
3102
+ };
3103
+ if (resetsAt !== null && resetsAt <= now / 1e3) return {
3104
+ ...common,
3105
+ status: "calibrating",
3106
+ reason: "stale"
2698
3107
  };
2699
- const deviations = positive.map((value) => Math.abs(value - pacePerHour));
2700
- const uncertaintyPerHour = deviations.length === 0 ? 0 : median(deviations) * 1.4826;
2701
- const lowerPacePerHour = Math.max(.02, pacePerHour - uncertaintyPerHour);
2702
- const upperPacePerHour = pacePerHour + uncertaintyPerHour;
3108
+ if (spanMs >= 5 * 6e4 && samples.length >= 3 && bounds.min <= 1e-9 && common.consumedPercent >= 1) {
3109
+ const pacePerHour = common.consumedPercent / (spanMs / HOUR_MS);
3110
+ return {
3111
+ ...common,
3112
+ status: "ready",
3113
+ provisional: true,
3114
+ pacePerHour,
3115
+ runwaySeconds: clampPercent(window.remainingPercent) / pacePerHour * 3600,
3116
+ survivesReset: false
3117
+ };
3118
+ }
3119
+ if (spanMs < 6e4 || bounds.min <= 1e-9) return {
3120
+ ...common,
3121
+ status: "calibrating",
3122
+ reason: "resolution"
3123
+ };
3124
+ const pacePerHour = (bounds.min + bounds.max) * 30;
2703
3125
  const remaining = clampPercent(window.remainingPercent);
2704
- const runwaySeconds = remaining / pacePerHour * 3600;
2705
- const runwayMinSeconds = remaining / upperPacePerHour * 3600;
2706
- const runwayMaxSeconds = remaining / lowerPacePerHour * 3600;
2707
- const resetSeconds = resetsAt === null ? null : Math.max(0, resetsAt - now / 1e3);
3126
+ const runwayMinSeconds = Math.max(0, remaining - 1) / common.upperPacePerHour * 3600;
3127
+ const runwayMaxSeconds = Math.min(100, remaining + 1) / common.lowerPacePerHour * 3600;
3128
+ const resetSeconds = resetsAt === null ? null : resetsAt - now / 1e3;
3129
+ if (resetSeconds !== null && resetSeconds <= 0) return {
3130
+ ...common,
3131
+ status: "calibrating",
3132
+ reason: "stale"
3133
+ };
2708
3134
  return {
3135
+ ...common,
2709
3136
  status: "ready",
2710
3137
  pacePerHour,
2711
- uncertaintyPerHour,
2712
- runwaySeconds,
3138
+ runwaySeconds: remaining / pacePerHour * 3600,
2713
3139
  runwayMinSeconds,
2714
3140
  runwayMaxSeconds,
2715
- survivesReset: resetSeconds !== null && runwayMinSeconds >= resetSeconds,
2716
- sampleCount: samples.length,
2717
- observedSpanMs: spanMs,
2718
- consumedPercent
3141
+ survivesReset: resetSeconds !== null && runwayMinSeconds >= resetSeconds
2719
3142
  };
2720
3143
  }
2721
3144
  function forecastUsage(usage, state = { windows: {} }, now = Date.now(), options = {}) {
3145
+ const observedAt = Number.isFinite(usage?.fetchedAt) ? usage.fetchedAt : now;
3146
+ if (observedAt > now || now - observedAt > 5 * 6e4) return {
3147
+ state,
3148
+ changed: false,
3149
+ usage: {
3150
+ ...usage,
3151
+ rateLimits: (usage?.rateLimits ?? []).map((limit) => ({
3152
+ ...limit,
3153
+ windows: limit.windows.map((window) => ({
3154
+ ...window,
3155
+ forecast: {
3156
+ status: "calibrating",
3157
+ reason: "stale"
3158
+ }
3159
+ }))
3160
+ }))
3161
+ }
3162
+ };
2722
3163
  let nextState = state;
2723
3164
  let changed = false;
2724
3165
  const rateLimits = (usage?.rateLimits ?? []).map((limit) => {
@@ -2726,7 +3167,7 @@ function forecastUsage(usage, state = { windows: {} }, now = Date.now(), options
2726
3167
  scope: options.scope,
2727
3168
  limitId: limit.id
2728
3169
  };
2729
- const observed = observeQuotaForecast(nextState, limit.windows, now, context);
3170
+ const observed = observeQuotaForecast(nextState, limit.windows, observedAt, context);
2730
3171
  nextState = observed.state;
2731
3172
  changed ||= observed.changed;
2732
3173
  return {
@@ -2749,40 +3190,67 @@ function forecastUsage(usage, state = { windows: {} }, now = Date.now(), options
2749
3190
  function createQuotaForecastReader({ reader, enabled, now = Date.now, scope = () => "default", stateStore }) {
2750
3191
  let state = { windows: {} };
2751
3192
  let loaded = false;
3193
+ let loading;
3194
+ let generation = 0;
3195
+ let historyGeneration = 0;
3196
+ let persistence = Promise.resolve();
3197
+ const persist = (operation) => {
3198
+ const pending = persistence.then(operation);
3199
+ persistence = pending.catch(() => {});
3200
+ return pending;
3201
+ };
2752
3202
  const load = async () => {
2753
3203
  if (loaded) return;
3204
+ if (loading) return loading;
3205
+ const current = historyGeneration;
3206
+ const pending = Promise.resolve().then(() => stateStore?.load?.()).then((restored) => {
3207
+ if (current !== historyGeneration) return;
3208
+ if (restored?.windows !== null && typeof restored?.windows === "object") state = restored;
3209
+ loaded = true;
3210
+ }).finally(() => {
3211
+ if (loading === pending) loading = void 0;
3212
+ });
3213
+ loading = pending;
3214
+ return pending;
3215
+ };
3216
+ const clearHistory = (clearReader = true) => {
3217
+ generation += 1;
3218
+ historyGeneration += 1;
3219
+ state = { windows: {} };
2754
3220
  loaded = true;
2755
- const restored = await stateStore?.load?.();
2756
- if (restored?.windows !== null && typeof restored?.windows === "object") state = restored;
3221
+ if (clearReader) reader.clear();
3222
+ return persist(() => stateStore?.clear?.());
2757
3223
  };
2758
3224
  return Object.freeze({
2759
3225
  async read(options) {
3226
+ const current = generation;
3227
+ const account = await scope();
2760
3228
  const usage = await reader.read(options);
3229
+ if (current !== generation) return usage;
2761
3230
  await load();
3231
+ const activeAccount = await scope();
3232
+ if (current !== generation || account !== activeAccount) return usage;
2762
3233
  if (!enabled()) {
2763
- state = { windows: {} };
2764
- await stateStore?.clear?.();
3234
+ await clearHistory(false);
2765
3235
  return usage;
2766
3236
  }
2767
- const forecast = forecastUsage(usage, state, now(), { scope: await scope() });
3237
+ const forecast = forecastUsage(usage, state, now(), { scope: account });
2768
3238
  state = forecast.state;
2769
- if (forecast.changed) await stateStore?.save?.(state);
2770
- return forecast.usage;
2771
- },
2772
- async clear() {
2773
- state = { windows: {} };
2774
- loaded = true;
2775
- reader.clear();
2776
- await stateStore?.clear?.();
3239
+ if (forecast.changed) await persist(() => stateStore?.save?.(forecast.state));
3240
+ return current === generation ? forecast.usage : usage;
2777
3241
  },
3242
+ clear: () => clearHistory(),
2778
3243
  clearCache() {
3244
+ generation += 1;
2779
3245
  reader.clear();
2780
3246
  },
2781
3247
  async clearScope(targetScope) {
3248
+ generation += 1;
2782
3249
  await load();
2783
3250
  const prefix = `[${JSON.stringify(cleanSegment(targetScope))},`;
2784
3251
  state = { windows: Object.fromEntries(Object.entries(state.windows).filter(([key]) => !key.startsWith(prefix))) };
2785
- await stateStore?.save?.(state);
3252
+ const snapshot = state;
3253
+ await persist(() => stateStore?.save?.(snapshot));
2786
3254
  }
2787
3255
  });
2788
3256
  }
@@ -3144,28 +3612,7 @@ function createCodexResetCreditService(options) {
3144
3612
  });
3145
3613
  }
3146
3614
  //#endregion
3147
- //#region src/index.js
3148
- const name = "codex-subscription";
3149
- const inject = [
3150
- "llm",
3151
- "credentials",
3152
- "settings",
3153
- "web",
3154
- "loader",
3155
- "tools",
3156
- "attachments"
3157
- ];
3158
- const PROVIDER = "openai-codex";
3159
- const OAUTH_EXPIRY_SKEW_MS = 6e4;
3160
- const CREDENTIAL_REF = dshCredentials.credentialRef("OPENAI_CODEX_SUBSCRIPTION_OAUTH");
3161
- const LEGACY_CREDENTIAL_REF = dshCredentials.credentialRef("WSL043_OPENAI_CODEX_OAUTH");
3162
- const ACCOUNT_VAULT_KEY = typeof dshCredentials.credentialKey === "function" ? dshCredentials.credentialKey("codex-subscription", "accounts") : void 0;
3163
- const CHANNEL = "/codex-subscription";
3164
- const WEB_ENTRY_ID = "web";
3165
- const DSH_SEARCH_PROVIDER_FALLBACK = "deepseek-official";
3166
- const MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
3167
- const REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048;
3168
- const REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
3615
+ //#region src/subscription-rpc.js
3169
3616
  const publicError = (code, message) => ({
3170
3617
  ok: false,
3171
3618
  error: {
@@ -3209,7 +3656,9 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
3209
3656
  ok: true,
3210
3657
  value: {
3211
3658
  contextModels: Array.isArray(value?.contextModels) ? value.contextModels : [],
3212
- verbosityModels: Array.isArray(value?.verbosityModels) ? value.verbosityModels : []
3659
+ verbosityModels: Array.isArray(value?.verbosityModels) ? value.verbosityModels : [],
3660
+ fastModels: Array.isArray(value?.fastModels) ? value.fastModels : [],
3661
+ catalogStatus: value?.catalogStatus
3213
3662
  }
3214
3663
  };
3215
3664
  } catch (error) {
@@ -3219,44 +3668,11 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
3219
3668
  if (endpoint === "preferences/status" || endpoint === "preferences/update") try {
3220
3669
  signal.throwIfAborted();
3221
3670
  if (endpoint === "preferences/update") {
3222
- const patch = {};
3223
- if (Object.hasOwn(payload ?? {}, "quickQuotaMode")) {
3224
- if (![
3225
- "off",
3226
- "percent",
3227
- "bar",
3228
- "forecast"
3229
- ].includes(payload["quickQuotaMode"])) return publicError("internal", "Invalid quick quota preference");
3230
- patch[QUICK_QUOTA_MODE_FIELD] = payload[QUICK_QUOTA_MODE_FIELD];
3231
- }
3232
- if (Object.hasOwn(payload ?? {}, "searchProvider")) {
3233
- if (![
3234
- "auto",
3235
- "dsh",
3236
- "codex"
3237
- ].includes(payload["searchProvider"])) return publicError("internal", "Invalid search provider preference");
3238
- patch[SEARCH_PROVIDER_FIELD] = payload[SEARCH_PROVIDER_FIELD];
3239
- }
3240
- if (Object.hasOwn(payload ?? {}, "speedMode")) {
3241
- if (!["standard", "fast"].includes(payload["speedMode"])) return publicError("internal", "Invalid speed mode preference");
3242
- patch[SPEED_MODE_FIELD] = payload[SPEED_MODE_FIELD];
3243
- }
3244
- if (Object.hasOwn(payload ?? {}, "outputVerbosity")) {
3245
- if (![
3246
- "default",
3247
- "low",
3248
- "medium",
3249
- "high"
3250
- ].includes(payload["outputVerbosity"])) return publicError("internal", "Invalid output verbosity preference");
3251
- patch[OUTPUT_VERBOSITY_FIELD] = payload[OUTPUT_VERBOSITY_FIELD];
3252
- }
3253
- if (Object.hasOwn(payload ?? {}, "contextMode")) {
3254
- if (![
3255
- "standard",
3256
- "extended",
3257
- "custom"
3258
- ].includes(payload["contextMode"])) return publicError("internal", "Invalid context mode preference");
3259
- patch[CONTEXT_MODE_FIELD] = payload[CONTEXT_MODE_FIELD];
3671
+ const patch = capabilityPatch(payload);
3672
+ for (const [field, rule] of Object.entries(PREFERENCE_FIELDS)) {
3673
+ if (!Object.hasOwn(payload ?? {}, field)) continue;
3674
+ if (!rule.choices.includes(payload[field])) return publicError("internal", rule.error);
3675
+ patch[field] = payload[field];
3260
3676
  }
3261
3677
  if (Object.hasOwn(payload ?? {}, "customContextWindow")) {
3262
3678
  if (normalizeCustomContextWindow(payload["customContextWindow"]) !== payload["customContextWindow"]) return publicError("internal", "Invalid custom context window");
@@ -3339,6 +3755,28 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
3339
3755
  return result;
3340
3756
  };
3341
3757
  }
3758
+ //#endregion
3759
+ //#region src/index.js
3760
+ const name = "codex-subscription";
3761
+ const inject = [
3762
+ "llm",
3763
+ "credentials",
3764
+ "settings",
3765
+ "web",
3766
+ "loader",
3767
+ "tools",
3768
+ "attachments"
3769
+ ];
3770
+ const PROVIDER = "openai-codex";
3771
+ const OAUTH_EXPIRY_SKEW_MS = 6e4;
3772
+ const CREDENTIAL_REF = dshCredentials.credentialRef("OPENAI_CODEX_SUBSCRIPTION_OAUTH");
3773
+ const LEGACY_CREDENTIAL_REF = dshCredentials.credentialRef("WSL043_OPENAI_CODEX_OAUTH");
3774
+ const ACCOUNT_VAULT_KEY = typeof dshCredentials.credentialKey === "function" ? dshCredentials.credentialKey("codex-subscription", "accounts") : void 0;
3775
+ const WEB_ENTRY_ID = "web";
3776
+ const DSH_SEARCH_PROVIDER_FALLBACK = "deepseek-official";
3777
+ const MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
3778
+ const REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048;
3779
+ const REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
3342
3780
  function createSearchProviderSwitcher(loader) {
3343
3781
  const webEntry = () => [...loader.entries()].find((entry) => entry.options?.id === WEB_ENTRY_ID);
3344
3782
  const dshProviderId = () => {
@@ -3365,30 +3803,23 @@ function createSearchProviderSwitcher(loader) {
3365
3803
  }
3366
3804
  function apply(ctx) {
3367
3805
  const settings = ctx.settings.register(SETTINGS_NAMESPACE, z.object({
3368
- [QUICK_QUOTA_MODE_FIELD]: z.union([
3369
- "off",
3370
- QUICK_QUOTA_MODE_PERCENT,
3371
- "bar",
3372
- QUICK_QUOTA_MODE_FORECAST
3373
- ]),
3374
- [LEGACY_QUICK_QUOTA_FIELD]: z.boolean(),
3375
- [SEARCH_PROVIDER_FIELD]: z.union([
3376
- SEARCH_PROVIDER_AUTO,
3377
- "dsh",
3378
- SEARCH_PROVIDER_CODEX
3379
- ]).default(DEFAULT_SEARCH_PROVIDER),
3380
- [SPEED_MODE_FIELD]: z.union([SPEED_MODE_STANDARD, SPEED_MODE_FAST]).default(DEFAULT_SPEED_MODE),
3381
- [OUTPUT_VERBOSITY_FIELD]: z.union([
3382
- OUTPUT_VERBOSITY_DEFAULT,
3806
+ ...Object.fromEntries(Object.entries(PREFERENCE_FIELDS).map(([field, rule]) => [field, rule.default === void 0 ? z.union(rule.choices) : z.union(rule.choices).default(rule.default)])),
3807
+ imageModel: z.union(Object.keys(IMAGE_MODELS)).default(DEFAULT_IMAGE_MODEL),
3808
+ imageQuality: z.union([
3809
+ "auto",
3383
3810
  "low",
3384
- OUTPUT_VERBOSITY_MEDIUM,
3385
- OUTPUT_VERBOSITY_HIGH
3386
- ]).default(DEFAULT_OUTPUT_VERBOSITY),
3387
- [CONTEXT_MODE_FIELD]: z.union([
3388
- CONTEXT_MODE_STANDARD,
3389
- CONTEXT_MODE_EXTENDED,
3390
- CONTEXT_MODE_CUSTOM
3391
- ]).default(DEFAULT_CONTEXT_MODE),
3811
+ "medium",
3812
+ "high",
3813
+ "xhigh",
3814
+ "max"
3815
+ ]).default("auto"),
3816
+ ...Object.fromEntries(Object.entries(IMAGE_FEATURE_DEFAULTS).map(([key, value]) => [key, z.boolean().default(value)])),
3817
+ [CUSTOM_CONTEXT_OVERRIDES_FIELD]: z.dict(z.number().step(1).min(1).max(MAX_CONTEXT_BUDGET)).default({}),
3818
+ [SEARCH_MODE_FIELD]: z.union(SEARCH_MODES).default("live"),
3819
+ [SEARCH_DOMAINS_FIELD]: z.transform(z.array(z.string()).max(20), (value) => readCapabilitySettings({ searchDomains: value }).searchDomains).default([]),
3820
+ ...Object.fromEntries(QUOTA_THRESHOLD_FIELDS.map((key) => [key, z.number().step(1).min(1).max(100).default(20)])),
3821
+ [QUOTA_ALERTS_FIELD]: z.union(QUOTA_ALERT_MODES).default("important"),
3822
+ [LEGACY_QUICK_QUOTA_FIELD]: z.boolean(),
3392
3823
  [CUSTOM_CONTEXT_WINDOW_FIELD]: z.number().step(1).min(128e3).max(1e6).default(DEFAULT_CUSTOM_CONTEXT_WINDOW),
3393
3824
  ...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])]))
3394
3825
  }));
@@ -3417,7 +3848,10 @@ function apply(ctx) {
3417
3848
  resolveOutputVerbosity: () => normalizeOutputVerbosity(settings.get()[OUTPUT_VERBOSITY_FIELD]),
3418
3849
  resolveContextMode: () => normalizeContextMode(settings.get()[CONTEXT_MODE_FIELD]),
3419
3850
  resolveCustomContextWindow: (modelKey) => {
3851
+ const overrides = readCapabilitySettings(settings.get())[CUSTOM_CONTEXT_OVERRIDES_FIELD];
3852
+ if (Object.hasOwn(overrides, modelKey)) return overrides[modelKey];
3420
3853
  const field = CUSTOM_CONTEXT_MODEL_FIELDS[modelKey];
3854
+ if (field === void 0) return void 0;
3421
3855
  return normalizeCustomContextWindow(settings.get()[field] ?? CUSTOM_CONTEXT_MODEL_DEFAULTS[modelKey], CUSTOM_CONTEXT_MODEL_CAPS[modelKey]);
3422
3856
  },
3423
3857
  catalog: modelCatalog,
@@ -3425,6 +3859,7 @@ function apply(ctx) {
3425
3859
  });
3426
3860
  const preferences = {
3427
3861
  status: () => ({
3862
+ ...readCapabilitySettings(settings.get()),
3428
3863
  [QUICK_QUOTA_MODE_FIELD]: normalizeQuickQuotaMode(settings.get()[QUICK_QUOTA_MODE_FIELD], settings.get()[LEGACY_QUICK_QUOTA_FIELD]),
3429
3864
  [SEARCH_PROVIDER_FIELD]: settings.get()[SEARCH_PROVIDER_FIELD],
3430
3865
  [SPEED_MODE_FIELD]: settings.get()[SPEED_MODE_FIELD],
@@ -3432,8 +3867,10 @@ function apply(ctx) {
3432
3867
  [CONTEXT_MODE_FIELD]: normalizeContextMode(settings.get()[CONTEXT_MODE_FIELD]),
3433
3868
  [CUSTOM_CONTEXT_WINDOW_FIELD]: normalizeCustomContextWindow(settings.get()[CUSTOM_CONTEXT_WINDOW_FIELD]),
3434
3869
  ...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])])),
3435
- contextModels: contextModelGroups(provider.getModels()),
3870
+ contextModels: contextModelGroups(modelCatalog.getModels(baseProvider.getModels())),
3871
+ catalogStatus: modelCatalog.status(),
3436
3872
  verbosityModels: provider.getModels().filter((model) => modelCatalog.metadata(model.id)?.supportVerbosity ?? model.id !== "gpt-5.3-codex-spark").map((model) => model.id),
3873
+ fastModels: provider.getModels().filter((model) => modelCatalog.metadata(model.id)?.supportsFast ?? supportsCodexFastMode(model.id)).map((model) => model.id),
3437
3874
  writable: ctx.settings.writable
3438
3875
  }),
3439
3876
  update: (patch) => settings.update(patch)
@@ -3455,11 +3892,12 @@ function apply(ctx) {
3455
3892
  let profileKey;
3456
3893
  let profileSnapshot;
3457
3894
  const profiles = () => {
3458
- const key = [
3895
+ const key = JSON.stringify([
3459
3896
  modelCatalog.revision(),
3460
3897
  normalizeContextMode(settings.get()[CONTEXT_MODE_FIELD]),
3898
+ settings.get()[CUSTOM_CONTEXT_OVERRIDES_FIELD],
3461
3899
  ...Object.values(CUSTOM_CONTEXT_MODEL_FIELDS).map((field) => settings.get()[field])
3462
- ].join(":");
3900
+ ]);
3463
3901
  if (key !== profileKey) {
3464
3902
  profileKey = key;
3465
3903
  profileSnapshot = /* @__PURE__ */ new Map([[PROVIDER, profile]]);
@@ -3474,14 +3912,15 @@ function apply(ctx) {
3474
3912
  fileExists: async () => false
3475
3913
  })
3476
3914
  });
3477
- ctx.tools.register(createCodexImageTool({
3915
+ ctx.effect(() => watchImageTool(settings, () => ctx.tools.register(createCodexImageTool({
3916
+ getFeatures: () => settings.get(),
3478
3917
  getAuth: resolveAuth,
3479
3918
  readCredential: (options) => store.read(PROVIDER, options),
3480
3919
  attachments: ctx.attachments,
3481
3920
  getSessionMessages: (sessionId) => ctx.get?.("sessions")?.get?.(sessionId)?.deriveMessages?.() ?? [],
3482
3921
  originalImages,
3483
3922
  fetch: (input, init) => network.fetch("image", input, init)
3484
- }));
3923
+ }))), "codex-subscription: image tool availability");
3485
3924
  const adapter = new PiAiAdapter({
3486
3925
  profiles,
3487
3926
  resolveApiKey: async () => {
@@ -3500,6 +3939,7 @@ function apply(ctx) {
3500
3939
  ctx.llm.registerAdapter([PROVIDER], adapter);
3501
3940
  const currentAgent = () => ctx.get?.("agents")?.currentInitiator?.();
3502
3941
  const codexSearch = createCodexSearchProvider({
3942
+ resolvePreferences: () => readCapabilitySettings(settings.get()),
3503
3943
  getAuth: resolveAuth,
3504
3944
  readCredential: (options) => store.read(PROVIDER, options),
3505
3945
  resolveModel: () => {
@@ -3547,12 +3987,22 @@ function apply(ctx) {
3547
3987
  stateStore: new QuotaForecastStateStore({ filename: dshHomePath("state", "codex-subscription", "quota-forecast.json") })
3548
3988
  });
3549
3989
  ctx.effect(() => {
3990
+ let forecasting = false;
3550
3991
  const warmForecast = (value) => {
3551
- if (normalizeQuickQuotaMode(value["quickQuotaMode"], value["quickQuotaVisible"]) !== "forecast") return;
3992
+ if (!(normalizeQuickQuotaMode(value["quickQuotaMode"], value["quickQuotaVisible"]) === "forecast")) {
3993
+ if (forecasting) usageReader.clear().catch((error) => ctx.logger?.debug?.("could not clear Codex quota forecast: %s", error.message));
3994
+ forecasting = false;
3995
+ return;
3996
+ }
3997
+ forecasting = true;
3552
3998
  usageReader.read().catch((error) => ctx.logger?.debug?.("could not warm Codex quota forecast: %s", error.message));
3553
3999
  };
3554
4000
  warmForecast(settings.get());
3555
- return settings.watch(warmForecast);
4001
+ const unwatch = settings.watch(warmForecast);
4002
+ return () => {
4003
+ unwatch();
4004
+ usageReader.clearCache();
4005
+ };
3556
4006
  }, "codex-subscription: quota forecast warm-up");
3557
4007
  const resetCreditService = createCodexResetCreditService({
3558
4008
  getAuth: resolveAuth,
@@ -3569,7 +4019,8 @@ function apply(ctx) {
3569
4019
  auth,
3570
4020
  preferences,
3571
4021
  login: coordinator.supportState(),
3572
- network
4022
+ network,
4023
+ modelCatalog
3573
4024
  }),
3574
4025
  modelCatalog,
3575
4026
  originalImages,
@@ -3578,7 +4029,7 @@ function apply(ctx) {
3578
4029
  ctx.effect(() => {
3579
4030
  modelCatalog.refresh().catch((error) => ctx.logger?.debug?.("could not refresh Codex model catalog: %s", error.message));
3580
4031
  }, "codex-subscription: official model catalog");
3581
- ctx.inject(["connection"], (connectionContext) => connectionContext.effect(() => connectionContext.connection.rpc.handle(CHANNEL, handler, { authority: "trusted-host" }), "codex-subscription: DSH-trusted account RPC"));
4032
+ ctx.inject(["connection"], (connectionContext) => connectionContext.effect(() => registerSubscriptionTransport(connectionContext.connection, handler), "codex-subscription: DSH-trusted account RPC"));
3582
4033
  }
3583
4034
  //#endregion
3584
4035
  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 };