@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/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-DQ2F4vc6.js";
2
- import { CODEX_AUTH_LOGIN_PATH, CODEX_AUTH_LOGOUT_PATH, CODEX_AUTH_SETTINGS_NAMESPACE, CODEX_AUTH_SETTINGS_PATH, CODEX_AUTH_STATUS_PATH, CODEX_GLOBAL_MODEL_PATH, CODEX_USAGE_PATH } from "./auth-paths.js";
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 z from "@deepseek-ai/schemastery";
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/settings-contract.ts
139
- const DEFAULT_CODEX_AUTH_SETTINGS = Object.freeze({
140
- enableImageTool: false,
141
- enableImageUpload: false
142
- });
143
- function isRecord(value) {
144
- return typeof value === "object" && value !== null && !Array.isArray(value);
145
- }
146
- /** Narrow the settings wire payload before it enters React state. */
147
- function decodeCodexAuthSettings(value) {
148
- if (!isRecord(value) || typeof value["enableImageTool"] !== "boolean") return void 0;
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
- enableImageTool: value["enableImageTool"],
151
- enableImageUpload: typeof value["enableImageUpload"] === "boolean" ? value["enableImageUpload"] : DEFAULT_CODEX_AUTH_SETTINGS.enableImageUpload
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 CODEX_SETTINGS_BODY_LIMIT = 8192;
162
- function withDshVersion(status) {
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 > CODEX_SETTINGS_BODY_LIMIT) {
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
- /** Register the plugin-owned bridge to DSH alpha.4's shared Agent default model. */
420
- function registerCodexGlobalModelRoute(ctx, defaultModel, llm) {
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: CODEX_GLOBAL_MODEL_PATH,
586
+ path: CODEX_MODEL_REFRESH_PATH,
430
587
  handler: async (req, res) => {
431
- if (req.method !== "GET" && req.method !== "PUT") return json(res, 405, { error: "method not allowed" });
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 next = decodeGlobalModelWrite(await readJsonBody(req));
436
- if (next === void 0) return json(res, 400, { error: CODEX_GLOBAL_MODEL_INVALID });
437
- const resolved = await llm.resolveCallConfig({
438
- provider: CODEX_PROVIDER,
439
- model: next.model,
440
- ...next.reasoningEffort === void 0 ? {} : { reasoningEffort: next.reasoningEffort }
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
- return json(res, 400, { error: safeMessage(error) });
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: global Codex model route");
607
+ }, "dsh-codex-auth-plugin: Codex model catalog refresh route");
451
608
  }
452
- /** Register the plugin-owned settings endpoint for browsers opened through a NAS authority. */
453
- function registerCodexSettingsRoute(ctx, settings) {
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: CODEX_AUTH_SETTINGS_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, settings.get());
623
+ if (req.method === "GET") return json(res, 200, { globalModel: codexGlobalModelOf(defaultModel.currentSelection()) });
467
624
  try {
468
- const next = decodeSettingsWrite(await readJsonBody(req));
469
- if (next === void 0) return json(res, 400, { error: CODEX_SETTINGS_INVALID });
470
- await settings.update(next);
471
- json(res, 200, settings.get());
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: remote settings route");
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, withDshVersion(await auth.status()));
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/settings.ts
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, settings card, and credential mirror. */
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
- registerCodexSettingsRoute(ctx, settings);
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, CODEX_AUTH_SETTINGS_NAMESPACE, 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 };
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 };