@eddyskywalker/dsh-chatgpt-subscription 0.1.9 → 0.1.11

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 (46) hide show
  1. package/CHANGELOG.md +11 -1
  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 +720 -248
  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 +57 -3
  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 +1 -2
  29. package/lib/types/host/responses-mapper.d.ts.map +1 -1
  30. package/lib/types/host/routes.d.ts +2 -1
  31. package/lib/types/host/routes.d.ts.map +1 -1
  32. package/lib/types/host/search-provider-switcher.d.ts +15 -0
  33. package/lib/types/host/search-provider-switcher.d.ts.map +1 -0
  34. package/lib/types/host/usage-service.d.ts +2 -1
  35. package/lib/types/host/usage-service.d.ts.map +1 -1
  36. package/lib/types/index.d.ts +3 -1
  37. package/lib/types/index.d.ts.map +1 -1
  38. package/lib/types/shared/contracts.d.ts +40 -3
  39. package/lib/types/shared/contracts.d.ts.map +1 -1
  40. package/lib/types/shared/model-catalog.d.ts +81 -0
  41. package/lib/types/shared/model-catalog.d.ts.map +1 -0
  42. package/lib/types/shared/preferences.d.ts +7 -0
  43. package/lib/types/shared/preferences.d.ts.map +1 -0
  44. package/package.json +21 -2
  45. package/lib/types/host/subagent-report-scheduling-compat.d.ts +0 -21
  46. 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.`;
@@ -1131,7 +1462,8 @@ function replayOutputItems(message) {
1131
1462
  if (message.source.kind !== "model") return null;
1132
1463
  const replay = message.source.replayState;
1133
1464
  if (typeof replay !== "object" || replay === null || Array.isArray(replay)) return null;
1134
- const items = replay.outputItems;
1465
+ const envelope = replay;
1466
+ const items = Array.isArray(envelope.outputItems) ? envelope.outputItems : Array.isArray(record$2(envelope.response)?.outputItems) ? record$2(envelope.response).outputItems : null;
1135
1467
  if (!Array.isArray(items)) return null;
1136
1468
  return structuredClone(items.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item)));
1137
1469
  }
@@ -1139,31 +1471,6 @@ function record$2(value) {
1139
1471
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1140
1472
  }
1141
1473
  //#endregion
1142
- //#region src/host/wire-auth.ts
1143
- function codexHeaders(credentials, sessionId) {
1144
- const dshAgent = attributionHeaders()["user-agent"] ?? "dsh/unknown";
1145
- return {
1146
- authorization: `Bearer ${credentials.accessToken}`,
1147
- ...credentials.accountId ? { "chatgpt-account-id": credentials.accountId } : {},
1148
- originator: CODEX_ORIGINATOR,
1149
- "user-agent": `dsh-chatgpt-subscription/${PLUGIN_VERSION} (${dshAgent})`,
1150
- ...sessionId ? { "session-id": sessionId } : {}
1151
- };
1152
- }
1153
- function stableSessionId(value) {
1154
- const source = value === void 0 || value === "" ? randomUUID() : value;
1155
- return `dsh-${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
1156
- }
1157
- function retryAfterMs(headers) {
1158
- const raw = headers.get("retry-after");
1159
- if (raw === null) return void 0;
1160
- const seconds = Number(raw);
1161
- if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds * 1e3, 10 * 6e4);
1162
- const timestamp = Date.parse(raw);
1163
- if (!Number.isFinite(timestamp)) return void 0;
1164
- return Math.min(Math.max(0, timestamp - Date.now()), 10 * 6e4);
1165
- }
1166
- //#endregion
1167
1474
  //#region src/host/responses-client.ts
1168
1475
  const MAX_VISIBLE_REASONING_CHARS = 12e3;
1169
1476
  const REASONING_DELTA_FLUSH_CHARS = 768;
@@ -1458,7 +1765,7 @@ async function* parseResponsesStream(response, signal, hiddenSandboxControls = /
1458
1765
  yield {
1459
1766
  type: "finish",
1460
1767
  reason: validToolCount > 0 ? { kind: "tool-calls" } : terminal,
1461
- replayState: { outputItems: replayOutput }
1768
+ replayState: { response: { outputItems: replayOutput } }
1462
1769
  };
1463
1770
  }
1464
1771
  function visibleReasoningDelta(delta, currentVisibleChars, alreadyTruncated) {
@@ -1540,6 +1847,13 @@ function number(value) {
1540
1847
  }
1541
1848
  //#endregion
1542
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
+ };
1543
1857
  var UsageService = class {
1544
1858
  oauth;
1545
1859
  fetchFn;
@@ -1557,7 +1871,7 @@ var UsageService = class {
1557
1871
  async status(authenticated, force = false) {
1558
1872
  if (!authenticated) return {
1559
1873
  state: "signed-out",
1560
- buckets: [],
1874
+ ...EMPTY_USAGE,
1561
1875
  fetchedAt: null,
1562
1876
  stale: false
1563
1877
  };
@@ -1634,9 +1948,9 @@ var UsageService = class {
1634
1948
  message: `Quota request failed (${response.status}).`
1635
1949
  });
1636
1950
  }
1637
- const buckets = mapCodexUsage(await response.json());
1951
+ const usage = parseCodexUsage(await response.json());
1638
1952
  this.cache = {
1639
- buckets,
1953
+ usage,
1640
1954
  fetchedAt: this.now(),
1641
1955
  accountKey
1642
1956
  };
@@ -1659,14 +1973,14 @@ var UsageService = class {
1659
1973
  fromCache(stale, error) {
1660
1974
  if (this.cache === null) return {
1661
1975
  state: error ? "error" : "empty",
1662
- buckets: [],
1976
+ ...EMPTY_USAGE,
1663
1977
  fetchedAt: null,
1664
1978
  stale,
1665
1979
  ...error ? { error } : {}
1666
1980
  };
1667
1981
  return {
1668
- state: error ? "stale" : this.cache.buckets.length > 0 ? "ready" : "empty",
1669
- buckets: structuredClone(this.cache.buckets),
1982
+ state: error ? "stale" : this.cache.usage.buckets.length > 0 ? "ready" : "empty",
1983
+ ...structuredClone(this.cache.usage),
1670
1984
  fetchedAt: Math.floor(this.cache.fetchedAt / 1e3),
1671
1985
  stale,
1672
1986
  ...error ? { error } : {}
@@ -1684,15 +1998,32 @@ var UsageServiceError = class extends Error {
1684
1998
  }
1685
1999
  };
1686
2000
  function mapCodexUsage(value) {
2001
+ return parseCodexUsage(value).buckets;
2002
+ }
2003
+ function parseCodexUsage(value) {
1687
2004
  const data = record(value);
1688
- if (data === null) return [];
2005
+ if (data === null) return structuredClone(EMPTY_USAGE);
1689
2006
  const planType = typeof data.plan_type === "string" ? data.plan_type : null;
1690
- const result = [];
1691
- addBucket(result, "codex", "Codex", planType, data.rate_limit);
1692
- addBucket(result, "code-review", "Code review", planType, data.code_review_rate_limit);
1693
- 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
+ };
1694
2025
  }
1695
- function addBucket(result, id, name, planType, value) {
2026
+ function addBucket(result, usedIds, id, name, planType, value) {
1696
2027
  const source = record(value);
1697
2028
  if (source === null) return;
1698
2029
  const primary = mapWindow(source.primary_window);
@@ -1703,8 +2034,10 @@ function addBucket(result, id, name, planType, value) {
1703
2034
  name,
1704
2035
  planType,
1705
2036
  primary,
1706
- secondary
2037
+ secondary,
2038
+ windows: [primary, secondary].filter(isWindow)
1707
2039
  });
2040
+ usedIds.add(id);
1708
2041
  }
1709
2042
  function mapWindow(value) {
1710
2043
  const data = record(value);
@@ -1719,21 +2052,86 @@ function mapWindow(value) {
1719
2052
  resetsAt: reset !== void 0 && reset > 0 ? reset : null
1720
2053
  };
1721
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
+ }
1722
2090
  function record(value) {
1723
2091
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1724
2092
  }
2093
+ function boolean(value) {
2094
+ return typeof value === "boolean" ? value : null;
2095
+ }
1725
2096
  function numeric(value) {
1726
2097
  if (typeof value !== "number" && (typeof value !== "string" || value.trim() === "")) return void 0;
1727
2098
  const number = Number(value);
1728
2099
  return Number.isFinite(number) ? number : void 0;
1729
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
+ }
1730
2128
  function identityKey(credentials) {
1731
2129
  return credentials.accountId ?? credentials.email ?? credentials.planType ?? "signed-in";
1732
2130
  }
1733
2131
  //#endregion
1734
2132
  //#region src/host/routes.ts
1735
2133
  const MAX_BODY_BYTES = 64 * 1024;
1736
- function registerRoutes(ctx, oauth, usage) {
2134
+ function registerRoutes(ctx, oauth, usage, preferences) {
1737
2135
  const handler = async (request, response) => {
1738
2136
  const url = new URL(request.url ?? "/", "http://dsh.local");
1739
2137
  if (request.method === "GET" && url.pathname === `/api/dsh-chatgpt-subscription/status`) {
@@ -1742,7 +2140,8 @@ function registerRoutes(ctx, oauth, usage) {
1742
2140
  ok: true,
1743
2141
  value: {
1744
2142
  ...oauthStatus,
1745
- quota: await usage.status(oauthStatus.authenticated)
2143
+ quota: await usage.status(oauthStatus.authenticated),
2144
+ preferences: preferences.status()
1746
2145
  }
1747
2146
  });
1748
2147
  return;
@@ -1809,7 +2208,8 @@ function registerRoutes(ctx, oauth, usage) {
1809
2208
  ok: true,
1810
2209
  value: {
1811
2210
  ...oauthStatus,
1812
- quota: await usage.status(oauthStatus.authenticated)
2211
+ quota: await usage.status(oauthStatus.authenticated),
2212
+ preferences: preferences.status()
1813
2213
  }
1814
2214
  });
1815
2215
  return;
@@ -1827,13 +2227,22 @@ function registerRoutes(ctx, oauth, usage) {
1827
2227
  value: await usage.testConnection()
1828
2228
  });
1829
2229
  return;
2230
+ case `${ROUTE_PREFIX}/preferences/update`:
2231
+ json(response, {
2232
+ ok: true,
2233
+ value: await preferences.update(readPreferencesUpdate(body))
2234
+ });
2235
+ return;
1830
2236
  default: jsonError(response, 404, {
1831
2237
  code: "bad-request",
1832
2238
  message: "Route not found."
1833
2239
  });
1834
2240
  }
1835
2241
  } catch (error) {
1836
- 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");
1837
2246
  jsonError(response, statusFor(mapped), mapped);
1838
2247
  }
1839
2248
  };
@@ -1933,6 +2342,18 @@ function field(value, name) {
1933
2342
  const candidate = value[name];
1934
2343
  return typeof candidate === "string" && candidate !== "" ? candidate : null;
1935
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
+ }
1936
2357
  function json(response, envelope, status = 200) {
1937
2358
  response.writeHead(status, {
1938
2359
  "content-type": "application/json; charset=utf-8",
@@ -2190,26 +2611,77 @@ function createPlatformTokenStore(platform = process.platform) {
2190
2611
  throw new Error(`Unsupported platform ${platform}; dsh-chatgpt-subscription supports Windows and Linux.`);
2191
2612
  }
2192
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
2193
2650
  //#region src/index.ts
2194
2651
  const inject = [
2195
2652
  "webServer",
2196
2653
  "llm",
2197
2654
  "attachments",
2198
- "agents"
2655
+ "tools",
2656
+ "web",
2657
+ "settings",
2658
+ "loader"
2199
2659
  ];
2200
2660
  function apply(ctx) {
2201
2661
  const oauth = new OAuthService(createPlatformTokenStore(), { logger: ctx.logger });
2202
2662
  const usage = new UsageService(oauth);
2663
+ const preferences = registerPreferenceStore(ctx.settings);
2203
2664
  const adapter = new CodexChatGptAdapter(new ResponsesClient(oauth, ctx.attachments, {
2204
2665
  localRawImages: { baseUrl: localWebServerBaseUrl(ctx.webServer.host, ctx.webServer.port) },
2205
2666
  onGenerationFinished: () => usage.invalidate()
2206
2667
  }));
2207
2668
  ctx.effect(() => {
2208
- 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);
2209
2676
  const disposeAdapter = ctx.llm.registerAdapter([PROVIDER_ID], adapter);
2210
- 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();
2211
2681
  return () => {
2212
- disposeSubagentReportCompat();
2682
+ disposePreferenceWatch();
2683
+ disposeSearchProvider();
2684
+ disposeImageTool();
2213
2685
  disposeAdapter();
2214
2686
  disposeRoutes();
2215
2687
  oauth.dispose();
@@ -2220,4 +2692,4 @@ function localWebServerBaseUrl(host, port) {
2220
2692
  return `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}`;
2221
2693
  }
2222
2694
  //#endregion
2223
- 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 };