@tnnevol/dsh-codex-auth 0.1.2-alpha.4 → 0.1.2-rc.1.2
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/compatibility.json +2 -2
- package/lib/{auth-DQ2F4vc6.js → auth-BdEwtK4R.js} +2 -2
- package/lib/{auth-BBBeI24-.d.ts → auth-DyHxRsSd.d.ts} +2 -2
- package/lib/auth-paths.d.ts +4 -6
- package/lib/auth-paths.js +4 -6
- package/lib/auth.d.ts +1 -1
- package/lib/auth.js +1 -1
- package/lib/client.js +205 -1238
- package/lib/index.d.ts +9 -10
- package/lib/index.js +250 -295
- package/lib/style.css +1 -1
- package/package.json +50 -55
package/lib/index.js
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
import { a as CODEX_PROVIDER, i as CODEX_AUTH_FILENAME, n as loginCodex, o as CodexCredentialStore, r as logoutCodex, s as codexAuthPath, t as codexAuthStatus } from "./auth-
|
|
2
|
-
import { CODEX_AUTH_LOGIN_PATH, CODEX_AUTH_LOGOUT_PATH,
|
|
3
|
-
import { APP_IDENTITY, createUserMessage, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
1
|
+
import { a as CODEX_PROVIDER, i as CODEX_AUTH_FILENAME, n as loginCodex, o as CodexCredentialStore, r as logoutCodex, s as codexAuthPath, t as codexAuthStatus } from "./auth-BdEwtK4R.js";
|
|
2
|
+
import { CODEX_AUTH_LOGIN_PATH, CODEX_AUTH_LOGOUT_PATH, CODEX_AUTH_STATUS_PATH, CODEX_GLOBAL_MODEL_PATH, CODEX_MODEL_REFRESH_PATH, CODEX_USAGE_PATH } from "./auth-paths.js";
|
|
4
3
|
import { createModels, defaultProviderAuthContext } from "@earendil-works/pi-ai";
|
|
5
4
|
import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex";
|
|
6
|
-
import { basename } from "node:path";
|
|
7
5
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
8
|
-
import
|
|
9
|
-
import { AttachmentId } from "@deepseek-ai/dsh-attachment";
|
|
10
|
-
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
6
|
+
import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
11
7
|
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
12
|
-
//#region src/usage.ts
|
|
8
|
+
//#region src/host/usage.ts
|
|
13
9
|
/** Read-only Codex account quota information for the local settings card. */
|
|
14
10
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
15
11
|
const CODEX_USAGE_TIMEOUT_MS = 1e4;
|
|
16
|
-
function record(value) {
|
|
12
|
+
function record$1(value) {
|
|
17
13
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
18
14
|
}
|
|
19
|
-
function number(value) {
|
|
15
|
+
function number$1(value) {
|
|
20
16
|
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
21
17
|
if (typeof value !== "string" || value.trim() === "") return void 0;
|
|
22
18
|
const parsed = Number(value);
|
|
@@ -25,7 +21,7 @@ function number(value) {
|
|
|
25
21
|
function boolean(value) {
|
|
26
22
|
return typeof value === "boolean" ? value : void 0;
|
|
27
23
|
}
|
|
28
|
-
function string(value) {
|
|
24
|
+
function string$1(value) {
|
|
29
25
|
if (typeof value === "string" && value.length > 0) return value;
|
|
30
26
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
31
27
|
}
|
|
@@ -33,14 +29,14 @@ function percentage(value) {
|
|
|
33
29
|
return Math.max(0, Math.min(100, value));
|
|
34
30
|
}
|
|
35
31
|
function window(value) {
|
|
36
|
-
const source = record(value);
|
|
32
|
+
const source = record$1(value);
|
|
37
33
|
if (source === void 0) return void 0;
|
|
38
34
|
const result = {};
|
|
39
|
-
const usedPercent = number(source["used_percent"]);
|
|
40
|
-
const explicitRemainingPercent = number(source["remaining_percent"]);
|
|
41
|
-
const limitWindowSeconds = number(source["limit_window_seconds"]);
|
|
42
|
-
const resetAfterSeconds = number(source["reset_after_seconds"]);
|
|
43
|
-
const resetAt = number(source["reset_at"]);
|
|
35
|
+
const usedPercent = number$1(source["used_percent"]);
|
|
36
|
+
const explicitRemainingPercent = number$1(source["remaining_percent"]);
|
|
37
|
+
const limitWindowSeconds = number$1(source["limit_window_seconds"]);
|
|
38
|
+
const resetAfterSeconds = number$1(source["reset_after_seconds"]);
|
|
39
|
+
const resetAt = number$1(source["reset_at"]);
|
|
44
40
|
if (explicitRemainingPercent !== void 0) result.remainingPercent = percentage(explicitRemainingPercent);
|
|
45
41
|
else if (usedPercent !== void 0) result.remainingPercent = percentage(100 - usedPercent);
|
|
46
42
|
if (limitWindowSeconds !== void 0) result.limitWindowSeconds = limitWindowSeconds;
|
|
@@ -49,12 +45,12 @@ function window(value) {
|
|
|
49
45
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
50
46
|
}
|
|
51
47
|
function credits(value) {
|
|
52
|
-
const source = record(value);
|
|
48
|
+
const source = record$1(value);
|
|
53
49
|
if (source === void 0) return void 0;
|
|
54
50
|
const result = {};
|
|
55
51
|
const hasCredits = boolean(source["has_credits"]);
|
|
56
52
|
const unlimited = boolean(source["unlimited"]);
|
|
57
|
-
const balance = string(source["balance"]);
|
|
53
|
+
const balance = string$1(source["balance"]);
|
|
58
54
|
if (hasCredits !== void 0) result.hasCredits = hasCredits;
|
|
59
55
|
if (unlimited !== void 0) result.unlimited = unlimited;
|
|
60
56
|
if (balance !== void 0) result.balance = balance;
|
|
@@ -62,11 +58,11 @@ function credits(value) {
|
|
|
62
58
|
}
|
|
63
59
|
/** Normalize the evolving private WHAM response into a small UI-safe shape. */
|
|
64
60
|
function normalizeCodexUsagePayload(value) {
|
|
65
|
-
const source = record(value);
|
|
61
|
+
const source = record$1(value);
|
|
66
62
|
if (source === void 0) throw new Error("Codex usage response was not an object");
|
|
67
|
-
const rateLimit = record(source["rate_limit"]);
|
|
63
|
+
const rateLimit = record$1(source["rate_limit"]);
|
|
68
64
|
const result = {};
|
|
69
|
-
const planType = string(source["plan_type"]);
|
|
65
|
+
const planType = string$1(source["plan_type"]);
|
|
70
66
|
const allowed = boolean(rateLimit?.["allowed"]);
|
|
71
67
|
const limitReached = boolean(rateLimit?.["limit_reached"]);
|
|
72
68
|
const primaryWindow = window(rateLimit?.["primary_window"]);
|
|
@@ -80,10 +76,10 @@ function normalizeCodexUsagePayload(value) {
|
|
|
80
76
|
if (quota !== void 0) result.credits = quota;
|
|
81
77
|
return result;
|
|
82
78
|
}
|
|
83
|
-
function accessToken(auth) {
|
|
79
|
+
function accessToken$1(auth) {
|
|
84
80
|
return typeof auth?.apiKey === "string" && auth.apiKey.length > 0 ? auth.apiKey : void 0;
|
|
85
81
|
}
|
|
86
|
-
function accountId(credential) {
|
|
82
|
+
function accountId$1(credential) {
|
|
87
83
|
if (credential?.type !== "oauth") return void 0;
|
|
88
84
|
return typeof credential.accountId === "string" && credential.accountId.length > 0 ? credential.accountId : void 0;
|
|
89
85
|
}
|
|
@@ -108,9 +104,9 @@ var CodexUsageService = class {
|
|
|
108
104
|
}
|
|
109
105
|
}
|
|
110
106
|
async readNow() {
|
|
111
|
-
const token = accessToken((await this.models.getAuth(CODEX_PROVIDER))?.auth);
|
|
107
|
+
const token = accessToken$1((await this.models.getAuth(CODEX_PROVIDER))?.auth);
|
|
112
108
|
if (token === void 0) return void 0;
|
|
113
|
-
const account = accountId(await this.store.read(CODEX_PROVIDER));
|
|
109
|
+
const account = accountId$1(await this.store.read(CODEX_PROVIDER));
|
|
114
110
|
if (account === void 0) return void 0;
|
|
115
111
|
const controller = new AbortController();
|
|
116
112
|
const timer = setTimeout(() => {
|
|
@@ -135,36 +131,198 @@ var CodexUsageService = class {
|
|
|
135
131
|
}
|
|
136
132
|
};
|
|
137
133
|
//#endregion
|
|
138
|
-
//#region src/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
134
|
+
//#region src/host/model-refresh.ts
|
|
135
|
+
/**
|
|
136
|
+
* Refresh the OpenAI Codex route's model catalog from the signed-in
|
|
137
|
+
* ChatGPT account instead of the pi-ai installed static table.
|
|
138
|
+
*
|
|
139
|
+
* pi-ai ships a build-time snapshot of Codex models (`gpt-5.3-codex-spark` …
|
|
140
|
+
* `gpt-5.6-*`). The ChatGPT backend lists what the *signed-in account* can
|
|
141
|
+
* actually use today (`chatgpt.com/backend-api/codex/models`), which drifts
|
|
142
|
+
* from that snapshot — new models appear (e.g. `gpt-6-astra`) and retired
|
|
143
|
+
* ones disappear. The harness model catalog, the in-conversation model
|
|
144
|
+
* picker, and the plugin's own global-model Cascader all read the route's
|
|
145
|
+
* model list from llm-pi-ai, which resolves the profile's `models` array
|
|
146
|
+
* over the pi-ai catalog. Writing that array therefore updates every
|
|
147
|
+
* selector at once, and writing it in the user settings layer *replaces*
|
|
148
|
+
* the composition's static list (DSH settings merge arrays wholesale).
|
|
149
|
+
*
|
|
150
|
+
* Only the account's user-visible model rows are written: internal service
|
|
151
|
+
* rows (`codex-auto-review`, `gpt-reserve`, anything the backend marks
|
|
152
|
+
* `visibility: hide`) are skipped so pickers keep showing models a human
|
|
153
|
+
* would choose.
|
|
154
|
+
*/
|
|
155
|
+
const CODEX_MODELS_URL = "https://chatgpt.com/backend-api/codex/models";
|
|
156
|
+
const CODEX_MODELS_CLIENT_VERSION = "1.0.0";
|
|
157
|
+
const CODEX_MODELS_TIMEOUT_MS = 15e3;
|
|
158
|
+
/**
|
|
159
|
+
* Thinking levels DSH's llm-pi-ai profile vocabulary can express. The ChatGPT
|
|
160
|
+
* backend may advertise `ultra` (and more in future) which this vocabulary
|
|
161
|
+
* cannot carry; such levels are dropped from the written profile, with the
|
|
162
|
+
* model's remaining levels preserved.
|
|
163
|
+
*/
|
|
164
|
+
const LLM_PI_AI_THINKING_LEVELS = /* @__PURE__ */ new Set([
|
|
165
|
+
"minimal",
|
|
166
|
+
"low",
|
|
167
|
+
"medium",
|
|
168
|
+
"high",
|
|
169
|
+
"xhigh",
|
|
170
|
+
"max"
|
|
171
|
+
]);
|
|
172
|
+
/** Rows the account endpoint returns for machinery, not human selection. */
|
|
173
|
+
const INTERNAL_CODEX_MODEL_IDS = /* @__PURE__ */ new Set(["codex-auto-review", "gpt-reserve"]);
|
|
174
|
+
function record(value) {
|
|
175
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
176
|
+
}
|
|
177
|
+
function accessToken(auth) {
|
|
178
|
+
return typeof auth?.apiKey === "string" && auth.apiKey.length > 0 ? auth.apiKey : void 0;
|
|
179
|
+
}
|
|
180
|
+
function accountId(credential) {
|
|
181
|
+
const value = record(credential)?.["accountId"];
|
|
182
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
183
|
+
}
|
|
184
|
+
function string(value) {
|
|
185
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
186
|
+
}
|
|
187
|
+
function number(value) {
|
|
188
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
189
|
+
}
|
|
190
|
+
/** Resolve the output-cap estimate for one account row. */
|
|
191
|
+
function outputCap(model) {
|
|
192
|
+
const context = number(model.context_window);
|
|
193
|
+
if (context === void 0) return 128e3;
|
|
194
|
+
return Math.min(128e3, Math.max(32768, context));
|
|
195
|
+
}
|
|
196
|
+
/** Map account modalities onto pi-ai's `input` vocabulary (text/image only). */
|
|
197
|
+
function inputModalities(model) {
|
|
198
|
+
const raw = model.input_modalities;
|
|
199
|
+
if (!Array.isArray(raw) || raw.length === 0) return ["text"];
|
|
200
|
+
const allowed = /* @__PURE__ */ new Set(["text", "image"]);
|
|
201
|
+
const values = raw.filter((value) => typeof value === "string" && allowed.has(value));
|
|
202
|
+
return values.length > 0 ? values : ["text"];
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Normalize the account's reasoning levels into llm-pi-ai's
|
|
206
|
+
* `reasoningEfforts` dict (level → wire value) or `false`.
|
|
207
|
+
*
|
|
208
|
+
* The backend effort names double as the wire value the responses API
|
|
209
|
+
* expects, so each supported level maps to itself. `ultra` and any future
|
|
210
|
+
* level outside llm-pi-ai's vocabulary is dropped; a model whose only
|
|
211
|
+
* declared level falls outside the vocabulary becomes `false` (no thinking).
|
|
212
|
+
*/
|
|
213
|
+
function normalizeReasoningEfforts(model) {
|
|
214
|
+
const rows = model.supported_reasoning_levels;
|
|
215
|
+
if (!Array.isArray(rows) || rows.length === 0) return false;
|
|
216
|
+
const efforts = {};
|
|
217
|
+
for (const row of rows) {
|
|
218
|
+
const effort = string(row.effort);
|
|
219
|
+
if (effort === void 0 || !LLM_PI_AI_THINKING_LEVELS.has(effort)) continue;
|
|
220
|
+
efforts[effort] = effort;
|
|
221
|
+
}
|
|
222
|
+
return Object.keys(efforts).length > 0 ? efforts : false;
|
|
223
|
+
}
|
|
224
|
+
function isSelectable(model) {
|
|
225
|
+
if (model.slug === void 0) return false;
|
|
226
|
+
if (INTERNAL_CODEX_MODEL_IDS.has(model.slug)) return false;
|
|
227
|
+
return model.visibility !== "hide";
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Normalize one raw account model row into a profile entry, or `undefined`
|
|
231
|
+
* when the row is not human-selectable or lacks the identity/context facts a
|
|
232
|
+
* profile entry requires.
|
|
233
|
+
*/
|
|
234
|
+
function normalizeCodexModel(model) {
|
|
235
|
+
const id = string(model.slug);
|
|
236
|
+
if (id === void 0 || !isSelectable(model)) return void 0;
|
|
237
|
+
const name = string(model.display_name) ?? id;
|
|
238
|
+
const context = number(model.context_window);
|
|
239
|
+
if (context === void 0) return void 0;
|
|
240
|
+
return {
|
|
241
|
+
id,
|
|
242
|
+
name,
|
|
243
|
+
contextWindow: context,
|
|
244
|
+
maxTokens: outputCap(model),
|
|
245
|
+
input: inputModalities(model),
|
|
246
|
+
reasoningEfforts: normalizeReasoningEfforts(model)
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Normalize a full account catalog response into the list of profile model
|
|
251
|
+
* entries and the skipped row slugs. The result is ready to write to the
|
|
252
|
+
* llm-pi-ai `providers.openai-codex.models` array.
|
|
253
|
+
*/
|
|
254
|
+
function normalizeCodexCatalog(payload) {
|
|
255
|
+
const rows = Array.isArray(payload.models) ? payload.models : [];
|
|
256
|
+
const models = [];
|
|
257
|
+
const skipped = [];
|
|
258
|
+
for (const row of rows) {
|
|
259
|
+
const entry = normalizeCodexModel(row);
|
|
260
|
+
if (entry === void 0) {
|
|
261
|
+
const slug = string(row.slug);
|
|
262
|
+
if (slug !== void 0 && row.visibility === "hide") skipped.push(slug);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
models.push(entry);
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
models,
|
|
269
|
+
skipped
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/** Read the signed-in account's current Codex model catalog. */
|
|
273
|
+
async function fetchCodexModelCatalog(store, fetchImpl = fetch) {
|
|
274
|
+
const models = createModels({ credentials: store });
|
|
275
|
+
models.setProvider(openaiCodexProvider());
|
|
276
|
+
const token = accessToken((await models.getAuth(CODEX_PROVIDER))?.auth);
|
|
277
|
+
if (token === void 0) throw new Error("codex model refresh requires a signed-in ChatGPT account");
|
|
278
|
+
const account = accountId(await store.read(CODEX_PROVIDER));
|
|
279
|
+
if (account === void 0) throw new Error("codex model refresh requires the ChatGPT account id");
|
|
280
|
+
const controller = new AbortController();
|
|
281
|
+
const timer = setTimeout(() => {
|
|
282
|
+
controller.abort();
|
|
283
|
+
}, CODEX_MODELS_TIMEOUT_MS);
|
|
284
|
+
try {
|
|
285
|
+
const url = new URL(CODEX_MODELS_URL);
|
|
286
|
+
url.searchParams.set("client_version", CODEX_MODELS_CLIENT_VERSION);
|
|
287
|
+
const response = await fetchImpl(url, {
|
|
288
|
+
method: "GET",
|
|
289
|
+
headers: {
|
|
290
|
+
accept: "application/json",
|
|
291
|
+
authorization: `Bearer ${token}`,
|
|
292
|
+
"ChatGPT-Account-Id": account,
|
|
293
|
+
"user-agent": "dsh-codex-auth-plugin"
|
|
294
|
+
},
|
|
295
|
+
signal: controller.signal
|
|
296
|
+
});
|
|
297
|
+
if (!response.ok) throw new Error(`Codex model catalog request failed with status ${String(response.status)}`);
|
|
298
|
+
const parsed = record(await response.json());
|
|
299
|
+
if (parsed === void 0 || !Array.isArray(parsed["models"])) throw new Error("Codex model catalog response was not an object with a models array");
|
|
300
|
+
return { models: parsed["models"] };
|
|
301
|
+
} finally {
|
|
302
|
+
clearTimeout(timer);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Fetch and normalize the account catalog. Returns the ready-to-write model
|
|
307
|
+
* list plus skipped row slugs; throws a plain `Error` when the account is
|
|
308
|
+
* signed out, the backend request fails, or the payload is unusable.
|
|
309
|
+
*/
|
|
310
|
+
async function refreshCodexModelCatalog(store, fetchImpl = fetch) {
|
|
311
|
+
const { models, skipped } = normalizeCodexCatalog(await fetchCodexModelCatalog(store, fetchImpl));
|
|
312
|
+
if (models.length === 0) throw new Error("Codex model catalog returned no selectable models");
|
|
149
313
|
return {
|
|
150
|
-
|
|
151
|
-
|
|
314
|
+
models,
|
|
315
|
+
skipped
|
|
152
316
|
};
|
|
153
317
|
}
|
|
154
318
|
//#endregion
|
|
155
|
-
//#region src/auth-routes.ts
|
|
319
|
+
//#region src/host/auth-routes.ts
|
|
156
320
|
const CODEX_AUTH_URL_TIMEOUT_MS = 3e4;
|
|
157
321
|
const REMOTE_WEB_ORIGIN_NOT_TRUSTED = "remote-web-origin-not-trusted";
|
|
158
322
|
const CODEX_USAGE_UNAVAILABLE = "codex-usage-unavailable";
|
|
159
|
-
const CODEX_SETTINGS_INVALID = "codex-settings-invalid";
|
|
160
323
|
const CODEX_GLOBAL_MODEL_INVALID = "codex-global-model-invalid";
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
...status,
|
|
165
|
-
dshVersion: APP_IDENTITY.version
|
|
166
|
-
};
|
|
167
|
-
}
|
|
324
|
+
const CODEX_MODEL_REFRESH_FAILED = "codex-model-refresh-failed";
|
|
325
|
+
const CODEX_REQUEST_BODY_LIMIT = 8192;
|
|
168
326
|
function signedInStatus(expiresAt) {
|
|
169
327
|
return expiresAt === void 0 ? { status: "signed-in" } : {
|
|
170
328
|
status: "signed-in",
|
|
@@ -370,7 +528,7 @@ function readJsonBody(req) {
|
|
|
370
528
|
req.on("data", (chunk) => {
|
|
371
529
|
if (finished) return;
|
|
372
530
|
size += Buffer.byteLength(chunk);
|
|
373
|
-
if (size >
|
|
531
|
+
if (size > CODEX_REQUEST_BODY_LIMIT) {
|
|
374
532
|
finished = true;
|
|
375
533
|
reject(/* @__PURE__ */ new Error("request body is too large"));
|
|
376
534
|
return;
|
|
@@ -393,12 +551,6 @@ function readJsonBody(req) {
|
|
|
393
551
|
});
|
|
394
552
|
});
|
|
395
553
|
}
|
|
396
|
-
function decodeSettingsWrite(value) {
|
|
397
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
398
|
-
const record = value;
|
|
399
|
-
if (typeof record["enableImageTool"] !== "boolean" || typeof record["enableImageUpload"] !== "boolean") return;
|
|
400
|
-
return decodeCodexAuthSettings(record);
|
|
401
|
-
}
|
|
402
554
|
function codexGlobalModelOf(selection) {
|
|
403
555
|
if (selection.provider !== "openai-codex") return void 0;
|
|
404
556
|
return {
|
|
@@ -416,8 +568,13 @@ function decodeGlobalModelWrite(value) {
|
|
|
416
568
|
...typeof record.reasoningEffort === "string" ? { reasoningEffort: record.reasoningEffort } : {}
|
|
417
569
|
};
|
|
418
570
|
}
|
|
419
|
-
/**
|
|
420
|
-
|
|
571
|
+
/**
|
|
572
|
+
* Register the endpoint that refreshes the OpenAI Codex route model catalog
|
|
573
|
+
* from the signed-in ChatGPT account and writes it into the llm-pi-ai user
|
|
574
|
+
* settings layer.
|
|
575
|
+
*/
|
|
576
|
+
function registerCodexModelRefreshRoute(ctx, store, settings) {
|
|
577
|
+
const llmPiAiNamespace = "llm-pi-ai";
|
|
421
578
|
ctx.effect(() => {
|
|
422
579
|
const authorize = (req, res) => {
|
|
423
580
|
if (trustedRequest(req)) return true;
|
|
@@ -426,31 +583,31 @@ function registerCodexGlobalModelRoute(ctx, defaultModel, llm) {
|
|
|
426
583
|
};
|
|
427
584
|
return ctx.webServer.register({
|
|
428
585
|
kind: "exact",
|
|
429
|
-
path:
|
|
586
|
+
path: CODEX_MODEL_REFRESH_PATH,
|
|
430
587
|
handler: async (req, res) => {
|
|
431
|
-
if (req.method !== "
|
|
588
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
432
589
|
if (!authorize(req, res)) return;
|
|
433
|
-
if (req.method === "GET") return json(res, 200, { globalModel: codexGlobalModelOf(defaultModel.currentSelection()) });
|
|
434
590
|
try {
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
591
|
+
const { models, skipped } = await refreshCodexModelCatalog(store);
|
|
592
|
+
await settings.update(llmPiAiNamespace, { providers: { [CODEX_PROVIDER]: { models } } });
|
|
593
|
+
json(res, 200, {
|
|
594
|
+
ok: true,
|
|
595
|
+
models,
|
|
596
|
+
skipped
|
|
441
597
|
});
|
|
442
|
-
if (resolved.provider !== "openai-codex") return json(res, 400, { error: CODEX_GLOBAL_MODEL_INVALID });
|
|
443
|
-
await defaultModel.saveSelection(resolved);
|
|
444
|
-
return json(res, 200, { globalModel: codexGlobalModelOf(defaultModel.currentSelection()) });
|
|
445
598
|
} catch (error) {
|
|
446
|
-
|
|
599
|
+
const message = safeMessage(error);
|
|
600
|
+
json(res, 502, {
|
|
601
|
+
error: CODEX_MODEL_REFRESH_FAILED,
|
|
602
|
+
message
|
|
603
|
+
});
|
|
447
604
|
}
|
|
448
605
|
}
|
|
449
606
|
});
|
|
450
|
-
}, "dsh-codex-auth-plugin:
|
|
607
|
+
}, "dsh-codex-auth-plugin: Codex model catalog refresh route");
|
|
451
608
|
}
|
|
452
|
-
/** Register the plugin-owned
|
|
453
|
-
function
|
|
609
|
+
/** Register the plugin-owned bridge to DSH alpha.4's shared Agent default model. */
|
|
610
|
+
function registerCodexGlobalModelRoute(ctx, defaultModel, llm) {
|
|
454
611
|
ctx.effect(() => {
|
|
455
612
|
const authorize = (req, res) => {
|
|
456
613
|
if (trustedRequest(req)) return true;
|
|
@@ -459,22 +616,28 @@ function registerCodexSettingsRoute(ctx, settings) {
|
|
|
459
616
|
};
|
|
460
617
|
return ctx.webServer.register({
|
|
461
618
|
kind: "exact",
|
|
462
|
-
path:
|
|
619
|
+
path: CODEX_GLOBAL_MODEL_PATH,
|
|
463
620
|
handler: async (req, res) => {
|
|
464
621
|
if (req.method !== "GET" && req.method !== "PUT") return json(res, 405, { error: "method not allowed" });
|
|
465
622
|
if (!authorize(req, res)) return;
|
|
466
|
-
if (req.method === "GET") return json(res, 200,
|
|
623
|
+
if (req.method === "GET") return json(res, 200, { globalModel: codexGlobalModelOf(defaultModel.currentSelection()) });
|
|
467
624
|
try {
|
|
468
|
-
const next =
|
|
469
|
-
if (next === void 0) return json(res, 400, { error:
|
|
470
|
-
await
|
|
471
|
-
|
|
625
|
+
const next = decodeGlobalModelWrite(await readJsonBody(req));
|
|
626
|
+
if (next === void 0) return json(res, 400, { error: CODEX_GLOBAL_MODEL_INVALID });
|
|
627
|
+
const resolved = await llm.resolveCallConfig({
|
|
628
|
+
provider: CODEX_PROVIDER,
|
|
629
|
+
model: next.model,
|
|
630
|
+
...next.reasoningEffort === void 0 ? {} : { reasoningEffort: next.reasoningEffort }
|
|
631
|
+
});
|
|
632
|
+
if (resolved.provider !== "openai-codex") return json(res, 400, { error: CODEX_GLOBAL_MODEL_INVALID });
|
|
633
|
+
await defaultModel.saveSelection(resolved);
|
|
634
|
+
return json(res, 200, { globalModel: codexGlobalModelOf(defaultModel.currentSelection()) });
|
|
472
635
|
} catch (error) {
|
|
473
|
-
json(res, 400, { error: safeMessage(error) });
|
|
636
|
+
return json(res, 400, { error: safeMessage(error) });
|
|
474
637
|
}
|
|
475
638
|
}
|
|
476
639
|
});
|
|
477
|
-
}, "dsh-codex-auth-plugin:
|
|
640
|
+
}, "dsh-codex-auth-plugin: global Codex model route");
|
|
478
641
|
}
|
|
479
642
|
/** Register the auth endpoints when the DSH Web server is available. */
|
|
480
643
|
function registerCodexAuthRoutes(ctx, store, mirror) {
|
|
@@ -494,7 +657,7 @@ function registerCodexAuthRoutes(ctx, store, mirror) {
|
|
|
494
657
|
if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
495
658
|
if (!authorize(req, res)) return;
|
|
496
659
|
try {
|
|
497
|
-
json(res, 200,
|
|
660
|
+
json(res, 200, await auth.status());
|
|
498
661
|
} catch (error) {
|
|
499
662
|
json(res, 500, { error: safeMessage(error) });
|
|
500
663
|
}
|
|
@@ -550,7 +713,7 @@ function registerCodexAuthRoutes(ctx, store, mirror) {
|
|
|
550
713
|
}, "dsh-codex-auth-plugin: Web OAuth routes");
|
|
551
714
|
}
|
|
552
715
|
//#endregion
|
|
553
|
-
//#region src/credential-mirror.ts
|
|
716
|
+
//#region src/host/credential-mirror.ts
|
|
554
717
|
/** Bridge the plugin-owned OAuth credential into dsh's generic LLM seam. */
|
|
555
718
|
/** Credential reference exposed to the official `llm-pi-ai` model settings UI. */
|
|
556
719
|
const CODEX_API_KEY_ENV = "OPENAI_CODEX_AUTH_TOKEN";
|
|
@@ -600,174 +763,7 @@ var CodexCredentialMirror = class {
|
|
|
600
763
|
}
|
|
601
764
|
};
|
|
602
765
|
//#endregion
|
|
603
|
-
//#region src/
|
|
604
|
-
/** Host settings registration that makes the standalone auth card discoverable. */
|
|
605
|
-
/** Branded namespace used by the Host settings service. */
|
|
606
|
-
const CODEX_AUTH_SETTINGS_NS = CODEX_AUTH_SETTINGS_NAMESPACE;
|
|
607
|
-
/** Settings schema for the plugin card and its optional image capability. */
|
|
608
|
-
const CodexAuthSettingsSchema = z.object({
|
|
609
|
-
enableImageTool: z.boolean().default(DEFAULT_CODEX_AUTH_SETTINGS.enableImageTool),
|
|
610
|
-
enableImageUpload: z.boolean().default(DEFAULT_CODEX_AUTH_SETTINGS.enableImageUpload)
|
|
611
|
-
});
|
|
612
|
-
//#endregion
|
|
613
|
-
//#region src/view-image.ts
|
|
614
|
-
/** Optional Codex image-reading tool, modeled on DSH's durable attachment seam. */
|
|
615
|
-
/** Stable name of the optional image-recognition tool. */
|
|
616
|
-
const VIEW_IMAGE_TOOL_NAME = "view_image";
|
|
617
|
-
function imageRefOf(image) {
|
|
618
|
-
return {
|
|
619
|
-
attachmentId: AttachmentId(image.attachmentId),
|
|
620
|
-
mediaType: image.mediaType,
|
|
621
|
-
bytes: image.bytes,
|
|
622
|
-
width: image.width,
|
|
623
|
-
height: image.height,
|
|
624
|
-
...image.name === void 0 ? {} : { name: image.name }
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
function contentOf(value) {
|
|
628
|
-
return [{
|
|
629
|
-
type: "text",
|
|
630
|
-
text: `<source>${value.source}</source>\n<image>${value.image.mediaType}, ${value.image.width}x${value.image.height} px, ${value.image.bytes} bytes</image>`
|
|
631
|
-
}, {
|
|
632
|
-
type: "image",
|
|
633
|
-
attachment: imageRefOf(value.image)
|
|
634
|
-
}];
|
|
635
|
-
}
|
|
636
|
-
function mediaTypeOf(data) {
|
|
637
|
-
if (data.length >= 8 && data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71 && data[4] === 13 && data[5] === 10 && data[6] === 26 && data[7] === 10) return "image/png";
|
|
638
|
-
if (data.length >= 3 && data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
|
|
639
|
-
if (data.length >= 6) {
|
|
640
|
-
const signature = String.fromCharCode(...data.subarray(0, 6));
|
|
641
|
-
if (signature === "GIF87a" || signature === "GIF89a") return "image/gif";
|
|
642
|
-
}
|
|
643
|
-
if (data.length >= 12 && String.fromCharCode(...data.subarray(0, 4)) === "RIFF" && String.fromCharCode(...data.subarray(8, 12)) === "WEBP") return "image/webp";
|
|
644
|
-
}
|
|
645
|
-
async function assertImageCapable(ctx, exec, source) {
|
|
646
|
-
const routed = exec.agent?.session.requestHeader()?.config;
|
|
647
|
-
const provider = routed?.provider ?? exec.agent?.options.provider;
|
|
648
|
-
const model = routed?.model ?? exec.agent?.options.model;
|
|
649
|
-
if (provider === void 0 || model === void 0) throw new Error(`cannot view ${JSON.stringify(source)}: the current model route is unavailable`);
|
|
650
|
-
const info = await ctx.llm.resolveModelInfo(provider, model, exec.signal);
|
|
651
|
-
if (info.inputModalities === void 0 || !info.inputModalities.includes("image")) throw new Error(`cannot view ${JSON.stringify(source)}: model "${model}" does not declare image input`);
|
|
652
|
-
}
|
|
653
|
-
/** Register a local-file image tool in the current DSH tool scope. */
|
|
654
|
-
function viewImageTool(ctx) {
|
|
655
|
-
return defineTool({
|
|
656
|
-
name: VIEW_IMAGE_TOOL_NAME,
|
|
657
|
-
description: "View a local PNG, JPEG, WebP, or GIF image and return it to an image-capable model.",
|
|
658
|
-
parameters: { source: {
|
|
659
|
-
type: "string",
|
|
660
|
-
required: true,
|
|
661
|
-
description: "Absolute or workspace-relative local image path."
|
|
662
|
-
} },
|
|
663
|
-
output: {
|
|
664
|
-
schema: {
|
|
665
|
-
type: "object",
|
|
666
|
-
additionalProperties: false,
|
|
667
|
-
properties: {
|
|
668
|
-
source: {
|
|
669
|
-
type: "string",
|
|
670
|
-
required: true
|
|
671
|
-
},
|
|
672
|
-
image: {
|
|
673
|
-
type: "object",
|
|
674
|
-
required: true,
|
|
675
|
-
additionalProperties: false,
|
|
676
|
-
properties: {
|
|
677
|
-
attachmentId: {
|
|
678
|
-
type: "string",
|
|
679
|
-
required: true
|
|
680
|
-
},
|
|
681
|
-
mediaType: {
|
|
682
|
-
type: "string",
|
|
683
|
-
required: true,
|
|
684
|
-
enum: [
|
|
685
|
-
"image/png",
|
|
686
|
-
"image/jpeg",
|
|
687
|
-
"image/webp",
|
|
688
|
-
"image/gif"
|
|
689
|
-
]
|
|
690
|
-
},
|
|
691
|
-
bytes: {
|
|
692
|
-
type: "integer",
|
|
693
|
-
required: true
|
|
694
|
-
},
|
|
695
|
-
width: {
|
|
696
|
-
type: "integer",
|
|
697
|
-
required: true
|
|
698
|
-
},
|
|
699
|
-
height: {
|
|
700
|
-
type: "integer",
|
|
701
|
-
required: true
|
|
702
|
-
},
|
|
703
|
-
name: { type: "string" }
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
},
|
|
708
|
-
render: (_args, value) => contentOf(value)
|
|
709
|
-
},
|
|
710
|
-
isConcurrencySafe: () => true,
|
|
711
|
-
async execute(args, exec) {
|
|
712
|
-
const source = args.source.trim();
|
|
713
|
-
if (source.length === 0) throw new Error("view_image source must not be empty");
|
|
714
|
-
await assertImageCapable(ctx, exec, source);
|
|
715
|
-
const attachments = ctx.attachments;
|
|
716
|
-
const maxBytes = Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
|
|
717
|
-
const cwd = exec.agent?.session.header.cwd;
|
|
718
|
-
const target = await ctx.fs.resolve(source, {
|
|
719
|
-
...cwd === void 0 ? {} : { cwd },
|
|
720
|
-
signal: exec.signal
|
|
721
|
-
});
|
|
722
|
-
const info = await ctx.fs.stat(target, exec.signal);
|
|
723
|
-
if (info === void 0) throw new Error(`image path does not exist: ${source}`);
|
|
724
|
-
if (info.type !== "file") throw new Error(`image path is not a regular file: ${source}`);
|
|
725
|
-
const data = await ctx.fs.readBytes(target, exec.signal, maxBytes);
|
|
726
|
-
ctx.emit("fs/observed", target, {
|
|
727
|
-
kind: "present",
|
|
728
|
-
version: info.version
|
|
729
|
-
}, exec);
|
|
730
|
-
const mediaType = mediaTypeOf(data);
|
|
731
|
-
if (mediaType === void 0) throw new Error("view_image supports PNG, JPEG, WebP, and GIF image bytes");
|
|
732
|
-
if (!attachments.imageLimits.mediaTypes.includes(mediaType)) throw new Error(`${mediaType} images are disabled by this deployment`);
|
|
733
|
-
const name = basename(target.displayPath);
|
|
734
|
-
const image = {
|
|
735
|
-
data,
|
|
736
|
-
mediaType,
|
|
737
|
-
...name.length === 0 ? {} : { name }
|
|
738
|
-
};
|
|
739
|
-
await attachments.validateImage(image);
|
|
740
|
-
const ref = await attachments.saveImage(image);
|
|
741
|
-
const value = {
|
|
742
|
-
source: target.displayPath,
|
|
743
|
-
image: {
|
|
744
|
-
attachmentId: ref.attachmentId,
|
|
745
|
-
mediaType: ref.mediaType,
|
|
746
|
-
bytes: ref.bytes,
|
|
747
|
-
width: ref.width,
|
|
748
|
-
height: ref.height,
|
|
749
|
-
...ref.name === void 0 ? {} : { name: ref.name }
|
|
750
|
-
}
|
|
751
|
-
};
|
|
752
|
-
if (exec.parent !== void 0) exec.deferContext(createUserMessage({
|
|
753
|
-
content: contentOf(value),
|
|
754
|
-
source: {
|
|
755
|
-
kind: "plugin",
|
|
756
|
-
plugin: "@tnnevol/dsh-codex-auth"
|
|
757
|
-
}
|
|
758
|
-
}));
|
|
759
|
-
return value;
|
|
760
|
-
},
|
|
761
|
-
presentCall: (args) => ({
|
|
762
|
-
card: "generic",
|
|
763
|
-
title: `View image ${args.source}`,
|
|
764
|
-
kind: "read",
|
|
765
|
-
locations: [{ path: args.source }]
|
|
766
|
-
})
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
|
-
//#endregion
|
|
770
|
-
//#region src/adapter.ts
|
|
766
|
+
//#region src/host/adapter.ts
|
|
771
767
|
/** OpenAI Codex adapter assembled from dsh's public pi-ai extension seam. */
|
|
772
768
|
/** Keep the Codex stream open while the provider is still producing output. */
|
|
773
769
|
const CODEX_STREAM_IDLE_TIMEOUT_MS = 3e5;
|
|
@@ -830,7 +826,7 @@ function createCodexAdapter(credentials, resolveAttachments) {
|
|
|
830
826
|
//#region src/index.ts
|
|
831
827
|
/** Stable Host bundle name. */
|
|
832
828
|
const name = "dsh-codex-auth-plugin";
|
|
833
|
-
/** Host services required by the routes
|
|
829
|
+
/** Host services required by the routes and credential mirror. */
|
|
834
830
|
const inject = [
|
|
835
831
|
"webServer",
|
|
836
832
|
"settings",
|
|
@@ -839,7 +835,6 @@ const inject = [
|
|
|
839
835
|
"llm"
|
|
840
836
|
];
|
|
841
837
|
function apply(ctx) {
|
|
842
|
-
const settings = ctx.settings.register(CODEX_AUTH_SETTINGS_NS, CodexAuthSettingsSchema);
|
|
843
838
|
const store = new CodexCredentialStore();
|
|
844
839
|
const mirror = new CodexCredentialMirror(ctx.credentials, store);
|
|
845
840
|
const syncMirror = () => {
|
|
@@ -855,48 +850,8 @@ function apply(ctx) {
|
|
|
855
850
|
};
|
|
856
851
|
}, "dsh-codex-auth: credential mirror");
|
|
857
852
|
registerCodexAuthRoutes(ctx, store, mirror);
|
|
858
|
-
|
|
853
|
+
registerCodexModelRefreshRoute(ctx, store, { update: (ns, patch) => ctx.settings.update(ns, patch) });
|
|
859
854
|
registerCodexGlobalModelRoute(ctx, ctx.agentDefaultModel, ctx.llm);
|
|
860
|
-
let stopped = false;
|
|
861
|
-
let imageFiber;
|
|
862
|
-
let imageTail = Promise.resolve();
|
|
863
|
-
const reconcileImageTool = async () => {
|
|
864
|
-
if (stopped) return;
|
|
865
|
-
const enabled = settings.get().enableImageTool;
|
|
866
|
-
if (enabled === (imageFiber !== void 0)) return;
|
|
867
|
-
const previous = imageFiber;
|
|
868
|
-
imageFiber = void 0;
|
|
869
|
-
if (previous !== void 0) await previous.dispose();
|
|
870
|
-
if (stopped || !enabled) return;
|
|
871
|
-
const fiber = ctx.inject([
|
|
872
|
-
"tools",
|
|
873
|
-
"fs",
|
|
874
|
-
"attachments",
|
|
875
|
-
"llm"
|
|
876
|
-
], (toolCtx) => toolCtx.tools.register(viewImageTool(toolCtx)));
|
|
877
|
-
imageFiber = fiber;
|
|
878
|
-
Promise.resolve(fiber).catch((error) => {
|
|
879
|
-
if (imageFiber === fiber) imageFiber = void 0;
|
|
880
|
-
ctx.logger.error("dsh-codex-auth: optional view_image tool failed to activate");
|
|
881
|
-
ctx.logger.error(error);
|
|
882
|
-
});
|
|
883
|
-
};
|
|
884
|
-
const scheduleImageTool = () => {
|
|
885
|
-
imageTail = imageTail.then(reconcileImageTool, reconcileImageTool).catch((error) => {
|
|
886
|
-
ctx.logger.error("dsh-codex-auth: could not apply the image-recognition configuration");
|
|
887
|
-
ctx.logger.error(error);
|
|
888
|
-
});
|
|
889
|
-
};
|
|
890
|
-
const unwatch = settings.watch(scheduleImageTool);
|
|
891
|
-
ctx.effect(() => async () => {
|
|
892
|
-
stopped = true;
|
|
893
|
-
unwatch();
|
|
894
|
-
await imageTail;
|
|
895
|
-
const image = imageFiber;
|
|
896
|
-
imageFiber = void 0;
|
|
897
|
-
await image?.dispose();
|
|
898
|
-
}, "dsh-codex-auth: optional image-tool lifecycle");
|
|
899
|
-
scheduleImageTool();
|
|
900
855
|
}
|
|
901
856
|
//#endregion
|
|
902
|
-
export { CODEX_API_KEY_ENV, CODEX_API_KEY_REF, CODEX_AUTH_FILENAME, CODEX_AUTH_LOGIN_PATH, CODEX_AUTH_LOGOUT_PATH,
|
|
857
|
+
export { CODEX_API_KEY_ENV, CODEX_API_KEY_REF, CODEX_AUTH_FILENAME, CODEX_AUTH_LOGIN_PATH, CODEX_AUTH_LOGOUT_PATH, CODEX_AUTH_STATUS_PATH, CODEX_GLOBAL_MODEL_PATH, CODEX_PROVIDER, CODEX_STREAM_IDLE_TIMEOUT_MS, CodexCredentialMirror, CodexCredentialStore, CodexUsageService, CodexWebAuth, apply, codexAuthPath, codexAuthStatus, createCodexAdapter, inject, loginCodex, logoutCodex, name, normalizeCodexUsagePayload, registerCodexAuthRoutes, trustedRequest };
|