@eddyskywalker/dsh-chatgpt-subscription 0.1.10 → 0.1.12

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +9 -50
  2. package/README.md +17 -7
  3. package/lib/client.js +442 -28
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +718 -247
  6. package/lib/types/client/CodexComposerQuota.d.ts +13 -0
  7. package/lib/types/client/CodexComposerQuota.d.ts.map +1 -0
  8. package/lib/types/client/CodexImageToolView.d.ts +10 -0
  9. package/lib/types/client/CodexImageToolView.d.ts.map +1 -0
  10. package/lib/types/client/CodexSubscriptionSection.d.ts.map +1 -1
  11. package/lib/types/client/api.d.ts +2 -1
  12. package/lib/types/client/api.d.ts.map +1 -1
  13. package/lib/types/client/index.d.ts.map +1 -1
  14. package/lib/types/client/locales.d.ts +168 -114
  15. package/lib/types/client/locales.d.ts.map +1 -1
  16. package/lib/types/client/quota.d.ts +9 -0
  17. package/lib/types/client/quota.d.ts.map +1 -0
  18. package/lib/types/client/styles.d.ts.map +1 -1
  19. package/lib/types/compat.d.ts +7 -0
  20. package/lib/types/compat.d.ts.map +1 -1
  21. package/lib/types/host/codex-images.d.ts +9 -0
  22. package/lib/types/host/codex-images.d.ts.map +1 -0
  23. package/lib/types/host/codex-search.d.ts +11 -0
  24. package/lib/types/host/codex-search.d.ts.map +1 -0
  25. package/lib/types/host/model-catalog.d.ts.map +1 -1
  26. package/lib/types/host/preferences.d.ts +12 -0
  27. package/lib/types/host/preferences.d.ts.map +1 -0
  28. package/lib/types/host/responses-mapper.d.ts.map +1 -1
  29. package/lib/types/host/routes.d.ts +2 -1
  30. package/lib/types/host/routes.d.ts.map +1 -1
  31. package/lib/types/host/search-provider-switcher.d.ts +15 -0
  32. package/lib/types/host/search-provider-switcher.d.ts.map +1 -0
  33. package/lib/types/host/usage-service.d.ts +2 -1
  34. package/lib/types/host/usage-service.d.ts.map +1 -1
  35. package/lib/types/index.d.ts +3 -1
  36. package/lib/types/index.d.ts.map +1 -1
  37. package/lib/types/shared/contracts.d.ts +40 -3
  38. package/lib/types/shared/contracts.d.ts.map +1 -1
  39. package/lib/types/shared/model-catalog.d.ts +81 -0
  40. package/lib/types/shared/model-catalog.d.ts.map +1 -0
  41. package/lib/types/shared/preferences.d.ts +7 -0
  42. package/lib/types/shared/preferences.d.ts.map +1 -0
  43. package/package.json +126 -107
  44. package/lib/types/host/subagent-report-scheduling-compat.d.ts +0 -21
  45. package/lib/types/host/subagent-report-scheduling-compat.d.ts.map +0 -1
package/lib/index.js CHANGED
@@ -1,23 +1,125 @@
1
- import { CallId, LlmAdapter, LlmError, ProviderRequestId, ReasoningEffortId, attributionHeaders, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
1
+ import { CallId, HarnessError, LlmAdapter, LlmError, ProviderRequestId, ReasoningEffortId, attributionHeaders, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
2
+ import { createUserMessage } from "@deepseek-ai/dsh-llm/message";
3
+ import { defineTool } from "@deepseek-ai/dsh-tools";
2
4
  import { createHash, randomBytes, randomUUID } from "node:crypto";
5
+ import { WebError } from "@deepseek-ai/dsh-web";
3
6
  import http from "node:http";
7
+ import { settingsNamespace } from "@deepseek-ai/dsh-settings";
8
+ import z from "@deepseek-ai/schemastery";
4
9
  import { constants } from "node:fs";
5
10
  import { chmod, lstat, mkdir, open, rename, stat, unlink } from "node:fs/promises";
6
11
  import { homedir } from "node:os";
7
12
  import { dirname, join } from "node:path";
8
13
  import { spawn } from "node:child_process";
14
+ //#region src/compat.ts
15
+ /**
16
+ * Compatibility constants for the ChatGPT-backed Codex flow. The backend and
17
+ * OAuth parameters are not a public third-party API contract, so every such
18
+ * value is isolated here for review and rollback.
19
+ */
20
+ const CHATGPT_OAUTH_ISSUER = "https://auth.openai.com";
21
+ const CHATGPT_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
22
+ const OAUTH_CALLBACK_HOST = "localhost";
23
+ const OAUTH_CALLBACK_PORT = 1455;
24
+ const OAUTH_REDIRECT_URI = `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}/auth/callback`;
25
+ const OAUTH_SCOPE = "openid profile email offline_access";
26
+ const OAUTH_ORIGINATOR = "opencode";
27
+ const ROUTE_PREFIX = "/api/dsh-chatgpt-subscription";
28
+ const PLUGIN_VERSION = "0.1.0-alpha.0";
29
+ const CODEX_CHATGPT_PROVIDER_ID = "codex-chatgpt";
30
+ const CODEX_API_BASE = "https://chatgpt.com/backend-api/codex";
31
+ const CODEX_RESPONSES_URL = `${CODEX_API_BASE}/responses`;
32
+ const CODEX_IMAGE_GENERATION_URL = `${CODEX_API_BASE}/images/generations`;
33
+ const CODEX_SEARCH_URL = `${CODEX_API_BASE}/alpha/search`;
34
+ const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
35
+ const CODEX_ORIGINATOR = "opencode";
36
+ const CODEX_IMAGE_TOOL_NAME = "codex_image_generate";
37
+ const CODEX_IMAGE_MODEL = "gpt-image-2";
38
+ const CODEX_SEARCH_PROVIDER_ID = "codex-subscription";
39
+ const QUOTA_MIN_UPSTREAM_INTERVAL_MS = 15e3;
40
+ const OAUTH_AUTHORIZE_URL = `${CHATGPT_OAUTH_ISSUER}/oauth/authorize`;
41
+ const OAUTH_TOKEN_URL = `${CHATGPT_OAUTH_ISSUER}/oauth/token`;
42
+ //#endregion
43
+ //#region src/shared/model-catalog.ts
44
+ const CODEX_MODEL_CATALOG = [
45
+ {
46
+ id: "gpt-5.6-sol",
47
+ name: "5.6 Sol",
48
+ contextWindow: 272e3,
49
+ inputModalities: ["text", "image"],
50
+ defaultReasoningEffort: "medium",
51
+ reasoningProfile: "gpt-5.6",
52
+ supportsReasoningSummary: true
53
+ },
54
+ {
55
+ id: "gpt-5.6-terra",
56
+ name: "5.6 Terra",
57
+ contextWindow: 272e3,
58
+ inputModalities: ["text", "image"],
59
+ defaultReasoningEffort: "medium",
60
+ reasoningProfile: "gpt-5.6",
61
+ supportsReasoningSummary: true
62
+ },
63
+ {
64
+ id: "gpt-5.6-luna",
65
+ name: "5.6 Luna",
66
+ contextWindow: 272e3,
67
+ inputModalities: ["text", "image"],
68
+ defaultReasoningEffort: "medium",
69
+ reasoningProfile: "gpt-5.6",
70
+ supportsReasoningSummary: true
71
+ },
72
+ {
73
+ id: "gpt-5.5",
74
+ name: "5.5",
75
+ contextWindow: 272e3,
76
+ inputModalities: ["text", "image"],
77
+ defaultReasoningEffort: "medium",
78
+ reasoningProfile: "standard",
79
+ supportsReasoningSummary: true
80
+ },
81
+ {
82
+ id: "gpt-5.4",
83
+ name: "5.4",
84
+ contextWindow: 272e3,
85
+ inputModalities: ["text", "image"],
86
+ defaultReasoningEffort: "none",
87
+ reasoningProfile: "standard",
88
+ supportsReasoningSummary: true
89
+ },
90
+ {
91
+ id: "gpt-5.4-mini",
92
+ name: "5.4 Mini",
93
+ contextWindow: 272e3,
94
+ inputModalities: ["text", "image"],
95
+ defaultReasoningEffort: "none",
96
+ reasoningProfile: "standard",
97
+ supportsReasoningSummary: true
98
+ },
99
+ {
100
+ id: "gpt-5.3-codex-spark",
101
+ name: "5.3 Codex Spark",
102
+ contextWindow: 258e3,
103
+ inputModalities: ["text"],
104
+ defaultReasoningEffort: "high",
105
+ reasoningProfile: "standard",
106
+ supportsReasoningSummary: false
107
+ }
108
+ ];
109
+ const DEFAULT_CODEX_MODEL = CODEX_MODEL_CATALOG[0];
110
+ function resolveCodexCatalogEntry(model) {
111
+ return CODEX_MODEL_CATALOG.find((entry) => entry.id === model) ?? DEFAULT_CODEX_MODEL;
112
+ }
113
+ function codexModelSupportsImageInput(model) {
114
+ return resolveCodexCatalogEntry(model).inputModalities.includes("image");
115
+ }
116
+ function codexModelSupportsReasoningSummary(model) {
117
+ return resolveCodexCatalogEntry(model).supportsReasoningSummary;
118
+ }
119
+ //#endregion
9
120
  //#region src/host/model-catalog.ts
10
- const PROVIDER_ID = "codex-chatgpt";
121
+ const PROVIDER_ID = CODEX_CHATGPT_PROVIDER_ID;
11
122
  const PROVIDER_NAME = "Codex(ChatGPT 订阅)";
12
- const MODEL_IDS = [
13
- "gpt-5.6-sol",
14
- "gpt-5.6-terra",
15
- "gpt-5.6-luna",
16
- "gpt-5.5",
17
- "gpt-5.4",
18
- "gpt-5.4-mini",
19
- "gpt-5.2"
20
- ];
21
123
  const STANDARD_REASONING_EFFORTS = [
22
124
  "none",
23
125
  "low",
@@ -25,60 +127,34 @@ const STANDARD_REASONING_EFFORTS = [
25
127
  "high",
26
128
  "xhigh"
27
129
  ];
28
- const GPT_56_REASONING_EFFORTS = [...STANDARD_REASONING_EFFORTS, "max"];
29
130
  const MODEL_REASONING = {
30
- "gpt-5.6-sol": {
31
- efforts: GPT_56_REASONING_EFFORTS,
32
- defaultEffort: "medium"
33
- },
34
- "gpt-5.6-terra": {
35
- efforts: GPT_56_REASONING_EFFORTS,
36
- defaultEffort: "medium"
37
- },
38
- "gpt-5.6-luna": {
39
- efforts: GPT_56_REASONING_EFFORTS,
40
- defaultEffort: "medium"
41
- },
42
- "gpt-5.5": {
43
- efforts: STANDARD_REASONING_EFFORTS,
44
- defaultEffort: "medium"
45
- },
46
- "gpt-5.4": {
47
- efforts: STANDARD_REASONING_EFFORTS,
48
- defaultEffort: "none"
49
- },
50
- "gpt-5.4-mini": {
51
- efforts: STANDARD_REASONING_EFFORTS,
52
- defaultEffort: "none"
53
- },
54
- "gpt-5.2": {
55
- efforts: STANDARD_REASONING_EFFORTS,
56
- defaultEffort: "none"
57
- }
131
+ standard: STANDARD_REASONING_EFFORTS,
132
+ "gpt-5.6": [...STANDARD_REASONING_EFFORTS, "max"]
58
133
  };
59
134
  function listCodexModels() {
60
- return MODEL_IDS.map((id) => ({
135
+ return CODEX_MODEL_CATALOG.map((entry) => ({
61
136
  provider: PROVIDER_ID,
62
- id,
63
- name: id,
64
- inputModalities: ["text", "image"]
137
+ id: entry.id,
138
+ name: entry.name,
139
+ inputModalities: [...entry.inputModalities]
65
140
  }));
66
141
  }
67
142
  function resolveCodexModel(model) {
68
- const reasoning = MODEL_REASONING[model] ?? MODEL_REASONING["gpt-5.6-sol"];
143
+ const entry = resolveCodexCatalogEntry(model);
144
+ const efforts = MODEL_REASONING[entry.reasoningProfile];
69
145
  return {
70
146
  provider: PROVIDER_ID,
71
147
  id: model,
72
- name: model,
73
- inputModalities: ["text", "image"],
74
- context: { contextWindow: 272e3 },
148
+ name: entry.id === model ? entry.name : model,
149
+ inputModalities: [...entry.inputModalities],
150
+ context: { contextWindow: entry.contextWindow },
75
151
  defaultMaxTokens: 32768,
76
152
  reasoning: {
77
- efforts: reasoning.efforts.map((effort) => ({
153
+ efforts: efforts.map((effort) => ({
78
154
  id: ReasoningEffortId(effort),
79
155
  name: effort
80
156
  })),
81
- defaultEffort: ReasoningEffortId(reasoning.defaultEffort)
157
+ defaultEffort: ReasoningEffortId(entry.defaultReasoningEffort)
82
158
  }
83
159
  };
84
160
  }
@@ -124,162 +200,365 @@ var CodexChatGptAdapter = class extends LlmAdapter {
124
200
  }
125
201
  };
126
202
  //#endregion
127
- //#region src/host/subagent-report-scheduling-compat.ts
128
- /**
129
- * DSH_COMPAT_REMOVE(subagent-report-settlement-dedup)
130
- *
131
- * Temporary compatibility shim for DSH 0.1.0-rc.6. A continuable child is told
132
- * to report its result before finishing, while DSH also unconditionally sends
133
- * the same closing output in a `subagent-settled` notice. The report is often
134
- * still queued when the settlement reaches the parent, so the parent sees the
135
- * result once and the equivalent report remains as duplicate next-turn work.
136
- *
137
- * Remove this module, its installation in `src/index.ts`, and its focused test
138
- * once upstream coalesces an equivalent final report with settlement delivery.
139
- */
140
- const DSH_SUBAGENT_REPORT_DEDUP_COMPAT_MARKER = "__dshChatgptSubscriptionSubagentReportDedupCompatV1";
141
- function sourceOf(message) {
142
- return message.source;
143
- }
144
- function isTextBlock(value, text) {
145
- return typeof value === "object" && value !== null && value.type === "text" && value.text === text;
146
- }
147
- function sameValue(left, right) {
148
- if (left === right) return true;
149
- if (Array.isArray(left) || Array.isArray(right)) return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => sameValue(value, right[index]));
150
- if (typeof left !== "object" || left === null || typeof right !== "object" || right === null) return false;
151
- const leftRecord = left;
152
- const rightRecord = right;
153
- const leftKeys = Object.keys(leftRecord).sort();
154
- const rightKeys = Object.keys(rightRecord).sort();
155
- return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => key === rightKeys[index] && sameValue(leftRecord[key], rightRecord[key]));
156
- }
157
- function duplicatePendingReports(agent, settlement) {
158
- const settlementSource = sourceOf(settlement);
159
- if (settlementSource.kind !== "subagent-settled" || settlementSource.senderSessionId === void 0) return [];
160
- if (settlement.content.length < 2 || !isTextBlock(settlement.content[1], "Its closing message:")) return [];
161
- const closingContent = settlement.content.slice(2);
162
- return [...agent.inbox.nextStep, ...agent.inbox.nextTurn].filter((pending) => {
163
- const pendingSource = sourceOf(pending);
164
- return pendingSource.kind === "subagent-report" && pendingSource.senderSessionId === settlementSource.senderSessionId && sameValue(pending.content.slice(1), closingContent);
165
- });
203
+ //#region src/host/wire-auth.ts
204
+ function codexHeaders(credentials, sessionId) {
205
+ const dshAgent = attributionHeaders()["user-agent"] ?? "dsh/unknown";
206
+ return {
207
+ authorization: `Bearer ${credentials.accessToken}`,
208
+ ...credentials.accountId ? { "chatgpt-account-id": credentials.accountId } : {},
209
+ originator: CODEX_ORIGINATOR,
210
+ "user-agent": `dsh-chatgpt-subscription/${PLUGIN_VERSION} (${dshAgent})`,
211
+ ...sessionId ? { "session-id": sessionId } : {}
212
+ };
166
213
  }
167
- function errorMessage(error) {
168
- return error instanceof Error ? error.message : String(error);
214
+ function stableSessionId(value) {
215
+ const source = value === void 0 || value === "" ? randomUUID() : value;
216
+ return `dsh-${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
169
217
  }
170
- /**
171
- * Discard only an exact, same-child report duplicate immediately before DSH
172
- * delivers the corresponding settlement notice. Partial reports, reports with
173
- * different content, and all unrelated inbox work remain untouched.
174
- */
175
- function installSubagentReportDedupCompat(ctx) {
176
- const patches = /* @__PURE__ */ new Map();
177
- const patch = (agent) => {
178
- if (patches.has(agent)) return;
179
- const shared = agent.followup[DSH_SUBAGENT_REPORT_DEDUP_COMPAT_MARKER];
180
- if (shared?.wrappers.followup === agent.followup && shared.wrappers.steer === agent.steer && shared.wrappers.inject === agent.inject) {
181
- shared.owners += 1;
182
- patches.set(agent, shared);
183
- return;
184
- }
185
- const originals = {
186
- followup: agent.followup,
187
- steer: agent.steer,
188
- inject: agent.inject
189
- };
190
- let record;
191
- const deliver = (name, message) => {
192
- for (const duplicate of duplicatePendingReports(agent, message)) try {
193
- agent.inbox.remove(duplicate.id);
194
- } catch (error) {
195
- ctx.logger.warn("[dsh-chatgpt-subscription] Could not discard a duplicate DSH subagent report: " + errorMessage(error));
196
- }
197
- originals[name].call(agent, message);
198
- };
199
- const wrappers = {
200
- followup(message) {
201
- deliver("followup", message);
202
- },
203
- steer(message) {
204
- deliver("steer", message);
218
+ function retryAfterMs(headers) {
219
+ const raw = headers.get("retry-after");
220
+ if (raw === null) return void 0;
221
+ const seconds = Number(raw);
222
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1e3, 10 * 6e4);
223
+ const timestamp = Date.parse(raw);
224
+ if (!Number.isFinite(timestamp)) return void 0;
225
+ return Math.min(Math.max(0, timestamp - Date.now()), 10 * 6e4);
226
+ }
227
+ //#endregion
228
+ //#region src/host/codex-images.ts
229
+ const PNG_SIGNATURE = [
230
+ 137,
231
+ 80,
232
+ 78,
233
+ 71,
234
+ 13,
235
+ 10,
236
+ 26,
237
+ 10
238
+ ];
239
+ function createCodexImageTool(oauth, attachments, options = {}) {
240
+ const fetchFn = options.fetchFn ?? fetch;
241
+ return defineTool({
242
+ name: CODEX_IMAGE_TOOL_NAME,
243
+ description: "Generate a PNG image using the signed-in ChatGPT subscription-backed Codex image endpoint.",
244
+ parameters: { prompt: {
245
+ type: "string",
246
+ required: true,
247
+ description: "A detailed image generation prompt."
248
+ } },
249
+ output: {
250
+ schema: {
251
+ type: "object",
252
+ additionalProperties: false,
253
+ properties: {
254
+ prompt: {
255
+ type: "string",
256
+ required: true
257
+ },
258
+ model: {
259
+ type: "string",
260
+ required: true
261
+ },
262
+ image: {
263
+ type: "object",
264
+ required: true,
265
+ additionalProperties: true,
266
+ properties: {
267
+ attachmentId: {
268
+ type: "string",
269
+ required: true
270
+ },
271
+ mediaType: {
272
+ type: "string",
273
+ enum: ["image/png"],
274
+ required: true
275
+ },
276
+ bytes: {
277
+ type: "integer",
278
+ required: true
279
+ },
280
+ width: {
281
+ type: "integer",
282
+ required: true
283
+ },
284
+ height: {
285
+ type: "integer",
286
+ required: true
287
+ },
288
+ name: { type: "string" }
289
+ }
290
+ }
291
+ }
205
292
  },
206
- inject(message) {
207
- deliver("inject", message);
293
+ render: (_args, value) => {
294
+ const output = value;
295
+ return [{
296
+ type: "text",
297
+ text: `Generated image for: ${output.prompt}`
298
+ }, {
299
+ type: "image",
300
+ attachment: output.image
301
+ }];
208
302
  }
209
- };
210
- record = {
211
- originals,
212
- wrappers,
213
- owners: 1
214
- };
215
- for (const wrapper of Object.values(wrappers)) Object.defineProperty(wrapper, DSH_SUBAGENT_REPORT_DEDUP_COMPAT_MARKER, { value: record });
216
- try {
217
- agent.followup = wrappers.followup;
218
- agent.steer = wrappers.steer;
219
- agent.inject = wrappers.inject;
220
- patches.set(agent, record);
221
- } catch (error) {
222
- record.owners = 0;
223
- for (const name of [
224
- "followup",
225
- "steer",
226
- "inject"
227
- ]) if (agent[name] === wrappers[name]) try {
228
- agent[name] = originals[name];
229
- } catch {}
230
- ctx.logger.warn("[dsh-chatgpt-subscription] Could not install temporary DSH subagent dedup compatibility: " + errorMessage(error));
303
+ },
304
+ timeoutMs: 5 * 6e4,
305
+ isConcurrencySafe: () => true,
306
+ presentCall: (args) => ({
307
+ card: "generic",
308
+ kind: "other",
309
+ title: "Generate image",
310
+ rawInput: args
311
+ }),
312
+ presentResult: (_args, result) => ({
313
+ card: "generic",
314
+ title: result.isError ? "Image generation failed" : "Generated image",
315
+ content: result.content
316
+ }),
317
+ async execute(args, exec) {
318
+ const prompt = args.prompt.trim();
319
+ if (prompt === "") throw new HarnessError("Image prompt cannot be empty.", "CODEX_IMAGE_INVALID_PROMPT");
320
+ if (!attachments.imageLimits.mediaTypes.includes("image/png")) throw new HarnessError("PNG image attachments are not enabled in this DSH environment.", "CODEX_IMAGE_ATTACHMENT_UNSUPPORTED");
321
+ let credentials = await imageCredentials(oauth);
322
+ let response = await requestImage(fetchFn, credentials, prompt, String(exec.callId), exec.signal);
323
+ if (response.status === 401) {
324
+ await response.body?.cancel().catch(() => void 0);
325
+ credentials = await imageCredentials(oauth, true);
326
+ response = await requestImage(fetchFn, credentials, prompt, String(exec.callId), exec.signal);
327
+ }
328
+ if (response.status === 429) {
329
+ await response.body?.cancel().catch(() => void 0);
330
+ throw new HarnessError("Codex image generation was rate limited.", "CODEX_IMAGE_RATE_LIMITED");
331
+ }
332
+ if (!response.ok) {
333
+ await response.body?.cancel().catch(() => void 0);
334
+ throw new HarnessError(`Codex image generation failed (${response.status}).`, "CODEX_IMAGE_FAILED");
335
+ }
336
+ const bytes = decodeImageBytes(readBase64Image(await response.json()), maxGeneratedImageBytes(attachments));
337
+ const image = await attachments.saveImage({
338
+ data: bytes,
339
+ mediaType: "image/png",
340
+ name: "codex-generated-image.png"
341
+ });
342
+ const output = {
343
+ prompt,
344
+ model: CODEX_IMAGE_MODEL,
345
+ image: {
346
+ attachmentId: image.attachmentId,
347
+ mediaType: "image/png",
348
+ bytes: image.bytes,
349
+ width: image.width,
350
+ height: image.height,
351
+ ...image.name !== void 0 ? { name: image.name } : {}
352
+ }
353
+ };
354
+ if (exec.parent !== void 0) exec.deferContext(createUserMessage({
355
+ content: [{
356
+ type: "image",
357
+ attachment: image
358
+ }],
359
+ source: {
360
+ kind: "plugin",
361
+ plugin: "dsh-chatgpt-subscription",
362
+ form: "notice",
363
+ summary: "Generated image from Codex image tool."
364
+ }
365
+ }));
366
+ return output;
231
367
  }
232
- };
233
- const unpatch = (agent) => {
234
- const record = patches.get(agent);
235
- if (!record) return;
236
- patches.delete(agent);
237
- record.owners -= 1;
238
- if (record.owners > 0) return;
239
- for (const name of [
240
- "followup",
241
- "steer",
242
- "inject"
243
- ]) {
244
- if (agent[name] !== record.wrappers[name]) continue;
245
- try {
246
- agent[name] = record.originals[name];
247
- } catch (error) {
248
- ctx.logger.warn("[dsh-chatgpt-subscription] Could not remove temporary DSH subagent dedup compatibility: " + errorMessage(error));
368
+ });
369
+ }
370
+ async function imageCredentials(oauth, force = false) {
371
+ try {
372
+ return await oauth.credentials(force);
373
+ } catch (error) {
374
+ throw new HarnessError("ChatGPT subscription credentials are required for Codex image generation.", "CODEX_IMAGE_CREDENTIAL_MISSING", { cause: error });
375
+ }
376
+ }
377
+ function requestImage(fetchFn, credentials, prompt, turnId, signal) {
378
+ return fetchFn(CODEX_IMAGE_GENERATION_URL, {
379
+ method: "POST",
380
+ headers: {
381
+ ...codexHeaders(credentials),
382
+ originator: "pi",
383
+ accept: "application/json",
384
+ "content-type": "application/json",
385
+ "x-codex-image-turn-id": turnId
386
+ },
387
+ body: JSON.stringify({
388
+ prompt,
389
+ background: "auto",
390
+ model: CODEX_IMAGE_MODEL,
391
+ quality: "auto",
392
+ size: "auto"
393
+ }),
394
+ signal
395
+ });
396
+ }
397
+ function readBase64Image(value) {
398
+ const root = record$4(value);
399
+ const data = Array.isArray(root?.data) ? root.data[0] : null;
400
+ const image = string$2(record$4(data)?.b64_json) ?? string$2(record$4(data)?.image_base64) ?? string$2(root?.b64_json) ?? string$2(root?.image);
401
+ if (image === null) throw new HarnessError("Codex image response did not include image data.", "CODEX_IMAGE_RESPONSE_INVALID");
402
+ return image;
403
+ }
404
+ function decodeImageBytes(base64, maxBytes) {
405
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(base64) || base64.length % 4 !== 0) throw new HarnessError("Codex image response was not valid base64.", "CODEX_IMAGE_RESPONSE_INVALID");
406
+ const buffer = Buffer.from(base64, "base64");
407
+ if (buffer.length > maxBytes) throw new HarnessError("Generated image exceeds the configured attachment size limit.", "CODEX_IMAGE_TOO_LARGE");
408
+ for (const [index, byte] of PNG_SIGNATURE.entries()) if (buffer[index] !== byte) throw new HarnessError("Codex image response was not a PNG image.", "CODEX_IMAGE_RESPONSE_INVALID");
409
+ return buffer;
410
+ }
411
+ function maxGeneratedImageBytes(attachments) {
412
+ return Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
413
+ }
414
+ function record$4(value) {
415
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
416
+ }
417
+ function string$2(value) {
418
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
419
+ }
420
+ //#endregion
421
+ //#region src/host/codex-search.ts
422
+ const DEFAULT_SEARCH_MODEL = "gpt-5.6-luna";
423
+ function createCodexSearchProvider(oauth, options = {}) {
424
+ const fetchFn = options.fetchFn ?? fetch;
425
+ const model = options.model ?? DEFAULT_SEARCH_MODEL;
426
+ const idFactory = options.idFactory ?? randomUUID;
427
+ return {
428
+ id: CODEX_SEARCH_PROVIDER_ID,
429
+ available: () => true,
430
+ async search(request, signal) {
431
+ const query = request.query.trim();
432
+ if (query === "") return {
433
+ sources: [],
434
+ truncated: false
435
+ };
436
+ let credentials = await searchCredentials(oauth);
437
+ let response = await sendSearch(fetchFn, credentials, query, model, idFactory(), signal);
438
+ if (response.status === 401) {
439
+ await response.body?.cancel().catch(() => void 0);
440
+ credentials = await searchCredentials(oauth, true);
441
+ response = await sendSearch(fetchFn, credentials, query, model, idFactory(), signal);
442
+ }
443
+ if (response.status === 429) {
444
+ await response.body?.cancel().catch(() => void 0);
445
+ throw new WebError("Codex subscription search was rate limited.", "WEB_PROVIDER_RATE_LIMITED");
446
+ }
447
+ if (!response.ok) {
448
+ await response.body?.cancel().catch(() => void 0);
449
+ throw new WebError(`Codex subscription search failed (${response.status}).`, "WEB_PROVIDER_ERROR");
249
450
  }
451
+ return normalizeSearchResult(await response.json(), request.maxResults);
250
452
  }
251
453
  };
252
- for (const agent of ctx.agents.list()) patch(agent);
253
- const disposeCreated = ctx.on("agent/created", ({ agent }) => patch(agent));
254
- const disposeDisposed = ctx.on("agent/disposed", ({ agent }) => unpatch(agent));
255
- return () => {
256
- disposeDisposed();
257
- disposeCreated();
258
- for (const agent of [...patches.keys()]) unpatch(agent);
454
+ }
455
+ async function searchCredentials(oauth, force = false) {
456
+ try {
457
+ return await oauth.credentials(force);
458
+ } catch (error) {
459
+ throw new WebError("ChatGPT subscription credentials are required for Codex search.", "WEB_PROVIDER_CREDENTIAL_MISSING", { cause: error });
460
+ }
461
+ }
462
+ function sendSearch(fetchFn, credentials, query, model, id, signal) {
463
+ return fetchFn(CODEX_SEARCH_URL, {
464
+ method: "POST",
465
+ headers: {
466
+ ...codexHeaders(credentials),
467
+ originator: "pi",
468
+ accept: "application/json",
469
+ "content-type": "application/json"
470
+ },
471
+ body: JSON.stringify({
472
+ id,
473
+ model,
474
+ input: query,
475
+ commands: {
476
+ search_query: [{ q: query }],
477
+ response_length: "short"
478
+ },
479
+ settings: {
480
+ allowed_callers: ["direct"],
481
+ external_web_access: true
482
+ },
483
+ max_output_tokens: 4096
484
+ }),
485
+ signal
486
+ });
487
+ }
488
+ function normalizeSearchResult(data, maxResults) {
489
+ const sources = dedupeSources(readSources(data));
490
+ const limit = typeof maxResults === "number" && Number.isFinite(maxResults) && maxResults >= 0 ? Math.floor(maxResults) : void 0;
491
+ const truncated = limit !== void 0 && sources.length > limit;
492
+ return {
493
+ content: readContent(data),
494
+ sources: limit !== void 0 ? sources.slice(0, limit) : sources,
495
+ truncated
259
496
  };
260
497
  }
261
- //#endregion
262
- //#region src/compat.ts
263
- /**
264
- * Compatibility constants for the ChatGPT-backed Codex flow. The backend and
265
- * OAuth parameters are not a public third-party API contract, so every such
266
- * value is isolated here for review and rollback.
267
- */
268
- const CHATGPT_OAUTH_ISSUER = "https://auth.openai.com";
269
- const CHATGPT_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
270
- const OAUTH_CALLBACK_HOST = "localhost";
271
- const OAUTH_CALLBACK_PORT = 1455;
272
- const OAUTH_REDIRECT_URI = `http://${OAUTH_CALLBACK_HOST}:${OAUTH_CALLBACK_PORT}/auth/callback`;
273
- const OAUTH_SCOPE = "openid profile email offline_access";
274
- const OAUTH_ORIGINATOR = "opencode";
275
- const ROUTE_PREFIX = "/api/dsh-chatgpt-subscription";
276
- const PLUGIN_VERSION = "0.1.0-alpha.0";
277
- const CODEX_RESPONSES_URL = `https://chatgpt.com/backend-api/codex/responses`;
278
- const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
279
- const CODEX_ORIGINATOR = "opencode";
280
- const QUOTA_MIN_UPSTREAM_INTERVAL_MS = 15e3;
281
- const OAUTH_AUTHORIZE_URL = `${CHATGPT_OAUTH_ISSUER}/oauth/authorize`;
282
- const OAUTH_TOKEN_URL = `${CHATGPT_OAUTH_ISSUER}/oauth/token`;
498
+ function readSources(data) {
499
+ const root = record$3(data);
500
+ if (root === null) return [];
501
+ const candidates = [
502
+ root.sources,
503
+ root.results,
504
+ record$3(root.search_result)?.sources,
505
+ record$3(root.web_search)?.sources
506
+ ];
507
+ for (const candidate of candidates) {
508
+ if (!Array.isArray(candidate)) continue;
509
+ const sources = candidate.map(readSource).filter(isSource);
510
+ if (sources.length > 0) return sources;
511
+ }
512
+ return [];
513
+ }
514
+ function readSource(value) {
515
+ const data = record$3(value);
516
+ if (data === null) return null;
517
+ const url = string$1(data.url) ?? string$1(data.link) ?? string$1(data.uri);
518
+ if (url === null || !isHttpUrl(url)) return null;
519
+ const title = string$1(data.title) ?? string$1(data.name);
520
+ const snippet = string$1(data.snippet) ?? string$1(data.text) ?? string$1(data.description);
521
+ const publishedAt = string$1(data.published_at) ?? string$1(data.publishedAt);
522
+ return {
523
+ url,
524
+ ...title !== null ? { title } : {},
525
+ ...snippet !== null ? { snippet } : {},
526
+ ...publishedAt !== null ? { publishedAt } : {}
527
+ };
528
+ }
529
+ function readContent(data) {
530
+ const root = record$3(data);
531
+ if (root === null) return void 0;
532
+ return string$1(root.content) ?? string$1(root.output_text) ?? string$1(root.summary) ?? void 0;
533
+ }
534
+ function dedupeSources(sources) {
535
+ const seen = /* @__PURE__ */ new Set();
536
+ const result = [];
537
+ for (const source of sources) {
538
+ const key = source.url.trim().toLowerCase();
539
+ if (seen.has(key)) continue;
540
+ seen.add(key);
541
+ result.push(source);
542
+ }
543
+ return result;
544
+ }
545
+ function record$3(value) {
546
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
547
+ }
548
+ function string$1(value) {
549
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
550
+ }
551
+ function isHttpUrl(value) {
552
+ try {
553
+ const url = new URL(value);
554
+ return url.protocol === "http:" || url.protocol === "https:";
555
+ } catch {
556
+ return false;
557
+ }
558
+ }
559
+ function isSource(value) {
560
+ return value !== null;
561
+ }
283
562
  //#endregion
284
563
  //#region src/host/callback-server.ts
285
564
  /** One-shot localhost OAuth callback listener. */
@@ -766,6 +1045,58 @@ async function oauthErrorIdentifier(response) {
766
1045
  return null;
767
1046
  }
768
1047
  //#endregion
1048
+ //#region src/shared/preferences.ts
1049
+ const PREFERENCES_NAMESPACE = "dsh-chatgpt-subscription";
1050
+ const DEFAULT_PREFERENCES = {
1051
+ quickQuotaVisible: false,
1052
+ searchProvider: "dsh"
1053
+ };
1054
+ const SEARCH_PROVIDER_CODEX = "codex";
1055
+ function isSearchProviderPreference(value) {
1056
+ return value === "dsh" || value === "codex";
1057
+ }
1058
+ //#endregion
1059
+ //#region src/host/preferences.ts
1060
+ function registerPreferenceStore(settings) {
1061
+ return new SettingsPreferenceStore(settings.register(settingsNamespace(PREFERENCES_NAMESPACE), z.object({
1062
+ quickQuotaVisible: z.boolean().default(DEFAULT_PREFERENCES.quickQuotaVisible),
1063
+ searchProvider: z.union([z.const("dsh"), z.const(SEARCH_PROVIDER_CODEX)]).default(DEFAULT_PREFERENCES.searchProvider)
1064
+ })));
1065
+ }
1066
+ var SettingsPreferenceStore = class {
1067
+ scope;
1068
+ constructor(scope) {
1069
+ this.scope = scope;
1070
+ }
1071
+ status() {
1072
+ return withWritable(this.scope.get());
1073
+ }
1074
+ async update(patch) {
1075
+ const normalized = {};
1076
+ if (patch.quickQuotaVisible !== void 0) normalized.quickQuotaVisible = patch.quickQuotaVisible;
1077
+ if (patch.searchProvider !== void 0) {
1078
+ if (!isSearchProviderPreference(patch.searchProvider)) throw new PreferenceError("Unsupported search provider preference.");
1079
+ normalized.searchProvider = patch.searchProvider;
1080
+ }
1081
+ await this.scope.update(normalized);
1082
+ return this.status();
1083
+ }
1084
+ watch(callback) {
1085
+ return this.scope.watch((next, prev) => callback(withWritable(next), withWritable(prev)));
1086
+ }
1087
+ };
1088
+ var PreferenceError = class extends Error {
1089
+ constructor(message) {
1090
+ super(message);
1091
+ }
1092
+ };
1093
+ function withWritable(value) {
1094
+ return {
1095
+ ...value,
1096
+ writable: true
1097
+ };
1098
+ }
1099
+ //#endregion
769
1100
  //#region src/host/responses-mapper.ts
770
1101
  function hiddenSandboxControlToolNames(options) {
771
1102
  const retryTools = recentSandboxRetryToolNames(options.messages);
@@ -850,10 +1181,10 @@ async function buildResponsesPayload(options, attachments, localRawImages = {})
850
1181
  payload.tool_choice = "auto";
851
1182
  payload.parallel_tool_calls = true;
852
1183
  }
853
- if (options.reasoningEffort !== void 0) payload.reasoning = {
1184
+ if (options.reasoningEffort !== void 0) payload.reasoning = codexModelSupportsReasoningSummary(options.model) ? {
854
1185
  effort: options.reasoningEffort,
855
1186
  summary: "auto"
856
- };
1187
+ } : { effort: options.reasoningEffort };
857
1188
  return payload;
858
1189
  }
859
1190
  function runCodeInstruction(tools) {
@@ -865,7 +1196,7 @@ function localRawImageInstruction(stats) {
865
1196
  return "Image attachment rule: a user message contains a markdown image link to a local/raw session URL but no structured image attachment. That link is not accessible image bytes for the provider. Do not claim to see the image; ask the user to resend it as an actual image attachment if visual inspection is required.";
866
1197
  }
867
1198
  function supportsImageInput(options) {
868
- return options.provider === "codex-chatgpt" && options.model.toLowerCase().startsWith("gpt-");
1199
+ return options.provider === "codex-chatgpt" && codexModelSupportsImageInput(options.model);
869
1200
  }
870
1201
  function toolDescriptionForCodex(name, description) {
871
1202
  if (name === "run_code") return `${description}\n\nCompatibility: code is strict JavaScript/TypeScript and nested shell commands are string data. Template literals may consume \${...}, backticks, backslashes, and escape sequences before PowerShell, Bash, or POSIX sh sees them. Prefer ordinary quoted string arrays joined with "\\n", or write a script file with a dedicated file tool before invoking the shell.`;
@@ -1140,31 +1471,6 @@ function record$2(value) {
1140
1471
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1141
1472
  }
1142
1473
  //#endregion
1143
- //#region src/host/wire-auth.ts
1144
- function codexHeaders(credentials, sessionId) {
1145
- const dshAgent = attributionHeaders()["user-agent"] ?? "dsh/unknown";
1146
- return {
1147
- authorization: `Bearer ${credentials.accessToken}`,
1148
- ...credentials.accountId ? { "chatgpt-account-id": credentials.accountId } : {},
1149
- originator: CODEX_ORIGINATOR,
1150
- "user-agent": `dsh-chatgpt-subscription/${PLUGIN_VERSION} (${dshAgent})`,
1151
- ...sessionId ? { "session-id": sessionId } : {}
1152
- };
1153
- }
1154
- function stableSessionId(value) {
1155
- const source = value === void 0 || value === "" ? randomUUID() : value;
1156
- return `dsh-${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
1157
- }
1158
- function retryAfterMs(headers) {
1159
- const raw = headers.get("retry-after");
1160
- if (raw === null) return void 0;
1161
- const seconds = Number(raw);
1162
- if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1e3, 10 * 6e4);
1163
- const timestamp = Date.parse(raw);
1164
- if (!Number.isFinite(timestamp)) return void 0;
1165
- return Math.min(Math.max(0, timestamp - Date.now()), 10 * 6e4);
1166
- }
1167
- //#endregion
1168
1474
  //#region src/host/responses-client.ts
1169
1475
  const MAX_VISIBLE_REASONING_CHARS = 12e3;
1170
1476
  const REASONING_DELTA_FLUSH_CHARS = 768;
@@ -1459,7 +1765,7 @@ async function* parseResponsesStream(response, signal, hiddenSandboxControls = /
1459
1765
  yield {
1460
1766
  type: "finish",
1461
1767
  reason: validToolCount > 0 ? { kind: "tool-calls" } : terminal,
1462
- replayState: { outputItems: replayOutput }
1768
+ replayState: { response: { outputItems: replayOutput } }
1463
1769
  };
1464
1770
  }
1465
1771
  function visibleReasoningDelta(delta, currentVisibleChars, alreadyTruncated) {
@@ -1541,6 +1847,13 @@ function number(value) {
1541
1847
  }
1542
1848
  //#endregion
1543
1849
  //#region src/host/usage-service.ts
1850
+ const EMPTY_USAGE = {
1851
+ buckets: [],
1852
+ credits: null,
1853
+ individualLimit: null,
1854
+ spendControlReached: null,
1855
+ resetCredits: null
1856
+ };
1544
1857
  var UsageService = class {
1545
1858
  oauth;
1546
1859
  fetchFn;
@@ -1558,7 +1871,7 @@ var UsageService = class {
1558
1871
  async status(authenticated, force = false) {
1559
1872
  if (!authenticated) return {
1560
1873
  state: "signed-out",
1561
- buckets: [],
1874
+ ...EMPTY_USAGE,
1562
1875
  fetchedAt: null,
1563
1876
  stale: false
1564
1877
  };
@@ -1635,9 +1948,9 @@ var UsageService = class {
1635
1948
  message: `Quota request failed (${response.status}).`
1636
1949
  });
1637
1950
  }
1638
- const buckets = mapCodexUsage(await response.json());
1951
+ const usage = parseCodexUsage(await response.json());
1639
1952
  this.cache = {
1640
- buckets,
1953
+ usage,
1641
1954
  fetchedAt: this.now(),
1642
1955
  accountKey
1643
1956
  };
@@ -1660,14 +1973,14 @@ var UsageService = class {
1660
1973
  fromCache(stale, error) {
1661
1974
  if (this.cache === null) return {
1662
1975
  state: error ? "error" : "empty",
1663
- buckets: [],
1976
+ ...EMPTY_USAGE,
1664
1977
  fetchedAt: null,
1665
1978
  stale,
1666
1979
  ...error ? { error } : {}
1667
1980
  };
1668
1981
  return {
1669
- state: error ? "stale" : this.cache.buckets.length > 0 ? "ready" : "empty",
1670
- buckets: structuredClone(this.cache.buckets),
1982
+ state: error ? "stale" : this.cache.usage.buckets.length > 0 ? "ready" : "empty",
1983
+ ...structuredClone(this.cache.usage),
1671
1984
  fetchedAt: Math.floor(this.cache.fetchedAt / 1e3),
1672
1985
  stale,
1673
1986
  ...error ? { error } : {}
@@ -1685,15 +1998,32 @@ var UsageServiceError = class extends Error {
1685
1998
  }
1686
1999
  };
1687
2000
  function mapCodexUsage(value) {
2001
+ return parseCodexUsage(value).buckets;
2002
+ }
2003
+ function parseCodexUsage(value) {
1688
2004
  const data = record(value);
1689
- if (data === null) return [];
2005
+ if (data === null) return structuredClone(EMPTY_USAGE);
1690
2006
  const planType = typeof data.plan_type === "string" ? data.plan_type : null;
1691
- const result = [];
1692
- addBucket(result, "codex", "Codex", planType, data.rate_limit);
1693
- addBucket(result, "code-review", "Code review", planType, data.code_review_rate_limit);
1694
- return result;
2007
+ const buckets = [];
2008
+ const usedIds = /* @__PURE__ */ new Set();
2009
+ addBucket(buckets, usedIds, "codex", "Codex", planType, data.rate_limit);
2010
+ addBucket(buckets, usedIds, "code-review", "Code review", planType, data.code_review_rate_limit);
2011
+ const additional = Array.isArray(data.additional_rate_limits) ? data.additional_rate_limits : [];
2012
+ for (const [index, value] of additional.entries()) {
2013
+ const limit = record(value);
2014
+ if (limit === null) continue;
2015
+ const idSource = text(limit.limit_name) ?? text(limit.metered_feature) ?? `additional-${index + 1}`;
2016
+ addBucket(buckets, usedIds, uniqueId(slug(idSource), usedIds), readableLimitName(text(limit.limit_name) ?? text(limit.metered_feature) ?? idSource), planType, limit.rate_limit);
2017
+ }
2018
+ return {
2019
+ buckets,
2020
+ credits: mapCredits(data.credits),
2021
+ individualLimit: mapIndividualLimit(record(data.spend_control)?.individual_limit),
2022
+ spendControlReached: boolean(record(data.spend_control)?.reached),
2023
+ resetCredits: mapResetCredits(data.rate_limit_reset_credits)
2024
+ };
1695
2025
  }
1696
- function addBucket(result, id, name, planType, value) {
2026
+ function addBucket(result, usedIds, id, name, planType, value) {
1697
2027
  const source = record(value);
1698
2028
  if (source === null) return;
1699
2029
  const primary = mapWindow(source.primary_window);
@@ -1704,8 +2034,10 @@ function addBucket(result, id, name, planType, value) {
1704
2034
  name,
1705
2035
  planType,
1706
2036
  primary,
1707
- secondary
2037
+ secondary,
2038
+ windows: [primary, secondary].filter(isWindow)
1708
2039
  });
2040
+ usedIds.add(id);
1709
2041
  }
1710
2042
  function mapWindow(value) {
1711
2043
  const data = record(value);
@@ -1720,21 +2052,86 @@ function mapWindow(value) {
1720
2052
  resetsAt: reset !== void 0 && reset > 0 ? reset : null
1721
2053
  };
1722
2054
  }
2055
+ function mapCredits(value) {
2056
+ const data = record(value);
2057
+ if (data === null) return null;
2058
+ const hasCredits = boolean(data.has_credits);
2059
+ const unlimited = boolean(data.unlimited);
2060
+ const balance = decimalText(data.balance);
2061
+ if (hasCredits === null && unlimited === null && balance === null) return null;
2062
+ return {
2063
+ hasCredits: hasCredits ?? (balance !== null || unlimited === true),
2064
+ unlimited: unlimited ?? false,
2065
+ balance
2066
+ };
2067
+ }
2068
+ function mapIndividualLimit(value) {
2069
+ const data = record(value);
2070
+ if (data === null) return null;
2071
+ const remaining = numeric(data.remaining_percent);
2072
+ const reset = numeric(data.reset_at);
2073
+ const limit = decimalText(data.limit);
2074
+ const used = decimalText(data.used);
2075
+ if (remaining === void 0 && reset === void 0 && limit === null && used === null) return null;
2076
+ return {
2077
+ limit,
2078
+ used,
2079
+ remainingPercent: remaining !== void 0 ? Math.min(100, Math.max(0, remaining)) : null,
2080
+ resetsAt: reset !== void 0 && reset > 0 ? reset : null
2081
+ };
2082
+ }
2083
+ function mapResetCredits(value) {
2084
+ const data = record(value);
2085
+ if (data === null) return null;
2086
+ const available = numeric(data.available_count);
2087
+ if (available === void 0) return null;
2088
+ return { availableCount: Math.max(0, Math.floor(available)) };
2089
+ }
1723
2090
  function record(value) {
1724
2091
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1725
2092
  }
2093
+ function boolean(value) {
2094
+ return typeof value === "boolean" ? value : null;
2095
+ }
1726
2096
  function numeric(value) {
1727
2097
  if (typeof value !== "number" && (typeof value !== "string" || value.trim() === "")) return void 0;
1728
2098
  const number = Number(value);
1729
2099
  return Number.isFinite(number) ? number : void 0;
1730
2100
  }
2101
+ function decimalText(value) {
2102
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
2103
+ if (typeof value !== "string") return null;
2104
+ const trimmed = value.trim();
2105
+ return trimmed !== "" ? trimmed : null;
2106
+ }
2107
+ function text(value) {
2108
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
2109
+ }
2110
+ function slug(value) {
2111
+ return value.trim().toLowerCase().replace(/[_\s]+/g, "-").replace(/[^a-z0-9-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "") || "additional";
2112
+ }
2113
+ function uniqueId(base, usedIds) {
2114
+ let candidate = base;
2115
+ let suffix = 2;
2116
+ while (usedIds.has(candidate)) {
2117
+ candidate = `${base}-${suffix}`;
2118
+ suffix += 1;
2119
+ }
2120
+ return candidate;
2121
+ }
2122
+ function readableLimitName(value) {
2123
+ return value.replace(/^codex[_-]/i, "").replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (match) => match.toUpperCase()) || "Additional limit";
2124
+ }
2125
+ function isWindow(value) {
2126
+ return value !== null;
2127
+ }
1731
2128
  function identityKey(credentials) {
1732
2129
  return credentials.accountId ?? credentials.email ?? credentials.planType ?? "signed-in";
1733
2130
  }
1734
2131
  //#endregion
1735
2132
  //#region src/host/routes.ts
1736
2133
  const MAX_BODY_BYTES = 64 * 1024;
1737
- function registerRoutes(ctx, oauth, usage) {
2134
+ function registerRoutes(ctx, oauth, usage, preferences) {
1738
2135
  const handler = async (request, response) => {
1739
2136
  const url = new URL(request.url ?? "/", "http://dsh.local");
1740
2137
  if (request.method === "GET" && url.pathname === `/api/dsh-chatgpt-subscription/status`) {
@@ -1743,7 +2140,8 @@ function registerRoutes(ctx, oauth, usage) {
1743
2140
  ok: true,
1744
2141
  value: {
1745
2142
  ...oauthStatus,
1746
- quota: await usage.status(oauthStatus.authenticated)
2143
+ quota: await usage.status(oauthStatus.authenticated),
2144
+ preferences: preferences.status()
1747
2145
  }
1748
2146
  });
1749
2147
  return;
@@ -1810,7 +2208,8 @@ function registerRoutes(ctx, oauth, usage) {
1810
2208
  ok: true,
1811
2209
  value: {
1812
2210
  ...oauthStatus,
1813
- quota: await usage.status(oauthStatus.authenticated)
2211
+ quota: await usage.status(oauthStatus.authenticated),
2212
+ preferences: preferences.status()
1814
2213
  }
1815
2214
  });
1816
2215
  return;
@@ -1828,13 +2227,22 @@ function registerRoutes(ctx, oauth, usage) {
1828
2227
  value: await usage.testConnection()
1829
2228
  });
1830
2229
  return;
2230
+ case `${ROUTE_PREFIX}/preferences/update`:
2231
+ json(response, {
2232
+ ok: true,
2233
+ value: await preferences.update(readPreferencesUpdate(body))
2234
+ });
2235
+ return;
1831
2236
  default: jsonError(response, 404, {
1832
2237
  code: "bad-request",
1833
2238
  message: "Route not found."
1834
2239
  });
1835
2240
  }
1836
2241
  } catch (error) {
1837
- const mapped = error instanceof UsageServiceError ? error.publicError : publicError(error, error instanceof Error && error.message === "missing loginId" ? "bad-request" : error instanceof Error && error.message === "not authenticated" ? "not-authenticated" : "internal");
2242
+ const mapped = error instanceof UsageServiceError ? error.publicError : error instanceof PreferenceError ? {
2243
+ code: "bad-request",
2244
+ message: error.message
2245
+ } : publicError(error, error instanceof Error && error.message === "missing loginId" ? "bad-request" : error instanceof Error && error.message === "not authenticated" ? "not-authenticated" : "internal");
1838
2246
  jsonError(response, statusFor(mapped), mapped);
1839
2247
  }
1840
2248
  };
@@ -1934,6 +2342,18 @@ function field(value, name) {
1934
2342
  const candidate = value[name];
1935
2343
  return typeof candidate === "string" && candidate !== "" ? candidate : null;
1936
2344
  }
2345
+ function readPreferencesUpdate(value) {
2346
+ const patch = {};
2347
+ if ("quickQuotaVisible" in value) {
2348
+ if (typeof value.quickQuotaVisible !== "boolean") throw new PreferenceError("quickQuotaVisible must be a boolean.");
2349
+ patch.quickQuotaVisible = value.quickQuotaVisible;
2350
+ }
2351
+ if ("searchProvider" in value) {
2352
+ if (value.searchProvider !== "dsh" && value.searchProvider !== "codex") throw new PreferenceError("searchProvider must be dsh or codex.");
2353
+ patch.searchProvider = value.searchProvider;
2354
+ }
2355
+ return patch;
2356
+ }
1937
2357
  function json(response, envelope, status = 200) {
1938
2358
  response.writeHead(status, {
1939
2359
  "content-type": "application/json; charset=utf-8",
@@ -2191,26 +2611,77 @@ function createPlatformTokenStore(platform = process.platform) {
2191
2611
  throw new Error(`Unsupported platform ${platform}; dsh-chatgpt-subscription supports Windows and Linux.`);
2192
2612
  }
2193
2613
  //#endregion
2614
+ //#region src/host/search-provider-switcher.ts
2615
+ var SearchProviderSwitcher = class {
2616
+ loader;
2617
+ originalProvider;
2618
+ initialized = false;
2619
+ constructor(loader) {
2620
+ this.loader = loader;
2621
+ }
2622
+ async select(preference) {
2623
+ const entry = this.findWebEntry();
2624
+ if (entry === null) return;
2625
+ const config = currentConfig(entry);
2626
+ if (!this.initialized) {
2627
+ this.originalProvider = typeof config.searchProvider === "string" && config.searchProvider !== "codex-subscription" ? config.searchProvider : void 0;
2628
+ this.initialized = true;
2629
+ }
2630
+ const selected = preference === "codex" ? CODEX_SEARCH_PROVIDER_ID : this.originalProvider;
2631
+ if (config.searchProvider === selected) return;
2632
+ const nextConfig = { ...config };
2633
+ if (selected === void 0) delete nextConfig.searchProvider;
2634
+ else nextConfig.searchProvider = selected;
2635
+ await entry.update({ config: nextConfig }, true);
2636
+ }
2637
+ findWebEntry() {
2638
+ for (const entry of this.loader.entries()) {
2639
+ if (entry.options.id === "web") return entry;
2640
+ if (entry.options.name === "@deepseek-ai/dsh-web") return entry;
2641
+ }
2642
+ return null;
2643
+ }
2644
+ };
2645
+ function currentConfig(entry) {
2646
+ const config = entry.options.config;
2647
+ return typeof config === "object" && config !== null && !Array.isArray(config) ? config : {};
2648
+ }
2649
+ //#endregion
2194
2650
  //#region src/index.ts
2195
2651
  const inject = [
2196
2652
  "webServer",
2197
2653
  "llm",
2198
2654
  "attachments",
2199
- "agents"
2655
+ "tools",
2656
+ "web",
2657
+ "settings",
2658
+ "loader"
2200
2659
  ];
2201
2660
  function apply(ctx) {
2202
2661
  const oauth = new OAuthService(createPlatformTokenStore(), { logger: ctx.logger });
2203
2662
  const usage = new UsageService(oauth);
2663
+ const preferences = registerPreferenceStore(ctx.settings);
2204
2664
  const adapter = new CodexChatGptAdapter(new ResponsesClient(oauth, ctx.attachments, {
2205
2665
  localRawImages: { baseUrl: localWebServerBaseUrl(ctx.webServer.host, ctx.webServer.port) },
2206
2666
  onGenerationFinished: () => usage.invalidate()
2207
2667
  }));
2208
2668
  ctx.effect(() => {
2209
- const disposeRoutes = registerRoutes(ctx, oauth, usage);
2669
+ const searchSwitcher = new SearchProviderSwitcher(ctx.loader);
2670
+ const applySearchPreference = (searchProvider = preferences.status().searchProvider) => {
2671
+ searchSwitcher.select(searchProvider).catch((error) => {
2672
+ ctx.logger.warn(`[dsh-chatgpt-subscription] Search provider preference could not be applied: ${error instanceof Error ? error.message : String(error)}`);
2673
+ });
2674
+ };
2675
+ const disposeRoutes = registerRoutes(ctx, oauth, usage, preferences);
2210
2676
  const disposeAdapter = ctx.llm.registerAdapter([PROVIDER_ID], adapter);
2211
- const disposeSubagentReportCompat = installSubagentReportDedupCompat(ctx);
2677
+ const disposeImageTool = ctx.tools.register(createCodexImageTool(oauth, ctx.attachments));
2678
+ const disposeSearchProvider = ctx.web.registerSearchProvider(createCodexSearchProvider(oauth));
2679
+ const disposePreferenceWatch = preferences.watch((next) => applySearchPreference(next.searchProvider));
2680
+ applySearchPreference();
2212
2681
  return () => {
2213
- disposeSubagentReportCompat();
2682
+ disposePreferenceWatch();
2683
+ disposeSearchProvider();
2684
+ disposeImageTool();
2214
2685
  disposeAdapter();
2215
2686
  disposeRoutes();
2216
2687
  oauth.dispose();
@@ -2221,4 +2692,4 @@ function localWebServerBaseUrl(host, port) {
2221
2692
  return `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}`;
2222
2693
  }
2223
2694
  //#endregion
2224
- export { CodexChatGptAdapter, LinuxFileTokenStore, OAuthService, ResponsesClient, UsageService, WindowsDpapiTokenStore, apply, createPlatformTokenStore, inject, mapCodexUsage, parseResponsesStream };
2695
+ export { CodexChatGptAdapter, LinuxFileTokenStore, OAuthService, ResponsesClient, UsageService, WindowsDpapiTokenStore, apply, createCodexImageTool, createCodexSearchProvider, createPlatformTokenStore, inject, mapCodexUsage, parseCodexUsage, parseResponsesStream };