@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.
- package/CHANGELOG.md +11 -1
- package/README.md +17 -7
- package/lib/client.js +442 -28
- package/lib/client.js.map +1 -1
- package/lib/index.js +720 -248
- 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 +57 -3
- 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 +1 -2
- 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 +21 -2
- 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.`;
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
1951
|
+
const usage = parseCodexUsage(await response.json());
|
|
1638
1952
|
this.cache = {
|
|
1639
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1691
|
-
|
|
1692
|
-
addBucket(
|
|
1693
|
-
|
|
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 :
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
-
|
|
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 };
|