@eddyskywalker/dsh-chatgpt-subscription 0.42.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +1 -0
  3. package/lib/client.js +180 -144
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +620 -216
  6. package/lib/types/client/command-code/CommandCodeComposerQuota.d.ts.map +1 -1
  7. package/lib/types/client/common/format.d.ts +16 -0
  8. package/lib/types/client/common/format.d.ts.map +1 -1
  9. package/lib/types/client/kimi-code/KimiCodeComposerQuota.d.ts.map +1 -1
  10. package/lib/types/client/kimi-code/KimiCodeSection.d.ts.map +1 -1
  11. package/lib/types/client/kimi-code/KimiQuotaCard.d.ts.map +1 -1
  12. package/lib/types/client/kimi-code/styles.d.ts.map +1 -1
  13. package/lib/types/client/locales-ru/opendesign.d.ts.map +1 -1
  14. package/lib/types/client/opendesign/OpenDesignSection.d.ts.map +1 -1
  15. package/lib/types/client/opendesign/locales.d.ts +12 -0
  16. package/lib/types/client/opendesign/locales.d.ts.map +1 -1
  17. package/lib/types/compat.d.ts +1 -1
  18. package/lib/types/host/claude/types.d.ts +7 -1
  19. package/lib/types/host/claude/types.d.ts.map +1 -1
  20. package/lib/types/host/glm-coding-plan/types.d.ts +5 -1
  21. package/lib/types/host/glm-coding-plan/types.d.ts.map +1 -1
  22. package/lib/types/host/kimi-code/account-pool.d.ts +26 -1
  23. package/lib/types/host/kimi-code/account-pool.d.ts.map +1 -1
  24. package/lib/types/host/kimi-code/client.d.ts +24 -0
  25. package/lib/types/host/kimi-code/client.d.ts.map +1 -1
  26. package/lib/types/host/kimi-code/oauth.d.ts +16 -0
  27. package/lib/types/host/kimi-code/oauth.d.ts.map +1 -1
  28. package/lib/types/host/kimi-code/routes.d.ts.map +1 -1
  29. package/lib/types/host/opendesign/adapter.d.ts +11 -13
  30. package/lib/types/host/opendesign/adapter.d.ts.map +1 -1
  31. package/lib/types/host/opendesign/catalog.d.ts +14 -0
  32. package/lib/types/host/opendesign/catalog.d.ts.map +1 -1
  33. package/lib/types/host/opendesign/line.d.ts +17 -6
  34. package/lib/types/host/opendesign/line.d.ts.map +1 -1
  35. package/lib/types/host/opendesign/routes.d.ts.map +1 -1
  36. package/lib/types/host/opendesign/token-store.d.ts +91 -4
  37. package/lib/types/host/opendesign/token-store.d.ts.map +1 -1
  38. package/lib/types/host/opendesign/types.d.ts.map +1 -1
  39. package/lib/types/host/qoder/types.d.ts +6 -1
  40. package/lib/types/host/qoder/types.d.ts.map +1 -1
  41. package/lib/types/host/step-code/types.d.ts +5 -1
  42. package/lib/types/host/step-code/types.d.ts.map +1 -1
  43. package/lib/types/shared/opendesign-contracts.d.ts +24 -1
  44. package/lib/types/shared/opendesign-contracts.d.ts.map +1 -1
  45. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -141,7 +141,7 @@ const ROUTE_PREFIX$4 = "/api/dsh-chatgpt-subscription";
141
141
  * `test/responses-client.test.ts` asserts the two agree, so a release that
142
142
  * forgets this constant fails the suite instead of shipping quietly.
143
143
  */
144
- const PLUGIN_VERSION = "0.42.0";
144
+ const PLUGIN_VERSION = "0.43.0";
145
145
  const CODEX_CHATGPT_PROVIDER_ID = "codex-chatgpt";
146
146
  const CODEX_API_BASE = "https://chatgpt.com/backend-api/codex";
147
147
  const CODEX_RESPONSES_URL = `${CODEX_API_BASE}/responses`;
@@ -26310,9 +26310,11 @@ function identityFromTokens(accessToken, refreshToken) {
26310
26310
  const refresh = refreshToken === void 0 ? void 0 : decodeJwtPayload(refreshToken);
26311
26311
  const userId = claimString(access?.user_id) ?? claimString(refresh?.user_id) ?? claimString(access?.sub) ?? claimString(refresh?.sub);
26312
26312
  const email = (claimString(access?.email) ?? claimString(refresh?.email))?.toLowerCase();
26313
+ const nickname = claimString(access?.nickname) ?? claimString(refresh?.nickname) ?? claimString(access?.name) ?? claimString(refresh?.name) ?? claimString(access?.preferred_username) ?? claimString(refresh?.preferred_username);
26313
26314
  return {
26314
26315
  ...userId === void 0 ? {} : { userId },
26315
- ...email === void 0 ? {} : { email }
26316
+ ...email === void 0 ? {} : { email },
26317
+ ...nickname === void 0 ? {} : { nickname }
26316
26318
  };
26317
26319
  }
26318
26320
  function parseTokenResponse$2(record) {
@@ -26511,7 +26513,7 @@ async function runDeviceLogin(options) {
26511
26513
  });
26512
26514
  if (outcome.kind === "success") {
26513
26515
  const identity = identityFromTokens(outcome.token.accessToken, outcome.token.refreshToken);
26514
- const credentials = {
26516
+ const signedIn = {
26515
26517
  accessToken: outcome.token.accessToken,
26516
26518
  refreshToken: outcome.token.refreshToken,
26517
26519
  expiresAt: outcome.token.expiresAt,
@@ -26523,8 +26525,10 @@ async function runDeviceLogin(options) {
26523
26525
  baseUrl: codingBaseUrl(options.region),
26524
26526
  authenticatedAt: Date.now(),
26525
26527
  ...identity.userId === void 0 ? {} : { userId: identity.userId },
26526
- ...identity.email === void 0 ? {} : { email: identity.email }
26528
+ ...identity.email === void 0 ? {} : { email: identity.email },
26529
+ ...identity.nickname === void 0 ? {} : { nickname: identity.nickname }
26527
26530
  };
26531
+ const credentials = options.enrich === void 0 ? signedIn : await options.enrich(signedIn).catch(() => signedIn);
26528
26532
  await options.store.write(credentials);
26529
26533
  if (options.onSave) await options.onSave(credentials).catch(() => void 0);
26530
26534
  await persistRegion(options.region);
@@ -26573,7 +26577,8 @@ async function beginWebLogin(store, options = {}) {
26573
26577
  region,
26574
26578
  signal: controller.signal,
26575
26579
  open,
26576
- ...options.onSave === void 0 ? {} : { onSave: options.onSave }
26580
+ ...options.onSave === void 0 ? {} : { onSave: options.onSave },
26581
+ ...options.enrich === void 0 ? {} : { enrich: options.enrich }
26577
26582
  });
26578
26583
  webLoginFlow = {
26579
26584
  ...webLoginFlow,
@@ -27365,6 +27370,92 @@ function parsePlanLevel(payload) {
27365
27370
  return asString$18(membership?.level ?? membership?.levelId ?? membership?.level_id) ?? null;
27366
27371
  }
27367
27372
  /**
27373
+ * Field names the service uses for the account's own name.
27374
+ *
27375
+ * `/me` documents a nickname; the platform also echoes a display name, and a
27376
+ * gateway envelope adds nothing but depth. All of them are the same fact: what
27377
+ * to title the account's card with, and what a `账号 N` placeholder should
27378
+ * become.
27379
+ */
27380
+ const PROFILE_NAME_KEYS = [
27381
+ "nickname",
27382
+ "name",
27383
+ "username",
27384
+ "display_name",
27385
+ "displayName",
27386
+ "preferred_username"
27387
+ ];
27388
+ /**
27389
+ * Every record a profile payload may keep the identity in, outermost first.
27390
+ *
27391
+ * The endpoint has answered flat (`{ nickname, email, ... }`) and nested
27392
+ * (`{ data: { userInfo: { ... } } }`) on different gateway versions, and a
27393
+ * nested envelope may hold credentials-adjacent siblings at the top level too,
27394
+ * so the search never commits to one shape: each container is tried in turn and
27395
+ * the first one that names a field wins.
27396
+ */
27397
+ function profileRecords(payload) {
27398
+ const root = asRecord$13(payload);
27399
+ if (root === void 0) return [];
27400
+ const data = asRecord$13(root.data);
27401
+ return [
27402
+ root,
27403
+ data,
27404
+ asRecord$13(root.userInfo) ?? asRecord$13(data?.userInfo),
27405
+ asRecord$13(root.user) ?? asRecord$13(data?.user)
27406
+ ].filter((record) => record !== void 0);
27407
+ }
27408
+ /** First value among `keys` found in any profile container, outermost first. */
27409
+ function profileString(payload, keys) {
27410
+ for (const record of profileRecords(payload)) {
27411
+ const found = firstString$1(record, keys);
27412
+ if (found !== void 0) return found;
27413
+ }
27414
+ }
27415
+ /**
27416
+ * Fold the facts a `/me` read established into a credential.
27417
+ *
27418
+ * Sign-in only names the account through its token, which carries an id and —
27419
+ * for an account with a mailbox — an address. The nickname lives on `/me` and
27420
+ * nowhere else, so the caller reads it once and stores it here; without this,
27421
+ * a phone-signed-in account could never be called anything but `账号 N`.
27422
+ *
27423
+ * Only fields the profile actually names are written: a gateway that drops the
27424
+ * nickname must not blank one learned earlier, and the token pair is copied
27425
+ * through untouched. Returns the same object when nothing new was learned, so a
27426
+ * caller can tell "nothing to store" from "store this".
27427
+ */
27428
+ function credentialsWithProfile(credentials, profile) {
27429
+ const nickname = profileString(profile, PROFILE_NAME_KEYS);
27430
+ const email = profileString(profile, ["email"])?.toLowerCase();
27431
+ const userId = profileString(profile, [
27432
+ "user_id",
27433
+ "userId",
27434
+ "global_id",
27435
+ "globalId"
27436
+ ]);
27437
+ const planName = parsePlanName(profile) ?? void 0;
27438
+ const next = { ...credentials };
27439
+ let changed = false;
27440
+ if (nickname !== void 0 && next.nickname !== nickname) {
27441
+ next.nickname = nickname;
27442
+ changed = true;
27443
+ }
27444
+ if (email !== void 0 && next.email !== email) {
27445
+ next.email = email;
27446
+ changed = true;
27447
+ }
27448
+ if (userId !== void 0 && next.userId !== userId) {
27449
+ next.userId = userId;
27450
+ changed = true;
27451
+ }
27452
+ if (planName !== void 0 && next.planName !== planName) {
27453
+ next.planName = planName;
27454
+ changed = true;
27455
+ }
27456
+ return changed ? next : credentials;
27457
+ }
27458
+ /**
27368
27459
  * Fetch and cache the account's quota snapshot.
27369
27460
  *
27370
27461
  * A 401 is surfaced as a rejection so the caller can invalidate the stored
@@ -27415,6 +27506,9 @@ async function fetchAccountQuota$3(store, options = {}) {
27415
27506
  ...credentials,
27416
27507
  ...learned
27417
27508
  }).catch(() => void 0);
27509
+ if (options.onAccount !== void 0) try {
27510
+ await options.onAccount(account, credentials);
27511
+ } catch {}
27418
27512
  const snapshot = {
27419
27513
  account,
27420
27514
  planName: account.planName,
@@ -27468,12 +27562,8 @@ function accountFromCredentials$1(credentials, payload, profile) {
27468
27562
  const planLevel = (profile === void 0 ? null : parsePlanLevel(profile)) ?? (payload === void 0 ? null : parsePlanLevel(payload));
27469
27563
  return {
27470
27564
  userId: credentials.userId ?? identity.userId ?? null,
27471
- nickname: (profile === void 0 ? null : firstString$1(asRecord$13(profile) ?? {}, [
27472
- "nickname",
27473
- "username",
27474
- "name"
27475
- ])) ?? credentials.nickname ?? null,
27476
- email: credentials.email ?? identity.email ?? null,
27565
+ nickname: (profile === void 0 ? void 0 : profileString(profile, PROFILE_NAME_KEYS)) ?? credentials.nickname ?? null,
27566
+ email: credentials.email ?? identity.email ?? (profile === void 0 ? void 0 : profileString(profile, ["email"])?.toLowerCase()) ?? null,
27477
27567
  planName,
27478
27568
  planLevel,
27479
27569
  region: credentials.region,
@@ -31991,6 +32081,46 @@ function processLine(line, state, wire) {
31991
32081
  function kimiCodePoolPath() {
31992
32082
  return path.join(dshHomeDir(), "storages", "kimi-code-pool.json");
31993
32083
  }
32084
+ /**
32085
+ * Aliases that name nobody: the placeholder this pool mints at sign-in.
32086
+ *
32087
+ * `defaultAlias` writes `账号 N` when the credential carried neither an address
32088
+ * nor a name (a phone-signed-in account carries neither), and the legacy
32089
+ * single-credential mirror wrote `主账号`. Both mean "no name learned yet", so
32090
+ * both may be upgraded once the profile reports one — while anything else is a
32091
+ * name someone chose or an address already on the card, and stays put.
32092
+ */
32093
+ const PLACEHOLDER_ALIAS = /^(?:账号|帐号|account)\s*\d*$|^主账号$|^primary$/i;
32094
+ /**
32095
+ * Whether an alias is still the placeholder a sign-in mints, rather than a name
32096
+ * somebody recognises.
32097
+ *
32098
+ * Exported because the routes decide whether one more profile read is worth it:
32099
+ * a row still titled `账号 N` is exactly the row whose name nobody has learned.
32100
+ */
32101
+ function isPlaceholderAlias(alias) {
32102
+ return PLACEHOLDER_ALIAS.test(alias.trim());
32103
+ }
32104
+ /** A field worth storing, or undefined when the service left it blank. */
32105
+ function learnedField(value) {
32106
+ const text = (value ?? "").trim();
32107
+ return text === "" ? void 0 : text;
32108
+ }
32109
+ /**
32110
+ * The alias one sign-in should leave the row with.
32111
+ *
32112
+ * A fresh sign-in computes one from the credential it just produced, and that is
32113
+ * the truth when it names something (an address, or a nickname in the token). It
32114
+ * names nothing for a phone account, though, while the row may already carry the
32115
+ * nickname a profile read learned or a title the user typed — dropping those
32116
+ * back to `账号 N` on a re-sign-in would undo both. So a minted placeholder only
32117
+ * beats another placeholder; anything that names somebody is kept.
32118
+ */
32119
+ function aliasForSignIn(minted, previous) {
32120
+ if (!isPlaceholderAlias(minted)) return minted;
32121
+ if (previous !== void 0 && !isPlaceholderAlias(previous)) return previous;
32122
+ return minted;
32123
+ }
31994
32124
  function optionalString$11(record, key) {
31995
32125
  const value = record[key];
31996
32126
  if (value === void 0 || value === null) return void 0;
@@ -32089,9 +32219,9 @@ var KimiCodeAccountPool = class extends AccountPoolCore {
32089
32219
  parsePoolData: parseKimiCodePoolData,
32090
32220
  dedupeKey: (credentials) => credentials.userId ?? credentials.email,
32091
32221
  defaultAlias: (credentials, position) => credentials.email ?? credentials.nickname ?? `账号 ${position}`,
32092
- createAccount: ({ id, alias, credentials, addedAt, isPrimary }) => ({
32222
+ createAccount: ({ id, alias, credentials, addedAt, isPrimary, existing }) => ({
32093
32223
  id,
32094
- alias,
32224
+ alias: aliasForSignIn(alias, existing?.alias),
32095
32225
  credentials,
32096
32226
  addedAt,
32097
32227
  isPrimary,
@@ -32183,6 +32313,47 @@ var KimiCodeAccountPool = class extends AccountPoolCore {
32183
32313
  mirrorStore() {
32184
32314
  return this.store;
32185
32315
  }
32316
+ /**
32317
+ * Remember who the service says one pooled account is.
32318
+ *
32319
+ * Sign-in can only name an account through its token, which carries an id and
32320
+ * — for an account with a mailbox — an address. That is why a phone-signed-in
32321
+ * row is titled `账号 N` and never changes on its own: the nickname lives on
32322
+ * `/me`, which only the quota read asks, and nothing used to carry the answer
32323
+ * back to the row. Every reading now calls this with what the profile said, so
32324
+ * the card can name its own account without a re-sign-in.
32325
+ *
32326
+ * Only the placeholder yields: a name someone chose stays, because silently
32327
+ * renaming a row a user named is worse than leaving a default in place. The
32328
+ * learned facts also go into the mirrored credential, so a later re-sign-in
32329
+ * (which mints the alias from the credential) keeps the name, and so do the
32330
+ * single-account fallbacks that read that copy.
32331
+ */
32332
+ async learnAccount(accountId, account) {
32333
+ const nickname = learnedField(account.nickname);
32334
+ const email = learnedField(account.email)?.toLowerCase();
32335
+ const userId = learnedField(account.userId);
32336
+ const planName = learnedField(account.planName);
32337
+ if (nickname === void 0 && email === void 0 && userId === void 0 && planName === void 0) return;
32338
+ await this.updatePool((data) => {
32339
+ const target = data.accounts.find((entry) => entry.id === accountId);
32340
+ if (target === void 0) return;
32341
+ if (nickname !== void 0) target.nickname = nickname;
32342
+ if (email !== void 0) target.email = email;
32343
+ if (userId !== void 0) target.userId = userId;
32344
+ if (planName !== void 0) target.planName = planName;
32345
+ const next = { ...target.credentials };
32346
+ if (nickname !== void 0) next.nickname = nickname;
32347
+ if (email !== void 0) next.email = email;
32348
+ if (userId !== void 0) next.userId = userId;
32349
+ if (planName !== void 0) next.planName = planName;
32350
+ target.credentials = next;
32351
+ if (isPlaceholderAlias(target.alias)) {
32352
+ const name = nickname ?? email;
32353
+ if (name !== void 0) target.alias = name;
32354
+ }
32355
+ });
32356
+ }
32186
32357
  };
32187
32358
  //#endregion
32188
32359
  //#region src/host/kimi-code/routes.ts
@@ -32426,6 +32597,57 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
32426
32597
  return (_credentials) => pool.renewCredential(accountId, fetchFn);
32427
32598
  };
32428
32599
  /**
32600
+ * Learning seam for the quota fetch, bound to one pool account.
32601
+ *
32602
+ * The reading is the only thing that asks `/me`, so it is also the only place
32603
+ * the account's nickname can be found. The pool owns the row, so the facts go
32604
+ * there: without this the card stayed titled `账号 N` forever, because nothing
32605
+ * ever carried the profile back to the account it describes.
32606
+ */
32607
+ const learnFor = (accountId) => {
32608
+ const pool = accountPool;
32609
+ if (pool === void 0 || accountId === void 0) return void 0;
32610
+ return (account) => pool.learnAccount(accountId, account);
32611
+ };
32612
+ /**
32613
+ * Accounts whose name has already been asked for, once per process.
32614
+ *
32615
+ * A row titled `账号 N` needs exactly one `/me`; retrying on every status poll
32616
+ * (the card asks every minute) would spend a request a minute on an account
32617
+ * whose profile simply has no nickname to give.
32618
+ */
32619
+ const nameProbes = /* @__PURE__ */ new Set();
32620
+ /**
32621
+ * One profile read for a row still carrying the sign-in placeholder.
32622
+ *
32623
+ * The quota read is the natural place to learn a name, but it happens only
32624
+ * when the snapshot is stale — and not at all while the usage call is failing
32625
+ * — so a fresh cache could hold the card at `账号 N` for as long as the TTL
32626
+ * lasts. This reads `/me` with the token the status call already holds, once
32627
+ * per account, and reports whether the row was renamed.
32628
+ */
32629
+ const probeName = async (target, rows) => {
32630
+ const pool = accountPool;
32631
+ if (pool === void 0 || target.id === void 0) return false;
32632
+ const row = rows.find((entry) => entry.id === target.id);
32633
+ if (row === void 0 || !isPlaceholderAlias(row.alias)) return false;
32634
+ if (nameProbes.has(target.id)) return false;
32635
+ nameProbes.add(target.id);
32636
+ try {
32637
+ const credentials = await effectiveCredentials(target);
32638
+ if (credentials === void 0) return false;
32639
+ const profile = await fetchProfile(store, {
32640
+ fetchFn,
32641
+ credentials
32642
+ });
32643
+ if (profile === null) return false;
32644
+ await pool.learnAccount(target.id, accountFromCredentials$1(credentials, void 0, profile));
32645
+ return true;
32646
+ } catch {
32647
+ return false;
32648
+ }
32649
+ };
32650
+ /**
32429
32651
  * The credential the active account should be used with right now.
32430
32652
  *
32431
32653
  * Every card path goes through the pool here. A pool owns both the rotation
@@ -32467,17 +32689,20 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
32467
32689
  ...accountPool === void 0 ? {} : {
32468
32690
  credentials,
32469
32691
  accountId: poolActive.id,
32470
- renewFn: renewFor(poolActive.id)
32692
+ renewFn: renewFor(poolActive.id),
32693
+ onAccount: learnFor(poolActive.id)
32471
32694
  }
32472
32695
  });
32473
32696
  };
32474
32697
  if (cached === null) await quotaRefresh.run(refresh);
32475
32698
  else quotaRefresh.start(refresh);
32476
32699
  }
32700
+ let value = await readStatus(url.searchParams.get("sessionId") ?? void 0);
32701
+ if (await probeName(poolActive, value.accounts ?? [])) value = await readStatus(url.searchParams.get("sessionId") ?? void 0);
32477
32702
  return sendJson$8(response, 200, {
32478
32703
  ok: true,
32479
32704
  value: {
32480
- ...await readStatus(url.searchParams.get("sessionId") ?? void 0),
32705
+ ...value,
32481
32706
  quotaError: quotaRefresh.lastError(),
32482
32707
  quotaRefreshing: quotaRefresh.refreshing
32483
32708
  }
@@ -32497,6 +32722,13 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
32497
32722
  fetchFn,
32498
32723
  region: requested,
32499
32724
  openBrowser: options.openBrowser,
32725
+ enrich: async (credentials) => {
32726
+ const profile = await fetchProfile(store, {
32727
+ fetchFn,
32728
+ credentials
32729
+ }).catch(() => null);
32730
+ return profile === null ? credentials : credentialsWithProfile(credentials, profile);
32731
+ },
32500
32732
  ...accountPool === void 0 ? {} : { onSave: (credentials) => accountPool.addAccount(credentials) }
32501
32733
  })
32502
32734
  });
@@ -32536,7 +32768,8 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
32536
32768
  ...accountPool === void 0 ? {} : {
32537
32769
  credentials: await effectiveCredentials(poolActive),
32538
32770
  accountId: poolActive.id,
32539
- renewFn: renewFor(poolActive.id)
32771
+ renewFn: renewFor(poolActive.id),
32772
+ onAccount: learnFor(poolActive.id)
32540
32773
  }
32541
32774
  });
32542
32775
  return sendJson$8(response, 200, {
@@ -32566,7 +32799,8 @@ function registerKimiCodeRoutes(ctx, store, modelSettings, preferences, options
32566
32799
  ...accountPool === void 0 ? {} : {
32567
32800
  credentials: await effectiveCredentials(quotaActive),
32568
32801
  accountId: quotaActive.id,
32569
- renewFn: renewFor(quotaActive.id)
32802
+ renewFn: renewFor(quotaActive.id),
32803
+ onAccount: learnFor(quotaActive.id)
32570
32804
  }
32571
32805
  }) === null) return sendJson$8(response, 502, {
32572
32806
  ok: false,
@@ -65296,14 +65530,14 @@ function publishedCatalogMeta(store, id) {
65296
65530
  function formatModelHint$1(contextWindow, input, reasoning) {
65297
65531
  return formatModelCapacity$1(contextWindow) + " · " + input.join("+") + (reasoning ? " · reasoning" : "");
65298
65532
  }
65299
- function uniqueModels$1(settings) {
65533
+ function uniqueModels(settings) {
65300
65534
  const ids = new Set(DEFAULT_MODELS);
65301
65535
  for (const model of listedClinePassCatalogModels(settings)) ids.add(model.id);
65302
65536
  return [...ids];
65303
65537
  }
65304
65538
  /** Every model the card should render, whether or not it is advertised. */
65305
65539
  function projectModels$1(settings, store) {
65306
- const known = uniqueModels$1(settings);
65540
+ const known = uniqueModels(settings);
65307
65541
  const hidden = new Set(settings.hiddenModels);
65308
65542
  const enabledSet = settings.enabledModelIds === null ? null : new Set(settings.enabledModelIds);
65309
65543
  return known.map((id) => {
@@ -65959,7 +66193,7 @@ function registerClinePassRoutes(ctx, options) {
65959
66193
  });
65960
66194
  const body = await readRequestJson$2(request).catch(() => ({}));
65961
66195
  const current = await settingsStore.read();
65962
- const known = uniqueModels$1(current);
66196
+ const known = uniqueModels(current);
65963
66197
  const hidden = new Set(current.hiddenModels);
65964
66198
  if (path === "models/visibility") {
65965
66199
  const model = typeof body.model === "string" ? body.model.trim() : "";
@@ -73021,8 +73255,7 @@ const MODEL_CATALOG$1 = Object.freeze({
73021
73255
  output: .28
73022
73256
  },
73023
73257
  contextWindow: 1048576,
73024
- input: ["text", "image"],
73025
- reasoning: false
73258
+ input: ["text", "image"]
73026
73259
  },
73027
73260
  "mimo-v2.6-pro": {
73028
73261
  name: "MiMo V2.6 Pro",
@@ -73031,8 +73264,7 @@ const MODEL_CATALOG$1 = Object.freeze({
73031
73264
  output: .87
73032
73265
  },
73033
73266
  contextWindow: 1048576,
73034
- input: ["text", "image"],
73035
- reasoning: false
73267
+ input: ["text", "image"]
73036
73268
  },
73037
73269
  "minimax-m3.1-flash": {
73038
73270
  name: "MiniMax M3.1 Flash",
@@ -73076,8 +73308,7 @@ const MODEL_CATALOG$1 = Object.freeze({
73076
73308
  },
73077
73309
  contextWindow: 1e6,
73078
73310
  maxTokens: 65536,
73079
- input: ["text"],
73080
- reasoning: false
73311
+ input: ["text"]
73081
73312
  }
73082
73313
  });
73083
73314
  /** Subscription models current at release time; the live catalog extends this. */
@@ -73099,6 +73330,96 @@ function catalogEntry$1(model) {
73099
73330
  return MODEL_CATALOG$1[model];
73100
73331
  }
73101
73332
  //#endregion
73333
+ //#region src/host/opendesign/catalog.ts
73334
+ /**
73335
+ * Merge the shipped table, the last gateway listing and the user's per-model
73336
+ * override into the descriptor the harness validates.
73337
+ *
73338
+ * Context window and output cap are provider facts, not preferences: the harness
73339
+ * sizes its prompt budget against `context.contextWindow`, so an understated
73340
+ * window silently truncates history. This line ships no per-model windows (the
73341
+ * gateway publishes none to an API key), so the window is the line default until
73342
+ * a live listing or a user override states one.
73343
+ *
73344
+ * @module dsh-chatgpt-subscription/opendesign-catalog
73345
+ */
73346
+ /** The wire's whole modality vocabulary; a listing may claim more than it accepts. */
73347
+ const SEAM_MODALITIES = /* @__PURE__ */ new Set(["text", "image"]);
73348
+ function definedOnly(value) {
73349
+ if (value === void 0) return {};
73350
+ const result = {};
73351
+ for (const [key, entry] of Object.entries(value)) if (entry !== void 0 && entry !== null) result[key] = entry;
73352
+ return result;
73353
+ }
73354
+ /**
73355
+ * The output budget to advertise for one model.
73356
+ *
73357
+ * `defaultMaxTokens` is what a request asks for, not what the model can emit,
73358
+ * and the harness spends it twice: it fills a request's `max_tokens` from it
73359
+ * and reserves the same number as completion room before deciding to compact.
73360
+ * So a published ceiling clamps the configured budget rather than becoming it,
73361
+ * and a budget is never taken past half the window.
73362
+ */
73363
+ function resolveOutputBudget(budget, ceiling, contextWindow) {
73364
+ const wanted = Number.isFinite(budget) && (budget ?? 0) > 0 ? budget : void 0;
73365
+ const most = Number.isFinite(ceiling) && (ceiling ?? 0) > 0 ? ceiling : void 0;
73366
+ const chosen = wanted === void 0 ? most : most === void 0 ? wanted : Math.min(wanted, most);
73367
+ if (chosen === void 0) return void 0;
73368
+ const half = Number.isFinite(contextWindow) && contextWindow > 0 ? Math.floor(contextWindow / 2) : void 0;
73369
+ return half === void 0 ? chosen : Math.min(chosen, half);
73370
+ }
73371
+ /**
73372
+ * Whether the subscription is known to serve one model.
73373
+ *
73374
+ * `servedModelIds === null` is "nobody has ever measured this plan": the absence
73375
+ * of a fact is not a denial, so nothing is hidden on its strength and the shipped
73376
+ * table stands in exactly as it did before this fact existed. An array is a
73377
+ * measurement taken from a successful scan, and an id outside it is a model this
73378
+ * plan does not serve — it stays in the editor as a described model, it just stops
73379
+ * being advertised.
73380
+ *
73381
+ * The user's own override lives one layer up (`forceVisibleModels`): this function
73382
+ * answers what the plan said, never what the user decided.
73383
+ */
73384
+ function isModelServed(model, servedModelIds) {
73385
+ return servedModelIds === null || servedModelIds.includes(model);
73386
+ }
73387
+ /**
73388
+ * Resolve the metadata one model is advertised with.
73389
+ *
73390
+ * Precedence per field: the user's override, then the last gateway listing,
73391
+ * then the shipped table, then the route-wide fallback. The listing sits above
73392
+ * the shipped table for the fields the table cannot carry (window, modalities):
73393
+ * the table's windows do not exist to be beaten, and its names/prices are
73394
+ * curated, so a listing that states no name keeps the table's.
73395
+ */
73396
+ function resolveModelInfo(model, options) {
73397
+ const synced = options.synced.find((entry) => entry.id === model);
73398
+ const shipped = catalogEntry$1(model);
73399
+ const published = {
73400
+ ...definedOnly(shipped),
73401
+ ...definedOnly(synced)
73402
+ };
73403
+ const fallbackWindow = options.defaultContextWindow ?? 128e3;
73404
+ const contextWindow = options.contextWindowOverride ?? published.contextWindow ?? fallbackWindow;
73405
+ const maxTokens = resolveOutputBudget(options.defaultMaxTokens ?? 64e3, published.maxTokens, contextWindow);
73406
+ const reasoning = published.reasoning ?? shipped?.reasoning ?? true;
73407
+ const efforts = (Array.isArray(published.efforts) && published.efforts.length > 0 ? published.efforts : void 0) ?? (shipped?.efforts !== void 0 && shipped.efforts.length > 0 ? [...shipped.efforts] : DEFAULT_EFFORTS);
73408
+ const defaultEffort = reasoning && shipped?.defaultEffort !== void 0 && efforts.includes(shipped.defaultEffort) ? shipped.defaultEffort : void 0;
73409
+ const input = (Array.isArray(published.input) && published.input.length > 0 ? published.input : shipped?.input ?? ["text"]).filter((entry) => SEAM_MODALITIES.has(entry));
73410
+ return {
73411
+ id: model,
73412
+ displayName: String(published.name ?? "").trim() || model,
73413
+ contextWindow,
73414
+ maxTokens: maxTokens ?? options.defaultMaxTokens ?? 64e3,
73415
+ reasoning,
73416
+ ...reasoning ? { efforts } : { efforts: [] },
73417
+ ...defaultEffort === void 0 ? {} : { defaultEffort },
73418
+ input: input.length > 0 ? input : ["text"],
73419
+ ...shipped?.pricing === void 0 ? {} : { pricing: shipped.pricing }
73420
+ };
73421
+ }
73422
+ //#endregion
73102
73423
  //#region src/host/opendesign/token-store.ts
73103
73424
  /** Whether one credential is a device-flow session rather than a pasted key. */
73104
73425
  function isOpenDesignOAuthCredentials(credentials) {
@@ -73254,6 +73575,24 @@ function readCatalogModels(value) {
73254
73575
  }
73255
73576
  return models;
73256
73577
  }
73578
+ /**
73579
+ * One stored id list, normalized: non-empty strings only, deduplicated, and
73580
+ * `null` when the value is not a list at all.
73581
+ *
73582
+ * `null` and `[]` stay distinct here; see the tri-state note on
73583
+ * {@link OpenDesignModelSettings.servedModelIds}.
73584
+ */
73585
+ function readIdList(value) {
73586
+ if (!Array.isArray(value)) return null;
73587
+ const ids = [];
73588
+ for (const entry of value) {
73589
+ if (typeof entry !== "string") continue;
73590
+ const id = entry.trim();
73591
+ if (id === "" || ids.includes(id)) continue;
73592
+ ids.push(id);
73593
+ }
73594
+ return ids;
73595
+ }
73257
73596
  function positiveInteger(value, fallback, allowZero = false) {
73258
73597
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < (allowZero ? 0 : 1)) return fallback;
73259
73598
  return value;
@@ -73268,6 +73607,8 @@ function parseOpenDesignModelSettings(value) {
73268
73607
  enabledModelIds,
73269
73608
  catalogModels: readCatalogModels(record.catalogModels),
73270
73609
  hiddenModels: Array.isArray(record.hiddenModels) ? record.hiddenModels.filter((id) => typeof id === "string") : [],
73610
+ servedModelIds: readIdList(record.servedModelIds),
73611
+ forceVisibleModels: readIdList(record.forceVisibleModels) ?? [],
73271
73612
  contextWindowOverrides: readNumberOverrides(record.contextWindowOverrides),
73272
73613
  defaultContextWindow: positiveInteger(record.defaultContextWindow, DEFAULT_CONTEXT_WINDOW),
73273
73614
  defaultMaxTokens: positiveInteger(record.defaultMaxTokens, DEFAULT_MAX_TOKENS),
@@ -73285,12 +73626,30 @@ function defaultOpenDesignModelSettings() {
73285
73626
  *
73286
73627
  * The file is written atomically (temp + rename) so a crash mid-write leaves the
73287
73628
  * previous document intact rather than a truncated one that reads as "reset all
73288
- * settings".
73629
+ * settings", and writes are SERIALIZED behind one queue so two read→compute→write
73630
+ * cycles can never interleave and erase each other's fields. Reads do not queue:
73631
+ * see {@link OpenDesignModelSettingsStore.pending}.
73289
73632
  */
73290
73633
  var OpenDesignModelSettingsStore = class {
73291
73634
  filePath;
73292
73635
  cached = null;
73293
73636
  loadPromise = null;
73637
+ /**
73638
+ * Tail of the write queue.
73639
+ *
73640
+ * Every mutator derives a WHOLE document from the one it read, so two of them in
73641
+ * flight at once each erase the other's fields. That is not hypothetical here:
73642
+ * the catalog sync is fire-and-forget (`void syncCatalog()` on account add and on
73643
+ * a completed sign-in), so it really can land between a selection change's read
73644
+ * and its write. Each mutation therefore runs its entire read → compute → write
73645
+ * inside one turn of this queue.
73646
+ *
73647
+ * `read()` stays OUTSIDE the queue on purpose: it is the hot path (every status
73648
+ * request, every keystroke-driven checkbox), and waiting behind a write would buy
73649
+ * nothing — by the time a queued operation runs, every write queued before it has
73650
+ * already landed, so the plain read it performs IS the in-lock read.
73651
+ */
73652
+ pending = Promise.resolve();
73294
73653
  constructor(filePath = modelSettingsPath$7()) {
73295
73654
  this.filePath = filePath;
73296
73655
  }
@@ -73300,7 +73659,14 @@ var OpenDesignModelSettingsStore = class {
73300
73659
  status() {
73301
73660
  return this.cached ?? defaultOpenDesignModelSettings();
73302
73661
  }
73303
- /** Read the document at activation time, synchronously. */
73662
+ /**
73663
+ * Read the document at activation time, synchronously.
73664
+ *
73665
+ * Synchronous by contract, so it cannot join the write queue — and it does not
73666
+ * need to: it only READS, a rename makes the file either the old or the new
73667
+ * document (never a half-written one), and a queued write assigns `cached` after
73668
+ * that rename, so a reading taken mid-write is superseded by the write's own.
73669
+ */
73304
73670
  readSync() {
73305
73671
  if (this.cached !== null) return this.cached;
73306
73672
  try {
@@ -73330,7 +73696,30 @@ var OpenDesignModelSettingsStore = class {
73330
73696
  this.cached = defaultOpenDesignModelSettings();
73331
73697
  return this.cached;
73332
73698
  }
73699
+ /**
73700
+ * Run one operation with exclusive access to the document.
73701
+ *
73702
+ * The queue's tail never carries a rejection: an operation that throws rejects
73703
+ * its OWN caller only, and the tail is flattened to a fulfilled promise right
73704
+ * afterwards, so a failed write cannot poison the writes queued behind it.
73705
+ */
73706
+ async exclusive(operation) {
73707
+ const result = this.pending.then(operation);
73708
+ this.pending = result.then(() => void 0, () => void 0);
73709
+ return await result;
73710
+ }
73711
+ /** Replace the whole document, queued behind any write already in flight. */
73333
73712
  async write(settings) {
73713
+ return await this.exclusive(() => this.writeUnlocked(settings));
73714
+ }
73715
+ /**
73716
+ * The file half of a write, for callers that ALREADY hold the queue.
73717
+ *
73718
+ * No public mutator may call another one: awaiting a queued mutator from inside
73719
+ * `exclusive` would wait for a queue position behind itself and deadlock. That is
73720
+ * why every mutator below composes `read()` with THIS, never with `write()`.
73721
+ */
73722
+ async writeUnlocked(settings) {
73334
73723
  await fs.mkdir(path.dirname(this.filePath), { recursive: true });
73335
73724
  const tmp = `${this.filePath}.tmp.${Date.now()}`;
73336
73725
  await fs.writeFile(tmp, JSON.stringify({
@@ -73342,32 +73731,75 @@ var OpenDesignModelSettingsStore = class {
73342
73731
  return settings;
73343
73732
  }
73344
73733
  async update(patch) {
73345
- const current = await this.read();
73346
- const next = {
73347
- ...current,
73348
- ...patch,
73349
- contextWindowOverrides: patch.contextWindowOverrides === void 0 ? current.contextWindowOverrides : patch.contextWindowOverrides
73350
- };
73351
- await this.write(next);
73352
- return next;
73734
+ return await this.exclusive(async () => {
73735
+ const current = await this.read();
73736
+ const next = {
73737
+ ...current,
73738
+ ...patch,
73739
+ contextWindowOverrides: patch.contextWindowOverrides === void 0 ? current.contextWindowOverrides : patch.contextWindowOverrides
73740
+ };
73741
+ return await this.writeUnlocked(next);
73742
+ });
73353
73743
  }
73354
- /** Record a catalog sync without touching the user's selection. */
73355
- async storeCatalog(models) {
73356
- const current = await this.read();
73357
- return await this.write({
73358
- ...current,
73359
- catalogModels: models
73744
+ /**
73745
+ * Record a catalog sync without touching the user's selection.
73746
+ *
73747
+ * `servedModelIds` is written only when the caller actually measured it: a sync
73748
+ * in which every scan failed must leave the previous measurement standing rather
73749
+ * than clearing the filter, so `undefined` keeps the stored value while an array
73750
+ * (including an empty one) replaces it.
73751
+ */
73752
+ async storeCatalog(models, servedModelIds) {
73753
+ return await this.exclusive(async () => {
73754
+ const current = await this.read();
73755
+ return await this.writeUnlocked(servedModelIds === void 0 ? {
73756
+ ...current,
73757
+ catalogModels: models
73758
+ } : {
73759
+ ...current,
73760
+ catalogModels: models,
73761
+ servedModelIds
73762
+ });
73763
+ });
73764
+ }
73765
+ /**
73766
+ * Replace the model selection, dropping any forced reveal it excludes.
73767
+ *
73768
+ * One `read` for both the prune input and the write baseline, so the two cannot
73769
+ * disagree: computing the prune outside and then patching would read the document
73770
+ * twice, and a forced reveal added in that gap would be recorded and then erased
73771
+ * by the second read's snapshot.
73772
+ *
73773
+ * The prune belongs to the selection rather than to `models/visibility` because
73774
+ * the card's only un-tick path is the selection patch: it removes the id from the
73775
+ * whitelist and never posts `{visible:false}`, so a forced reveal would otherwise
73776
+ * outlive the tick that asked for it. `null` is "no filter" — not a "keep only
73777
+ * these" statement — so it prunes nothing.
73778
+ */
73779
+ async selectModels(ids) {
73780
+ return await this.exclusive(async () => {
73781
+ const current = await this.read();
73782
+ return await this.writeUnlocked(ids === null ? {
73783
+ ...current,
73784
+ enabledModelIds: null
73785
+ } : {
73786
+ ...current,
73787
+ enabledModelIds: ids,
73788
+ forceVisibleModels: current.forceVisibleModels.filter((id) => ids.includes(id))
73789
+ });
73360
73790
  });
73361
73791
  }
73362
73792
  /** Store or clear (null) one model's context-window override. */
73363
73793
  async setContextWindowOverride(model, contextWindow) {
73364
- const current = await this.read();
73365
- const overrides = { ...current.contextWindowOverrides };
73366
- if (contextWindow === null) delete overrides[model];
73367
- else overrides[model] = contextWindow;
73368
- return await this.write({
73369
- ...current,
73370
- contextWindowOverrides: overrides
73794
+ return await this.exclusive(async () => {
73795
+ const current = await this.read();
73796
+ const overrides = { ...current.contextWindowOverrides };
73797
+ if (contextWindow === null) delete overrides[model];
73798
+ else overrides[model] = contextWindow;
73799
+ return await this.writeUnlocked({
73800
+ ...current,
73801
+ contextWindowOverrides: overrides
73802
+ });
73371
73803
  });
73372
73804
  }
73373
73805
  };
@@ -73521,80 +73953,6 @@ var OpenDesignAccountPool = class extends AccountPoolCore {
73521
73953
  }
73522
73954
  };
73523
73955
  //#endregion
73524
- //#region src/host/opendesign/catalog.ts
73525
- /**
73526
- * Merge the shipped table, the last gateway listing and the user's per-model
73527
- * override into the descriptor the harness validates.
73528
- *
73529
- * Context window and output cap are provider facts, not preferences: the harness
73530
- * sizes its prompt budget against `context.contextWindow`, so an understated
73531
- * window silently truncates history. This line ships no per-model windows (the
73532
- * gateway publishes none to an API key), so the window is the line default until
73533
- * a live listing or a user override states one.
73534
- *
73535
- * @module dsh-chatgpt-subscription/opendesign-catalog
73536
- */
73537
- /** The wire's whole modality vocabulary; a listing may claim more than it accepts. */
73538
- const SEAM_MODALITIES = /* @__PURE__ */ new Set(["text", "image"]);
73539
- function definedOnly(value) {
73540
- if (value === void 0) return {};
73541
- const result = {};
73542
- for (const [key, entry] of Object.entries(value)) if (entry !== void 0 && entry !== null) result[key] = entry;
73543
- return result;
73544
- }
73545
- /**
73546
- * The output budget to advertise for one model.
73547
- *
73548
- * `defaultMaxTokens` is what a request asks for, not what the model can emit,
73549
- * and the harness spends it twice: it fills a request's `max_tokens` from it
73550
- * and reserves the same number as completion room before deciding to compact.
73551
- * So a published ceiling clamps the configured budget rather than becoming it,
73552
- * and a budget is never taken past half the window.
73553
- */
73554
- function resolveOutputBudget(budget, ceiling, contextWindow) {
73555
- const wanted = Number.isFinite(budget) && (budget ?? 0) > 0 ? budget : void 0;
73556
- const most = Number.isFinite(ceiling) && (ceiling ?? 0) > 0 ? ceiling : void 0;
73557
- const chosen = wanted === void 0 ? most : most === void 0 ? wanted : Math.min(wanted, most);
73558
- if (chosen === void 0) return void 0;
73559
- const half = Number.isFinite(contextWindow) && contextWindow > 0 ? Math.floor(contextWindow / 2) : void 0;
73560
- return half === void 0 ? chosen : Math.min(chosen, half);
73561
- }
73562
- /**
73563
- * Resolve the metadata one model is advertised with.
73564
- *
73565
- * Precedence per field: the user's override, then the last gateway listing,
73566
- * then the shipped table, then the route-wide fallback. The listing sits above
73567
- * the shipped table for the fields the table cannot carry (window, modalities):
73568
- * the table's windows do not exist to be beaten, and its names/prices are
73569
- * curated, so a listing that states no name keeps the table's.
73570
- */
73571
- function resolveModelInfo(model, options) {
73572
- const synced = options.synced.find((entry) => entry.id === model);
73573
- const shipped = catalogEntry$1(model);
73574
- const published = {
73575
- ...definedOnly(shipped),
73576
- ...definedOnly(synced)
73577
- };
73578
- const fallbackWindow = options.defaultContextWindow ?? 128e3;
73579
- const contextWindow = options.contextWindowOverride ?? published.contextWindow ?? fallbackWindow;
73580
- const maxTokens = resolveOutputBudget(options.defaultMaxTokens ?? 64e3, published.maxTokens, contextWindow);
73581
- const reasoning = published.reasoning ?? shipped?.reasoning ?? true;
73582
- const efforts = (Array.isArray(published.efforts) && published.efforts.length > 0 ? published.efforts : void 0) ?? (shipped?.efforts !== void 0 && shipped.efforts.length > 0 ? [...shipped.efforts] : DEFAULT_EFFORTS);
73583
- const defaultEffort = reasoning && shipped?.defaultEffort !== void 0 && efforts.includes(shipped.defaultEffort) ? shipped.defaultEffort : void 0;
73584
- const input = (Array.isArray(published.input) && published.input.length > 0 ? published.input : shipped?.input ?? ["text"]).filter((entry) => SEAM_MODALITIES.has(entry));
73585
- return {
73586
- id: model,
73587
- displayName: String(published.name ?? "").trim() || model,
73588
- contextWindow,
73589
- maxTokens: maxTokens ?? options.defaultMaxTokens ?? 64e3,
73590
- reasoning,
73591
- ...reasoning ? { efforts } : { efforts: [] },
73592
- ...defaultEffort === void 0 ? {} : { defaultEffort },
73593
- input: input.length > 0 ? input : ["text"],
73594
- ...shipped?.pricing === void 0 ? {} : { pricing: shipped.pricing }
73595
- };
73596
- }
73597
- //#endregion
73598
73956
  //#region src/host/opendesign/client.ts
73599
73957
  /**
73600
73958
  * Transport for the OpenDesign link gateway. Transport only: no account or
@@ -75019,27 +75377,6 @@ var OpenDesignAdapter = class extends LlmAdapter {
75019
75377
  settings() {
75020
75378
  return this.modelSettings.read();
75021
75379
  }
75022
- /** Every id the line knows: the shipped table plus the synced live catalog. */
75023
- knownModelIds(settings) {
75024
- const ids = new Set(DEFAULT_MODELS$1);
75025
- for (const model of settings.catalogModels) ids.add(model.id);
75026
- return [...ids];
75027
- }
75028
- /**
75029
- * The ids the route advertises, narrowed by the user's selection and hidden
75030
- * list.
75031
- *
75032
- * The selection is captured against this same id vocabulary (the gateway's
75033
- * own wire ids), so a cold start filters the shipped table through it without
75034
- * the empty-intersection trap a foreign-vocabulary whitelist would spring —
75035
- * and an explicit "everything off" (`[]`) still wins, because that intent
75036
- * does not depend on a vocabulary at all.
75037
- */
75038
- visibleModelIds(settings) {
75039
- const hidden = new Set(settings.hiddenModels);
75040
- const enabled = settings.enabledModelIds === null ? null : new Set(settings.enabledModelIds);
75041
- return this.knownModelIds(settings).filter((id) => !hidden.has(id) && (enabled === null || enabled.has(id)));
75042
- }
75043
75380
  /** One model's resolved descriptor, from the shipped table plus live data. */
75044
75381
  async modelInfo(modelId) {
75045
75382
  const settings = await this.settings();
@@ -75054,7 +75391,7 @@ var OpenDesignAdapter = class extends LlmAdapter {
75054
75391
  const prov = provider || "opendesign";
75055
75392
  const settings = await this.settings();
75056
75393
  if (settings.enabled === false) return [];
75057
- const ids = this.visibleModelIds(settings);
75394
+ const ids = openDesignVisibleModelIds(settings);
75058
75395
  return await Promise.all(ids.map(async (id) => {
75059
75396
  const info = await this.modelInfo(id);
75060
75397
  return {
@@ -75131,28 +75468,33 @@ var OpenDesignAdapter = class extends LlmAdapter {
75131
75468
  try {
75132
75469
  let response;
75133
75470
  let transportError;
75134
- for (let transportTry = 0; transportTry <= TRANSPORT_RETRIES; transportTry += 1) try {
75135
- response = await openChatStream$1({
75136
- fetchFn: this.options.fetchFn,
75137
- baseURL,
75138
- apiKey,
75139
- body,
75140
- signal: withAttemptTimeout(upstream)
75141
- });
75142
- break;
75143
- } catch (error) {
75144
- if (signal.aborted) throw new LlmError("OpenDesign request aborted by caller", "ABORTED", { cause: error });
75145
- transportError = error;
75146
- if (transportTry < TRANSPORT_RETRIES && isPreConnectionFailure(error)) {
75147
- try {
75148
- await setTimeout$1(TRANSPORT_RETRY_BASE_MS * (transportTry + 1), void 0, { signal: upstream });
75149
- } catch (waitError) {
75150
- if (signal.aborted) throw new LlmError("OpenDesign request aborted by caller", "ABORTED", { cause: waitError });
75151
- throw waitError;
75471
+ for (let transportTry = 0; transportTry <= TRANSPORT_RETRIES; transportTry += 1) {
75472
+ const headerDeadline = withAttemptTimeout(upstream, this.options.headerTimeoutMs);
75473
+ try {
75474
+ response = await openChatStream$1({
75475
+ fetchFn: this.options.fetchFn,
75476
+ baseURL,
75477
+ apiKey,
75478
+ body,
75479
+ signal: headerDeadline.signal
75480
+ });
75481
+ headerDeadline.clear();
75482
+ break;
75483
+ } catch (error) {
75484
+ headerDeadline.clear();
75485
+ if (signal.aborted) throw new LlmError("OpenDesign request aborted by caller", "ABORTED", { cause: error });
75486
+ transportError = error;
75487
+ if (transportTry < TRANSPORT_RETRIES && isPreConnectionFailure(error)) {
75488
+ try {
75489
+ await setTimeout$1(TRANSPORT_RETRY_BASE_MS * (transportTry + 1), void 0, { signal: upstream });
75490
+ } catch (waitError) {
75491
+ if (signal.aborted) throw new LlmError("OpenDesign request aborted by caller", "ABORTED", { cause: waitError });
75492
+ throw waitError;
75493
+ }
75494
+ continue;
75152
75495
  }
75153
- continue;
75496
+ break;
75154
75497
  }
75155
- break;
75156
75498
  }
75157
75499
  if (response === void 0) {
75158
75500
  lastError = new LlmError(`${errorChain$1(transportError)} The request never reached the gateway: ${baseURL} failed during TLS/connect. Check the network or proxy.`, "TRANSPORT", { cause: transportError });
@@ -75220,9 +75562,31 @@ var OpenDesignAdapter = class extends LlmAdapter {
75220
75562
  await this.accountPool.markCooldown(accountId, POOL_COOLDOWN_MS$1, `${PROVIDER_NAME$2} credits exhausted`).catch(() => void 0);
75221
75563
  }
75222
75564
  };
75223
- /** A caller signal combined with the attempt's header deadline. */
75224
- function withAttemptTimeout(signal) {
75225
- return AbortSignal.any([signal, AbortSignal.timeout(ATTEMPT_TIMEOUT_MS)]);
75565
+ /**
75566
+ * A caller signal combined with the attempt's CLEARABLE header deadline.
75567
+ *
75568
+ * The clear is not a nicety, it is the difference between a header deadline and
75569
+ * a request deadline. `AbortSignal.timeout` is absolute and cannot be disarmed,
75570
+ * so attaching one to the fetch signal and leaving it there keeps counting
75571
+ * through the whole response body — a model that thinks for more than
75572
+ * `ATTEMPT_TIMEOUT_MS` then dies mid-answer with "The operation was aborted due
75573
+ * to timeout", is retried by the host, and dies again at the same wall clock.
75574
+ * That is exactly what happened here: two attempts streamed reasoning for 155s
75575
+ * and 173s and both were cut 180s after their request went out.
75576
+ *
75577
+ * Cleared after the headers (see the transport loop), at which point the stream
75578
+ * idle watchdog owns liveness: it watches for SILENCE and resets on every
75579
+ * chunk, so a long thinking pass is fine and a dead socket is still caught.
75580
+ */
75581
+ function withAttemptTimeout(signal, timeoutMs = ATTEMPT_TIMEOUT_MS) {
75582
+ const deadline = new AbortController();
75583
+ const timer = setTimeout(() => {
75584
+ deadline.abort(new DOMException("The operation was aborted due to timeout", "TimeoutError"));
75585
+ }, timeoutMs);
75586
+ return {
75587
+ signal: AbortSignal.any([signal, deadline.signal]),
75588
+ clear: () => clearTimeout(timer)
75589
+ };
75226
75590
  }
75227
75591
  /**
75228
75592
  * Whether a transport failure happened before any request was sent.
@@ -75540,6 +75904,15 @@ const OPENDESIGN_ROUTE_PREFIX = "/opendesign/api";
75540
75904
  const OPENDESIGN_CONSOLE_URL = "https://open-design.ai/cloud";
75541
75905
  //#endregion
75542
75906
  //#region src/host/opendesign/routes.ts
75907
+ /**
75908
+ * Accounts one catalog sync asks at most.
75909
+ *
75910
+ * Each account costs up to two upstream reads, so this is what stands between a
75911
+ * twenty-key pool and forty requests behind one button. It bounds the SCAN, not
75912
+ * the pool: an account beyond the cap contributes nothing to the union, because
75913
+ * the cap is a hard bound on the measurement rather than a rotating window.
75914
+ */
75915
+ const MAX_SYNC_ACCOUNTS = 8;
75543
75916
  function sendJson(response, status, body) {
75544
75917
  response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
75545
75918
  response.end(JSON.stringify(body));
@@ -75592,17 +75965,20 @@ function formatModelHint(contextWindow, input, reasoning, efforts, pricing) {
75592
75965
  const base = formatModelCapacity(contextWindow) + " · " + input.join("+") + (ladder === "" ? "" : " · " + ladder);
75593
75966
  return pricing === void 0 ? base : `${base} · $${pricing.input}/$${pricing.output}`;
75594
75967
  }
75595
- /** Every id the line knows: the shipped table plus the synced live catalog. */
75596
- function uniqueModels(settings) {
75597
- const ids = new Set(DEFAULT_MODELS$1);
75598
- for (const model of settings.catalogModels) ids.add(model.id);
75599
- return [...ids];
75600
- }
75601
- /** Every model the card should render, whether or not it is advertised. */
75968
+ /**
75969
+ * Every model the card should render, whether or not it is advertised.
75970
+ *
75971
+ * `enabled` is the state of the card's checklist row, and it is read from the ONE
75972
+ * visibility rule (`openDesignVisibleModelIds` in ./line.ts) — the very function
75973
+ * the adapter offers the picker with. A row that reads "checked" while no picker
75974
+ * offers it is the contradiction this projection must not be able to produce, so
75975
+ * the two are not two calculations: a model the plan withholds reads as unchecked
75976
+ * until the user forces it, because that is what the picker does with it.
75977
+ */
75602
75978
  function projectModels(settings) {
75603
- const known = uniqueModels(settings);
75604
- const hidden = new Set(settings.hiddenModels);
75605
- const enabledSet = settings.enabledModelIds === null ? null : new Set(settings.enabledModelIds);
75979
+ const known = openDesignKnownModelIds(settings);
75980
+ const visible = new Set(openDesignVisibleModelIds(settings));
75981
+ const forcedVisible = new Set(settings.forceVisibleModels);
75606
75982
  return known.map((id) => {
75607
75983
  const base = resolveModelInfo(id, {
75608
75984
  synced: settings.catalogModels,
@@ -75615,6 +75991,7 @@ function projectModels(settings) {
75615
75991
  defaultMaxTokens: settings.defaultMaxTokens,
75616
75992
  contextWindowOverride: settings.contextWindowOverrides[id]
75617
75993
  });
75994
+ const served = isModelServed(id, settings.servedModelIds);
75618
75995
  return {
75619
75996
  id,
75620
75997
  name: info.displayName,
@@ -75624,7 +76001,9 @@ function projectModels(settings) {
75624
76001
  maxTokens: info.maxTokens,
75625
76002
  reasoning: info.reasoning,
75626
76003
  input: [...info.input],
75627
- enabled: (enabledSet === null || enabledSet.has(id)) && !hidden.has(id),
76004
+ enabled: visible.has(id),
76005
+ served,
76006
+ forcedVisible: forcedVisible.has(id),
75628
76007
  ...info.pricing === void 0 ? {} : { pricing: info.pricing },
75629
76008
  ...settings.contextWindowOverrides[id] === void 0 ? {} : { contextWindowOverride: settings.contextWindowOverrides[id] }
75630
76009
  };
@@ -75717,16 +76096,22 @@ function registerOpenDesignRoutes(ctx, options) {
75717
76096
  };
75718
76097
  const status = async () => await getOpenDesignWebStatus(options, usageStore);
75719
76098
  /**
75720
- * Sync the live catalog with the first usable account's key.
76099
+ * Sync the live catalog with every usable account's key.
76100
+ *
76101
+ * The gateway's listing is the authority on what a key's plan serves, and a plan
76102
+ * belongs to an ACCOUNT rather than to the line: two pooled keys can serve two
76103
+ * different model sets, so every account is asked and the union is what gets
76104
+ * remembered. That union is persisted as `servedModelIds` — the fact the picker
76105
+ * needs and the one this sync used to throw away — and it is written only when a
76106
+ * scan really succeeded, so a network failure cannot switch the filter off.
75721
76107
  *
75722
- * The gateway's listing is the authority on what this key's plan serves; a
75723
- * failed or refused read changes nothing (the shipped table still stands in),
75724
- * and the sync's answer says which happened. Announces only a real change:
75725
- * the picker rebuilds on `llm/adapters-updated`, so a no-change sync must
75726
- * stay silent.
76108
+ * A failed or refused read changes nothing (the shipped table still stands in),
76109
+ * and the sync's answer says which happened. Announces only a real change: the
76110
+ * picker rebuilds on `llm/adapters-updated`, so a no-change sync must stay
76111
+ * silent.
75727
76112
  */
75728
76113
  const syncCatalog = async () => {
75729
- const known = uniqueModels(await settingsStore.read());
76114
+ const known = openDesignKnownModelIds(await settingsStore.read());
75730
76115
  const data = await accountPool.read().catch(() => null);
75731
76116
  const now = Date.now();
75732
76117
  const accounts = (data?.accounts ?? []).filter((account) => isUsable(account, now));
@@ -75737,27 +76122,22 @@ function registerOpenDesignRoutes(ctx, options) {
75737
76122
  };
75738
76123
  const scans = [];
75739
76124
  let controlRefused = false;
75740
- for (const account of accounts) {
76125
+ const settings = await settingsStore.read();
76126
+ for (const account of accounts.slice(0, MAX_SYNC_ACCOUNTS)) {
75741
76127
  const controlKey = controlKeyFor(account.credentials);
75742
- if (controlKey === void 0) continue;
75743
- const scan = await fetchControlPlaneCatalog({
75744
- fetchFn,
75745
- controlKey,
75746
- knownIds: known
75747
- });
75748
- if (scan === null) {
75749
- controlRefused = true;
75750
- continue;
76128
+ if (controlKey !== void 0) {
76129
+ const scan = await fetchControlPlaneCatalog({
76130
+ fetchFn,
76131
+ controlKey,
76132
+ knownIds: known
76133
+ });
76134
+ if (scan === null) controlRefused = true;
76135
+ else if (scan.ids.length > 0) scans.push(scan);
75751
76136
  }
75752
- if (scan.ids.length > 0) scans.push(scan);
75753
- break;
75754
- }
75755
- {
75756
- const settings = await settingsStore.read();
75757
76137
  const gateway = await fetchGatewayCatalog$1({
75758
76138
  fetchFn,
75759
76139
  baseURL: settings.baseURL,
75760
- apiKey: bearerFor(accounts[0].credentials)
76140
+ apiKey: bearerFor(account.credentials)
75761
76141
  });
75762
76142
  if (gateway !== null && gateway.ids.length > 0) scans.push(gateway);
75763
76143
  }
@@ -75787,9 +76167,10 @@ function registerOpenDesignRoutes(ctx, options) {
75787
76167
  const merged = [...existing.values()].filter((entry) => {
75788
76168
  return !current.catalogModels.some((old) => old.id === entry.id) || syncedIds.has(entry.id);
75789
76169
  });
76170
+ const served = [...new Set(scans.flatMap((scan) => scan.ids))];
75790
76171
  const fingerprintBefore = current.catalogModels.map((entry) => entry.id).join("\n");
75791
76172
  const fingerprintAfter = merged.map((entry) => entry.id).join("\n");
75792
- await settingsStore.storeCatalog(merged);
76173
+ await settingsStore.storeCatalog(merged, served);
75793
76174
  if (fingerprintBefore !== fingerprintAfter) emitModelsUpdated();
75794
76175
  return {
75795
76176
  added,
@@ -76033,8 +76414,7 @@ function registerOpenDesignRoutes(ctx, options) {
76033
76414
  const body = await readRequestJson(request).catch(() => ({}));
76034
76415
  let touched = false;
76035
76416
  if (body.enabledModelIds === null || Array.isArray(body.enabledModelIds)) {
76036
- const ids = body.enabledModelIds === null ? null : body.enabledModelIds.filter((id) => typeof id === "string");
76037
- await settingsStore.update({ enabledModelIds: ids });
76417
+ await settingsStore.selectModels(body.enabledModelIds === null ? null : body.enabledModelIds.filter((id) => typeof id === "string"));
76038
76418
  touched = true;
76039
76419
  }
76040
76420
  if (typeof body.enabled === "boolean") {
@@ -76055,16 +76435,24 @@ function registerOpenDesignRoutes(ctx, options) {
76055
76435
  });
76056
76436
  const body = await readRequestJson(request).catch(() => ({}));
76057
76437
  const current = await settingsStore.read();
76058
- const known = uniqueModels(current);
76438
+ const known = openDesignKnownModelIds(current);
76059
76439
  const hidden = new Set(current.hiddenModels);
76440
+ const forced = new Set(current.forceVisibleModels);
76060
76441
  if (path === "models/visibility") {
76061
76442
  const model = typeof body.model === "string" ? body.model.trim() : "";
76062
76443
  if (!known.includes(model)) return sendJson(response, 400, {
76063
76444
  ok: false,
76064
76445
  error: `${JSON.stringify(model)} is not one of this route's models`
76065
76446
  });
76066
- if (body.visible === false) hidden.add(model);
76067
- else hidden.delete(model);
76447
+ const served = isModelServed(model, current.servedModelIds);
76448
+ if (body.visible === false) {
76449
+ if (served) hidden.add(model);
76450
+ forced.delete(model);
76451
+ } else {
76452
+ hidden.delete(model);
76453
+ if (served) forced.delete(model);
76454
+ else forced.add(model);
76455
+ }
76068
76456
  } else {
76069
76457
  const mode = body.mode;
76070
76458
  if (mode !== "all" && mode !== "none" && mode !== "invert") return sendJson(response, 400, {
@@ -76077,7 +76465,11 @@ function registerOpenDesignRoutes(ctx, options) {
76077
76465
  const wasHidden = current.hiddenModels.includes(id);
76078
76466
  return mode === "all" ? false : mode === "none" ? true : !wasHidden;
76079
76467
  }) : [...hidden];
76080
- await settingsStore.update({ hiddenModels: nextHidden });
76468
+ const nextForced = path !== "models/visibility-all" ? [...forced] : body.mode === "invert" ? current.forceVisibleModels : [];
76469
+ await settingsStore.update({
76470
+ hiddenModels: nextHidden,
76471
+ forceVisibleModels: nextForced
76472
+ });
76081
76473
  emitModelsUpdated();
76082
76474
  return sendJson(response, 200, {
76083
76475
  ok: true,
@@ -76285,18 +76677,30 @@ function openDesignKnownModelIds(settings) {
76285
76677
  return [...ids];
76286
76678
  }
76287
76679
  /**
76288
- * Every id the line advertises right now.
76680
+ * Every id the line advertises right now — THE one visibility rule.
76681
+ *
76682
+ * The adapter (what the picker is offered), the status route (what each checklist
76683
+ * row reports as checked) and the hub card (how many models the line advertises)
76684
+ * all read this function and nothing else. Three private copies is how the hub
76685
+ * card ended up counting models the picker refused to show, which is exactly the
76686
+ * contradiction this rule exists to make impossible.
76687
+ *
76688
+ * Three layers, applied in this order:
76289
76689
  *
76290
- * The selection rule is shared by the adapter, the status route and the hub
76291
- * card — they must all read this one function, not three private copies: the
76292
- * user's tick list and the shipped table speak the same id vocabulary here (the
76293
- * gateway's own wire ids), so a cold-start intersection cannot empty the picker,
76294
- * and an explicit "everything off" (`[]`) still wins.
76690
+ * 1. the user's hidden list;
76691
+ * 2. the user's tick list — unset means every known model, an explicit `[]` means
76692
+ * everything off (the tick list and the shipped table speak the same wire ids,
76693
+ * so a cold-start intersection cannot empty the picker);
76694
+ * 3. the PLAN, once somebody measured it: an id the last successful scan did not
76695
+ * list is not served, and only a forced reveal (`forceVisibleModels`) keeps it.
76696
+ * Before that first scan `servedModelIds` is `null` and this layer hides
76697
+ * nothing — an unmeasured plan is not a denial.
76295
76698
  */
76296
76699
  function openDesignVisibleModelIds(settings) {
76297
76700
  const hidden = new Set(settings.hiddenModels);
76298
76701
  const enabled = settings.enabledModelIds === null ? null : new Set(settings.enabledModelIds);
76299
- return openDesignKnownModelIds(settings).filter((id) => !hidden.has(id) && (enabled === null || enabled.has(id)));
76702
+ const forced = new Set(settings.forceVisibleModels);
76703
+ return openDesignKnownModelIds(settings).filter((id) => !hidden.has(id) && (enabled === null || enabled.has(id)) && (isModelServed(id, settings.servedModelIds) || forced.has(id)));
76300
76704
  }
76301
76705
  /**
76302
76706
  * Build the line.