@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.
- package/CHANGELOG.md +9 -50
- package/README.md +17 -7
- package/lib/client.js +442 -28
- package/lib/client.js.map +1 -1
- package/lib/index.js +718 -247
- package/lib/types/client/CodexComposerQuota.d.ts +13 -0
- package/lib/types/client/CodexComposerQuota.d.ts.map +1 -0
- package/lib/types/client/CodexImageToolView.d.ts +10 -0
- package/lib/types/client/CodexImageToolView.d.ts.map +1 -0
- package/lib/types/client/CodexSubscriptionSection.d.ts.map +1 -1
- package/lib/types/client/api.d.ts +2 -1
- package/lib/types/client/api.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +168 -114
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/quota.d.ts +9 -0
- package/lib/types/client/quota.d.ts.map +1 -0
- package/lib/types/client/styles.d.ts.map +1 -1
- package/lib/types/compat.d.ts +7 -0
- package/lib/types/compat.d.ts.map +1 -1
- package/lib/types/host/codex-images.d.ts +9 -0
- package/lib/types/host/codex-images.d.ts.map +1 -0
- package/lib/types/host/codex-search.d.ts +11 -0
- package/lib/types/host/codex-search.d.ts.map +1 -0
- package/lib/types/host/model-catalog.d.ts.map +1 -1
- package/lib/types/host/preferences.d.ts +12 -0
- package/lib/types/host/preferences.d.ts.map +1 -0
- package/lib/types/host/responses-mapper.d.ts.map +1 -1
- package/lib/types/host/routes.d.ts +2 -1
- package/lib/types/host/routes.d.ts.map +1 -1
- package/lib/types/host/search-provider-switcher.d.ts +15 -0
- package/lib/types/host/search-provider-switcher.d.ts.map +1 -0
- package/lib/types/host/usage-service.d.ts +2 -1
- package/lib/types/host/usage-service.d.ts.map +1 -1
- package/lib/types/index.d.ts +3 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/shared/contracts.d.ts +40 -3
- package/lib/types/shared/contracts.d.ts.map +1 -1
- package/lib/types/shared/model-catalog.d.ts +81 -0
- package/lib/types/shared/model-catalog.d.ts.map +1 -0
- package/lib/types/shared/preferences.d.ts +7 -0
- package/lib/types/shared/preferences.d.ts.map +1 -0
- package/package.json +126 -107
- package/lib/types/host/subagent-report-scheduling-compat.d.ts +0 -21
- 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 =
|
|
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
|
-
|
|
31
|
-
|
|
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
|
|
135
|
+
return CODEX_MODEL_CATALOG.map((entry) => ({
|
|
61
136
|
provider: PROVIDER_ID,
|
|
62
|
-
id,
|
|
63
|
-
name:
|
|
64
|
-
inputModalities: [
|
|
137
|
+
id: entry.id,
|
|
138
|
+
name: entry.name,
|
|
139
|
+
inputModalities: [...entry.inputModalities]
|
|
65
140
|
}));
|
|
66
141
|
}
|
|
67
142
|
function resolveCodexModel(model) {
|
|
68
|
-
const
|
|
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: [
|
|
74
|
-
context: { contextWindow:
|
|
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:
|
|
153
|
+
efforts: efforts.map((effort) => ({
|
|
78
154
|
id: ReasoningEffortId(effort),
|
|
79
155
|
name: effort
|
|
80
156
|
})),
|
|
81
|
-
defaultEffort: ReasoningEffortId(
|
|
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/
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
168
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
1951
|
+
const usage = parseCodexUsage(await response.json());
|
|
1639
1952
|
this.cache = {
|
|
1640
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1692
|
-
|
|
1693
|
-
addBucket(
|
|
1694
|
-
|
|
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 :
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
-
|
|
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 };
|