@eddyskywalker/dsh-chatgpt-subscription 0.42.0 → 0.43.1

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 (55) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +1 -0
  3. package/lib/client.js +256 -182
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +813 -237
  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/locales-ru/qoder.d.ts.map +1 -1
  15. package/lib/types/client/opendesign/OpenDesignSection.d.ts.map +1 -1
  16. package/lib/types/client/opendesign/locales.d.ts +12 -0
  17. package/lib/types/client/opendesign/locales.d.ts.map +1 -1
  18. package/lib/types/client/qoder/QoderSection.d.ts.map +1 -1
  19. package/lib/types/client/qoder/locales.d.ts +9 -1
  20. package/lib/types/client/qoder/locales.d.ts.map +1 -1
  21. package/lib/types/client/qoder/styles.d.ts.map +1 -1
  22. package/lib/types/compat.d.ts +1 -1
  23. package/lib/types/host/claude/types.d.ts +7 -1
  24. package/lib/types/host/claude/types.d.ts.map +1 -1
  25. package/lib/types/host/glm-coding-plan/types.d.ts +5 -1
  26. package/lib/types/host/glm-coding-plan/types.d.ts.map +1 -1
  27. package/lib/types/host/kimi-code/account-pool.d.ts +26 -1
  28. package/lib/types/host/kimi-code/account-pool.d.ts.map +1 -1
  29. package/lib/types/host/kimi-code/client.d.ts +24 -0
  30. package/lib/types/host/kimi-code/client.d.ts.map +1 -1
  31. package/lib/types/host/kimi-code/oauth.d.ts +16 -0
  32. package/lib/types/host/kimi-code/oauth.d.ts.map +1 -1
  33. package/lib/types/host/kimi-code/routes.d.ts.map +1 -1
  34. package/lib/types/host/opendesign/adapter.d.ts +11 -13
  35. package/lib/types/host/opendesign/adapter.d.ts.map +1 -1
  36. package/lib/types/host/opendesign/catalog.d.ts +14 -0
  37. package/lib/types/host/opendesign/catalog.d.ts.map +1 -1
  38. package/lib/types/host/opendesign/line.d.ts +17 -6
  39. package/lib/types/host/opendesign/line.d.ts.map +1 -1
  40. package/lib/types/host/opendesign/routes.d.ts.map +1 -1
  41. package/lib/types/host/opendesign/token-store.d.ts +91 -4
  42. package/lib/types/host/opendesign/token-store.d.ts.map +1 -1
  43. package/lib/types/host/opendesign/types.d.ts.map +1 -1
  44. package/lib/types/host/qoder/client.d.ts.map +1 -1
  45. package/lib/types/host/qoder/mapper.d.ts +74 -2
  46. package/lib/types/host/qoder/mapper.d.ts.map +1 -1
  47. package/lib/types/host/qoder/types.d.ts +6 -1
  48. package/lib/types/host/qoder/types.d.ts.map +1 -1
  49. package/lib/types/host/step-code/types.d.ts +5 -1
  50. package/lib/types/host/step-code/types.d.ts.map +1 -1
  51. package/lib/types/shared/opendesign-contracts.d.ts +24 -1
  52. package/lib/types/shared/opendesign-contracts.d.ts.map +1 -1
  53. package/lib/types/shared/qoder-contracts.d.ts +10 -0
  54. package/lib/types/shared/qoder-contracts.d.ts.map +1 -1
  55. 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.1";
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() : "";
@@ -69668,11 +69902,15 @@ function normalizeQoderQuotaSnapshot(value, region, fetchedAt = Date.now()) {
69668
69902
  if (addOn !== void 0) buckets.push(addOn);
69669
69903
  const org = normalizeQoderBucket(value.orgResourcePackage, "org");
69670
69904
  if (org !== void 0) buckets.push(org);
69671
- if (buckets.length === 0) return void 0;
69905
+ const quotaExceeded = value.isQuotaExceeded === true;
69906
+ const upgradeUrl = asText$1(value.upgradeUrl) ?? null;
69907
+ if (buckets.length === 0 && !quotaExceeded) return void 0;
69672
69908
  const expiresRaw = asQuotaNumber(value.expiresAt) ?? null;
69673
69909
  return {
69674
69910
  buckets,
69675
69911
  expiresAt: expiresRaw !== null && expiresRaw <= 864e13 ? expiresRaw : null,
69912
+ quotaExceeded,
69913
+ upgradeUrl,
69676
69914
  fetchedAt,
69677
69915
  region
69678
69916
  };
@@ -70410,7 +70648,7 @@ function processQoderStreamLine(line, state) {
70410
70648
  const envelope = safeJsonParse(payload);
70411
70649
  if (!isRecord$6(envelope)) return [];
70412
70650
  const statusCode = optionalNumber(envelope.statusCodeValue);
70413
- if (statusCode !== void 0 && statusCode !== 200) throw classifyQoderFailure(statusCode, asString$2(envelope.body) ?? "");
70651
+ if (statusCode !== void 0 && statusCode !== 200) throw classifyQoderEnvelopeFailure(statusCode, envelope);
70414
70652
  const innerStr = asString$2(envelope.body);
70415
70653
  if (innerStr === "[DONE]") {
70416
70654
  state.done = true;
@@ -70421,7 +70659,11 @@ function processQoderStreamLine(line, state) {
70421
70659
  if (!isRecord$6(inner)) return [];
70422
70660
  const out = [];
70423
70661
  const errorPayload = isRecord$6(inner.error) ? inner.error : void 0;
70424
- if (errorPayload !== void 0) throw new LlmError$1("Qoder(订阅) stream error: " + (asString$2(errorPayload.message) ?? "unknown error"), isContextOverflow(errorPayload) ? CONTEXT_OVERFLOW_CODE : "PROVIDER_ERROR");
70662
+ if (errorPayload !== void 0) {
70663
+ const innerFacts = qoderFailureFacts([errorPayload]);
70664
+ if (isQoderQuotaExhausted(innerFacts, void 0) || isQoderResourceLimited(innerFacts, void 0)) throw classifyQoderFailureFacts(void 0, "", innerFacts);
70665
+ throw new LlmError$1("Qoder(订阅) stream error: " + (asString$2(errorPayload.message) ?? "unknown error"), isContextOverflow(errorPayload) ? CONTEXT_OVERFLOW_CODE : "PROVIDER_ERROR");
70666
+ }
70425
70667
  const usage = parseQoderUsage(inner.usage);
70426
70668
  if (usage !== void 0) {
70427
70669
  state.sawUsage = true;
@@ -70496,32 +70738,196 @@ function assertQoderStreamComplete(state) {
70496
70738
  }
70497
70739
  /** 错误体里引用回消息的最大长度。 */
70498
70740
  const MAX_FAILURE_DETAIL_LENGTH = 600;
70499
- /** 从错误体里尽量读一个业务错误码(字符串 \`code\`,顶层或 \`error\` 里)。 */
70500
- function qoderErrorCode(detail) {
70501
- const parsed = safeJsonParse(detail);
70502
- if (!isRecord$6(parsed)) return void 0;
70503
- const code = parsed.code ?? (isRecord$6(parsed.error) ? parsed.error.code : void 0);
70504
- return typeof code === "string" && code !== "" ? code : typeof code === "number" ? String(code) : void 0;
70741
+ /** 事实扫描的深度与节点上限:上游的错误体很小,但别让它变成一次无界的走树。 */
70742
+ const FAILURE_FACT_MAX_DEPTH = 6;
70743
+ const FAILURE_FACT_MAX_NODES = 200;
70744
+ /** 一个码字段的值(字符串或数字)转成字符串。 */
70745
+ function qoderCodeText(value) {
70746
+ if (typeof value === "string") {
70747
+ const trimmed = value.trim();
70748
+ return trimmed === "" ? void 0 : trimmed;
70749
+ }
70750
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
70751
+ }
70752
+ /** 一个 URL 字段的值。 */
70753
+ function qoderUrlText(value) {
70754
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
70755
+ }
70756
+ /**
70757
+ * 扫一遍上游的失败载荷,把认得的字段收成事实。
70758
+ *
70759
+ * 为什么要扫而不是只取顶层:上游把这几个字段放在**不同层**(信封外层 / body /
70760
+ * body 里的 error 对象),而码是**一条链**(403 → 10605)。逐层找比猜位置可靠:
70761
+ * 猜错位置就等于把「排队」读成「凭据被拒」。
70762
+ */
70763
+ function qoderFailureFacts(roots) {
70764
+ const facts = {
70765
+ codes: [],
70766
+ subCodes: []
70767
+ };
70768
+ const budget = { nodes: FAILURE_FACT_MAX_NODES };
70769
+ for (const root of roots) collectQoderFailureFacts(root, facts, 0, budget);
70770
+ return facts;
70771
+ }
70772
+ function collectQoderFailureFacts(node, facts, depth, budget) {
70773
+ if (depth > FAILURE_FACT_MAX_DEPTH || budget.nodes <= 0) return;
70774
+ if (Array.isArray(node)) {
70775
+ for (const item of node) collectQoderFailureFacts(item, facts, depth + 1, budget);
70776
+ return;
70777
+ }
70778
+ if (!isRecord$6(node)) return;
70779
+ budget.nodes -= 1;
70780
+ for (const [key, value] of Object.entries(node)) {
70781
+ switch (key) {
70782
+ case "code":
70783
+ case "errorCode":
70784
+ case "error_code": {
70785
+ const text = qoderCodeText(value);
70786
+ if (text !== void 0) facts.codes.push(text);
70787
+ break;
70788
+ }
70789
+ case "subCode":
70790
+ case "sub_code": {
70791
+ const text = qoderCodeText(value);
70792
+ if (text !== void 0) facts.subCodes.push(text);
70793
+ break;
70794
+ }
70795
+ case "queueCount":
70796
+ case "queue_count":
70797
+ facts.queueCount ??= optionalNumber(value);
70798
+ break;
70799
+ case "retryAfterSeconds":
70800
+ case "retry_after_seconds":
70801
+ facts.retryAfterSeconds ??= optionalNumber(value);
70802
+ break;
70803
+ case "serviceAvailable":
70804
+ case "service_available":
70805
+ if (typeof value === "boolean") facts.serviceAvailable ??= value;
70806
+ break;
70807
+ case "isQueued":
70808
+ case "is_queued":
70809
+ if (typeof value === "boolean") facts.isQueued ??= value;
70810
+ break;
70811
+ case "modelKey":
70812
+ case "model_key":
70813
+ facts.modelKey ??= qoderCodeText(value);
70814
+ break;
70815
+ case "pricingUrl":
70816
+ case "pricing_url":
70817
+ facts.pricingUrl ??= qoderUrlText(value);
70818
+ break;
70819
+ case "upgradeUrl":
70820
+ case "upgrade_url":
70821
+ facts.upgradeUrl ??= qoderUrlText(value);
70822
+ break;
70823
+ case "isQuotaExceeded":
70824
+ case "is_quota_exceeded":
70825
+ if (typeof value === "boolean") facts.quotaExceeded ??= value;
70826
+ break;
70827
+ default: break;
70828
+ }
70829
+ collectQoderFailureFacts(value, facts, depth + 1, budget);
70830
+ }
70831
+ }
70832
+ /**
70833
+ * 上游是不是在说「资源不足 / 排队中」。
70834
+ *
70835
+ * 认两种凭据:上游**点名**的(\`RESOURCE_LIMIT\` / 10605)与 403 搭配的容量信号
70836
+ * (服务不可用 / 这次在排队 / 前面排了人)。点名的凭据压过状态码——429 带上
70837
+ * RESOURCE_LIMIT 说的是「上游没容量」,不是「这个账号被限流」(与 kimi-code 把
70838
+ * 「引擎过载」从 429 里分出来是同一判断);只有排队人数这种弱信号时,429 的含义
70839
+ * 不变,号池照旧冷却账号并轮转。
70840
+ */
70841
+ function isQoderResourceLimited(facts, status) {
70842
+ if (facts.subCodes.some((code) => code.toUpperCase() === "RESOURCE_LIMIT")) return true;
70843
+ if (facts.codes.includes("10605")) return true;
70844
+ if (status !== 403) return false;
70845
+ return facts.serviceAvailable === false || facts.isQueued === true || facts.queueCount !== void 0 && facts.queueCount > 0;
70846
+ }
70847
+ /**
70848
+ * 上游是不是在说「额度用尽」。
70849
+ *
70850
+ * 认两种凭据:业务码 112,以及 403 搭配的额度信号(\`isQuotaExceeded\` / 购买页)。
70851
+ * 额度用尽不是「这个账号坏了」,所以它归 QUOTA 而不是 AUTH——它只能等周期重置或加购。
70852
+ */
70853
+ function isQoderQuotaExhausted(facts, status) {
70854
+ if (facts.codes.includes("112")) return true;
70855
+ if (status !== 403) return false;
70856
+ return facts.quotaExceeded === true || facts.pricingUrl !== void 0;
70857
+ }
70858
+ /** 上游排队 / 资源不足的文案:说清「不是凭据问题」,并带上上游给的排队事实。 */
70859
+ function qoderResourceLimitMessage(facts, status) {
70860
+ const details = [];
70861
+ const codes = [...facts.subCodes, ...facts.codes].filter((code) => code !== "" && code !== String(status));
70862
+ if (codes.length > 0) details.push("code " + codes.join(" / "));
70863
+ if (facts.queueCount !== void 0) details.push("队列 " + facts.queueCount + " 人");
70864
+ if (facts.retryAfterSeconds !== void 0) details.push("上游建议 " + facts.retryAfterSeconds + " 秒后再试");
70865
+ if (facts.modelKey !== void 0) details.push("modelKey " + facts.modelKey);
70866
+ return "Qoder(订阅) 上游资源不足,这个模型正在排队" + (details.length === 0 ? "" : ":" + details.join(",")) + "。这是上游容量问题,不是凭据问题——账号仍是登录状态,不需要重新登录;稍后重试即可。";
70867
+ }
70868
+ /** 上游额度用尽的文案:额度恢复前这一请求不会成功,并给出上游给的购买/升级页。 */
70869
+ function qoderQuotaExhaustedMessage(facts, status) {
70870
+ const details = [];
70871
+ const codes = facts.codes.filter((code) => code !== "" && code !== String(status));
70872
+ if (codes.length > 0) details.push("code " + codes.join(" / "));
70873
+ const url = facts.pricingUrl ?? facts.upgradeUrl;
70874
+ if (url !== void 0) details.push(url);
70875
+ return "Qoder(订阅) 额度已用尽,当前请求被上游拒绝" + (details.length === 0 ? "" : ":" + details.join(",")) + "。额度恢复之前这一请求不会成功;可以等周期重置,或在额度页升级套餐。";
70505
70876
  }
70506
70877
  /**
70507
70878
  * 把一次失败(非 2xx 响应,或信封里的错误状态)分类成 DSH failure。
70508
70879
  *
70509
70880
  * 错误码是 harness 的共享词汇而不是本线路自己的:号池认得 \`AUTH\`
70510
70881
  * (「这个账号必须重新登录」),而重试集合不会捡起一次套餐拒绝。
70511
- * Qoder 的两个业务码有自己的归处:\`103\`(重复 request_id)不是可安全
70512
- * 自动重试的状态(qodercli2api §8 原话);\`105\`(Login expired)归为 AUTH。
70882
+ * Qoder 的业务码各有归处:
70883
+ *
70884
+ * - \`103\`(重复 request_id)不是可安全自动重试的状态(qodercli2api §8 原话);
70885
+ * - \`105\`(Login expired)归 AUTH;
70886
+ * - \`10605\` / \`RESOURCE_LIMIT\`(上游排队、资源不足)归 **SERVER**——它描述的是
70887
+ * 上游容量,换账号与重新登录都救不了,退避重试才有意义(与 kimi-code 的
70888
+ * 「引擎过载」同一判断:过载不是账号被限流,所以不借 RATE_LIMIT 的语义);
70889
+ * - \`112\`(额度用尽,带 pricingUrl)归 **QUOTA**——额度恢复前这一请求不可能成功,
70890
+ * 而 DSH 正是用这个码显示「当前请求的额度已用尽」。
70891
+ *
70892
+ * 剩下认不出原因的 403 仍然归 AUTH:那时「凭据被拒」是上游最常见的那一种。
70893
+ * 但把一次排队误报成「API 密钥无效」会让人去重建一把根本没坏的令牌,
70894
+ * 所以上面两条必须先于它判掉。
70513
70895
  */
70514
70896
  function classifyQoderFailure(status, detail) {
70515
- const trimmed = detail.trim().slice(0, MAX_FAILURE_DETAIL_LENGTH);
70516
- const suffix = trimmed === "" ? "" : ": " + trimmed;
70517
- const code = qoderErrorCode(trimmed);
70518
- if (status === 401 || status === 403 || code === "105") return new LlmError$1("Qoder(订阅) rejected the credential (" + status + (code === void 0 ? "" : ", code " + code) + "). Sign the account in again." + suffix, "AUTH", { status });
70519
- if (code === "103") return new LlmError$1("Qoder(订阅) rejected a duplicated request id (code 103)." + suffix, "PROVIDER_ERROR", { status });
70520
- if (status === 429) return new LlmError$1("Qoder(订阅) rate limit or plan limit (" + status + ")." + suffix, "RATE_LIMIT", { status });
70521
- if (isHttpContextOverflow(status, detail)) return new LlmError$1("Qoder(订阅) says the conversation no longer fits the model window." + suffix, CONTEXT_OVERFLOW_CODE, { status });
70522
- if (status === 404) return new LlmError$1("Qoder(订阅) does not know this model or route (" + status + ")." + suffix, "NOT_FOUND", { status });
70523
- if (status >= 500) return new LlmError$1("Qoder(订阅) upstream error (" + status + ")." + suffix, "SERVER", { status });
70524
- return new LlmError$1("Qoder(订阅) request failed (" + status + ")." + suffix, "PROVIDER_ERROR", { status });
70897
+ return classifyQoderFailureFacts(status, detail.trim().slice(0, MAX_FAILURE_DETAIL_LENGTH), qoderFailureFacts([safeJsonParse(detail)]));
70898
+ }
70899
+ /**
70900
+ * 信封路径的失败分类。
70901
+ *
70902
+ * 上游把排队人数、\`subCode\` 这些事实放在**信封层**还是 body 里都可能(body 本身
70903
+ * 还是一段 JSON 字符串,要再解析一次),所以两处都扫:只读 body 会把「排队」读成
70904
+ * 「凭据被拒」。
70905
+ */
70906
+ function classifyQoderEnvelopeFailure(status, envelope) {
70907
+ const bodyText = asString$2(envelope.body) ?? "";
70908
+ return classifyQoderFailureFacts(status, bodyText.trim().slice(0, MAX_FAILURE_DETAIL_LENGTH), qoderFailureFacts([envelope, safeJsonParse(bodyText)]));
70909
+ }
70910
+ /** 事实齐了以后的那次判定(AUTH / QUOTA / SERVER 的先后是硬要求)。 */
70911
+ function classifyQoderFailureFacts(status, detail, facts) {
70912
+ const suffix = detail === "" ? "" : ": " + detail;
70913
+ const code = facts.codes.length > 0 ? facts.codes[0] : void 0;
70914
+ const options = status === void 0 ? {} : { status };
70915
+ if (status === 401 || facts.codes.includes("105")) return new LlmError$1("Qoder(订阅) rejected the credential (" + (status ?? "no status") + (code === void 0 ? "" : ", code " + code) + "). Sign the account in again." + suffix, "AUTH", options);
70916
+ if (isQoderQuotaExhausted(facts, status)) return new LlmError$1(qoderQuotaExhaustedMessage(facts, status) + suffix, "QUOTA", options);
70917
+ if (isQoderResourceLimited(facts, status)) {
70918
+ const after = facts.retryAfterSeconds;
70919
+ return new LlmError$1(qoderResourceLimitMessage(facts, status) + suffix, "SERVER", after === void 0 || after <= 0 ? options : {
70920
+ ...options,
70921
+ providerRetryAfterMs: after * 1e3
70922
+ });
70923
+ }
70924
+ if (status === 403) return new LlmError$1("Qoder(订阅) rejected the credential (" + status + (code === void 0 ? "" : ", code " + code) + "). Sign the account in again." + suffix, "AUTH", options);
70925
+ if (code === "103") return new LlmError$1("Qoder(订阅) rejected a duplicated request id (code 103)." + suffix, "PROVIDER_ERROR", options);
70926
+ if (status === 429) return new LlmError$1("Qoder(订阅) rate limit or plan limit (" + status + ")." + suffix, "RATE_LIMIT", options);
70927
+ if (status !== void 0 && isHttpContextOverflow(status, detail)) return new LlmError$1("Qoder(订阅) says the conversation no longer fits the model window." + suffix, CONTEXT_OVERFLOW_CODE, options);
70928
+ if (status === 404) return new LlmError$1("Qoder(订阅) does not know this model or route (" + status + ")." + suffix, "NOT_FOUND", options);
70929
+ if (status !== void 0 && status >= 500) return new LlmError$1("Qoder(订阅) upstream error (" + status + ")." + suffix, "SERVER", options);
70930
+ return new LlmError$1("Qoder(订阅) request failed (" + (status ?? "no status") + ")." + suffix, "PROVIDER_ERROR", options);
70525
70931
  }
70526
70932
  //#endregion
70527
70933
  //#region src/host/qoder/adapter.ts
@@ -73021,8 +73427,7 @@ const MODEL_CATALOG$1 = Object.freeze({
73021
73427
  output: .28
73022
73428
  },
73023
73429
  contextWindow: 1048576,
73024
- input: ["text", "image"],
73025
- reasoning: false
73430
+ input: ["text", "image"]
73026
73431
  },
73027
73432
  "mimo-v2.6-pro": {
73028
73433
  name: "MiMo V2.6 Pro",
@@ -73031,8 +73436,7 @@ const MODEL_CATALOG$1 = Object.freeze({
73031
73436
  output: .87
73032
73437
  },
73033
73438
  contextWindow: 1048576,
73034
- input: ["text", "image"],
73035
- reasoning: false
73439
+ input: ["text", "image"]
73036
73440
  },
73037
73441
  "minimax-m3.1-flash": {
73038
73442
  name: "MiniMax M3.1 Flash",
@@ -73076,8 +73480,7 @@ const MODEL_CATALOG$1 = Object.freeze({
73076
73480
  },
73077
73481
  contextWindow: 1e6,
73078
73482
  maxTokens: 65536,
73079
- input: ["text"],
73080
- reasoning: false
73483
+ input: ["text"]
73081
73484
  }
73082
73485
  });
73083
73486
  /** Subscription models current at release time; the live catalog extends this. */
@@ -73099,6 +73502,96 @@ function catalogEntry$1(model) {
73099
73502
  return MODEL_CATALOG$1[model];
73100
73503
  }
73101
73504
  //#endregion
73505
+ //#region src/host/opendesign/catalog.ts
73506
+ /**
73507
+ * Merge the shipped table, the last gateway listing and the user's per-model
73508
+ * override into the descriptor the harness validates.
73509
+ *
73510
+ * Context window and output cap are provider facts, not preferences: the harness
73511
+ * sizes its prompt budget against `context.contextWindow`, so an understated
73512
+ * window silently truncates history. This line ships no per-model windows (the
73513
+ * gateway publishes none to an API key), so the window is the line default until
73514
+ * a live listing or a user override states one.
73515
+ *
73516
+ * @module dsh-chatgpt-subscription/opendesign-catalog
73517
+ */
73518
+ /** The wire's whole modality vocabulary; a listing may claim more than it accepts. */
73519
+ const SEAM_MODALITIES = /* @__PURE__ */ new Set(["text", "image"]);
73520
+ function definedOnly(value) {
73521
+ if (value === void 0) return {};
73522
+ const result = {};
73523
+ for (const [key, entry] of Object.entries(value)) if (entry !== void 0 && entry !== null) result[key] = entry;
73524
+ return result;
73525
+ }
73526
+ /**
73527
+ * The output budget to advertise for one model.
73528
+ *
73529
+ * `defaultMaxTokens` is what a request asks for, not what the model can emit,
73530
+ * and the harness spends it twice: it fills a request's `max_tokens` from it
73531
+ * and reserves the same number as completion room before deciding to compact.
73532
+ * So a published ceiling clamps the configured budget rather than becoming it,
73533
+ * and a budget is never taken past half the window.
73534
+ */
73535
+ function resolveOutputBudget(budget, ceiling, contextWindow) {
73536
+ const wanted = Number.isFinite(budget) && (budget ?? 0) > 0 ? budget : void 0;
73537
+ const most = Number.isFinite(ceiling) && (ceiling ?? 0) > 0 ? ceiling : void 0;
73538
+ const chosen = wanted === void 0 ? most : most === void 0 ? wanted : Math.min(wanted, most);
73539
+ if (chosen === void 0) return void 0;
73540
+ const half = Number.isFinite(contextWindow) && contextWindow > 0 ? Math.floor(contextWindow / 2) : void 0;
73541
+ return half === void 0 ? chosen : Math.min(chosen, half);
73542
+ }
73543
+ /**
73544
+ * Whether the subscription is known to serve one model.
73545
+ *
73546
+ * `servedModelIds === null` is "nobody has ever measured this plan": the absence
73547
+ * of a fact is not a denial, so nothing is hidden on its strength and the shipped
73548
+ * table stands in exactly as it did before this fact existed. An array is a
73549
+ * measurement taken from a successful scan, and an id outside it is a model this
73550
+ * plan does not serve — it stays in the editor as a described model, it just stops
73551
+ * being advertised.
73552
+ *
73553
+ * The user's own override lives one layer up (`forceVisibleModels`): this function
73554
+ * answers what the plan said, never what the user decided.
73555
+ */
73556
+ function isModelServed(model, servedModelIds) {
73557
+ return servedModelIds === null || servedModelIds.includes(model);
73558
+ }
73559
+ /**
73560
+ * Resolve the metadata one model is advertised with.
73561
+ *
73562
+ * Precedence per field: the user's override, then the last gateway listing,
73563
+ * then the shipped table, then the route-wide fallback. The listing sits above
73564
+ * the shipped table for the fields the table cannot carry (window, modalities):
73565
+ * the table's windows do not exist to be beaten, and its names/prices are
73566
+ * curated, so a listing that states no name keeps the table's.
73567
+ */
73568
+ function resolveModelInfo(model, options) {
73569
+ const synced = options.synced.find((entry) => entry.id === model);
73570
+ const shipped = catalogEntry$1(model);
73571
+ const published = {
73572
+ ...definedOnly(shipped),
73573
+ ...definedOnly(synced)
73574
+ };
73575
+ const fallbackWindow = options.defaultContextWindow ?? 128e3;
73576
+ const contextWindow = options.contextWindowOverride ?? published.contextWindow ?? fallbackWindow;
73577
+ const maxTokens = resolveOutputBudget(options.defaultMaxTokens ?? 64e3, published.maxTokens, contextWindow);
73578
+ const reasoning = published.reasoning ?? shipped?.reasoning ?? true;
73579
+ 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);
73580
+ const defaultEffort = reasoning && shipped?.defaultEffort !== void 0 && efforts.includes(shipped.defaultEffort) ? shipped.defaultEffort : void 0;
73581
+ const input = (Array.isArray(published.input) && published.input.length > 0 ? published.input : shipped?.input ?? ["text"]).filter((entry) => SEAM_MODALITIES.has(entry));
73582
+ return {
73583
+ id: model,
73584
+ displayName: String(published.name ?? "").trim() || model,
73585
+ contextWindow,
73586
+ maxTokens: maxTokens ?? options.defaultMaxTokens ?? 64e3,
73587
+ reasoning,
73588
+ ...reasoning ? { efforts } : { efforts: [] },
73589
+ ...defaultEffort === void 0 ? {} : { defaultEffort },
73590
+ input: input.length > 0 ? input : ["text"],
73591
+ ...shipped?.pricing === void 0 ? {} : { pricing: shipped.pricing }
73592
+ };
73593
+ }
73594
+ //#endregion
73102
73595
  //#region src/host/opendesign/token-store.ts
73103
73596
  /** Whether one credential is a device-flow session rather than a pasted key. */
73104
73597
  function isOpenDesignOAuthCredentials(credentials) {
@@ -73254,6 +73747,24 @@ function readCatalogModels(value) {
73254
73747
  }
73255
73748
  return models;
73256
73749
  }
73750
+ /**
73751
+ * One stored id list, normalized: non-empty strings only, deduplicated, and
73752
+ * `null` when the value is not a list at all.
73753
+ *
73754
+ * `null` and `[]` stay distinct here; see the tri-state note on
73755
+ * {@link OpenDesignModelSettings.servedModelIds}.
73756
+ */
73757
+ function readIdList(value) {
73758
+ if (!Array.isArray(value)) return null;
73759
+ const ids = [];
73760
+ for (const entry of value) {
73761
+ if (typeof entry !== "string") continue;
73762
+ const id = entry.trim();
73763
+ if (id === "" || ids.includes(id)) continue;
73764
+ ids.push(id);
73765
+ }
73766
+ return ids;
73767
+ }
73257
73768
  function positiveInteger(value, fallback, allowZero = false) {
73258
73769
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < (allowZero ? 0 : 1)) return fallback;
73259
73770
  return value;
@@ -73268,6 +73779,8 @@ function parseOpenDesignModelSettings(value) {
73268
73779
  enabledModelIds,
73269
73780
  catalogModels: readCatalogModels(record.catalogModels),
73270
73781
  hiddenModels: Array.isArray(record.hiddenModels) ? record.hiddenModels.filter((id) => typeof id === "string") : [],
73782
+ servedModelIds: readIdList(record.servedModelIds),
73783
+ forceVisibleModels: readIdList(record.forceVisibleModels) ?? [],
73271
73784
  contextWindowOverrides: readNumberOverrides(record.contextWindowOverrides),
73272
73785
  defaultContextWindow: positiveInteger(record.defaultContextWindow, DEFAULT_CONTEXT_WINDOW),
73273
73786
  defaultMaxTokens: positiveInteger(record.defaultMaxTokens, DEFAULT_MAX_TOKENS),
@@ -73285,12 +73798,30 @@ function defaultOpenDesignModelSettings() {
73285
73798
  *
73286
73799
  * The file is written atomically (temp + rename) so a crash mid-write leaves the
73287
73800
  * previous document intact rather than a truncated one that reads as "reset all
73288
- * settings".
73801
+ * settings", and writes are SERIALIZED behind one queue so two read→compute→write
73802
+ * cycles can never interleave and erase each other's fields. Reads do not queue:
73803
+ * see {@link OpenDesignModelSettingsStore.pending}.
73289
73804
  */
73290
73805
  var OpenDesignModelSettingsStore = class {
73291
73806
  filePath;
73292
73807
  cached = null;
73293
73808
  loadPromise = null;
73809
+ /**
73810
+ * Tail of the write queue.
73811
+ *
73812
+ * Every mutator derives a WHOLE document from the one it read, so two of them in
73813
+ * flight at once each erase the other's fields. That is not hypothetical here:
73814
+ * the catalog sync is fire-and-forget (`void syncCatalog()` on account add and on
73815
+ * a completed sign-in), so it really can land between a selection change's read
73816
+ * and its write. Each mutation therefore runs its entire read → compute → write
73817
+ * inside one turn of this queue.
73818
+ *
73819
+ * `read()` stays OUTSIDE the queue on purpose: it is the hot path (every status
73820
+ * request, every keystroke-driven checkbox), and waiting behind a write would buy
73821
+ * nothing — by the time a queued operation runs, every write queued before it has
73822
+ * already landed, so the plain read it performs IS the in-lock read.
73823
+ */
73824
+ pending = Promise.resolve();
73294
73825
  constructor(filePath = modelSettingsPath$7()) {
73295
73826
  this.filePath = filePath;
73296
73827
  }
@@ -73300,7 +73831,14 @@ var OpenDesignModelSettingsStore = class {
73300
73831
  status() {
73301
73832
  return this.cached ?? defaultOpenDesignModelSettings();
73302
73833
  }
73303
- /** Read the document at activation time, synchronously. */
73834
+ /**
73835
+ * Read the document at activation time, synchronously.
73836
+ *
73837
+ * Synchronous by contract, so it cannot join the write queue — and it does not
73838
+ * need to: it only READS, a rename makes the file either the old or the new
73839
+ * document (never a half-written one), and a queued write assigns `cached` after
73840
+ * that rename, so a reading taken mid-write is superseded by the write's own.
73841
+ */
73304
73842
  readSync() {
73305
73843
  if (this.cached !== null) return this.cached;
73306
73844
  try {
@@ -73330,7 +73868,30 @@ var OpenDesignModelSettingsStore = class {
73330
73868
  this.cached = defaultOpenDesignModelSettings();
73331
73869
  return this.cached;
73332
73870
  }
73871
+ /**
73872
+ * Run one operation with exclusive access to the document.
73873
+ *
73874
+ * The queue's tail never carries a rejection: an operation that throws rejects
73875
+ * its OWN caller only, and the tail is flattened to a fulfilled promise right
73876
+ * afterwards, so a failed write cannot poison the writes queued behind it.
73877
+ */
73878
+ async exclusive(operation) {
73879
+ const result = this.pending.then(operation);
73880
+ this.pending = result.then(() => void 0, () => void 0);
73881
+ return await result;
73882
+ }
73883
+ /** Replace the whole document, queued behind any write already in flight. */
73333
73884
  async write(settings) {
73885
+ return await this.exclusive(() => this.writeUnlocked(settings));
73886
+ }
73887
+ /**
73888
+ * The file half of a write, for callers that ALREADY hold the queue.
73889
+ *
73890
+ * No public mutator may call another one: awaiting a queued mutator from inside
73891
+ * `exclusive` would wait for a queue position behind itself and deadlock. That is
73892
+ * why every mutator below composes `read()` with THIS, never with `write()`.
73893
+ */
73894
+ async writeUnlocked(settings) {
73334
73895
  await fs.mkdir(path.dirname(this.filePath), { recursive: true });
73335
73896
  const tmp = `${this.filePath}.tmp.${Date.now()}`;
73336
73897
  await fs.writeFile(tmp, JSON.stringify({
@@ -73342,32 +73903,75 @@ var OpenDesignModelSettingsStore = class {
73342
73903
  return settings;
73343
73904
  }
73344
73905
  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;
73906
+ return await this.exclusive(async () => {
73907
+ const current = await this.read();
73908
+ const next = {
73909
+ ...current,
73910
+ ...patch,
73911
+ contextWindowOverrides: patch.contextWindowOverrides === void 0 ? current.contextWindowOverrides : patch.contextWindowOverrides
73912
+ };
73913
+ return await this.writeUnlocked(next);
73914
+ });
73353
73915
  }
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
73916
+ /**
73917
+ * Record a catalog sync without touching the user's selection.
73918
+ *
73919
+ * `servedModelIds` is written only when the caller actually measured it: a sync
73920
+ * in which every scan failed must leave the previous measurement standing rather
73921
+ * than clearing the filter, so `undefined` keeps the stored value while an array
73922
+ * (including an empty one) replaces it.
73923
+ */
73924
+ async storeCatalog(models, servedModelIds) {
73925
+ return await this.exclusive(async () => {
73926
+ const current = await this.read();
73927
+ return await this.writeUnlocked(servedModelIds === void 0 ? {
73928
+ ...current,
73929
+ catalogModels: models
73930
+ } : {
73931
+ ...current,
73932
+ catalogModels: models,
73933
+ servedModelIds
73934
+ });
73935
+ });
73936
+ }
73937
+ /**
73938
+ * Replace the model selection, dropping any forced reveal it excludes.
73939
+ *
73940
+ * One `read` for both the prune input and the write baseline, so the two cannot
73941
+ * disagree: computing the prune outside and then patching would read the document
73942
+ * twice, and a forced reveal added in that gap would be recorded and then erased
73943
+ * by the second read's snapshot.
73944
+ *
73945
+ * The prune belongs to the selection rather than to `models/visibility` because
73946
+ * the card's only un-tick path is the selection patch: it removes the id from the
73947
+ * whitelist and never posts `{visible:false}`, so a forced reveal would otherwise
73948
+ * outlive the tick that asked for it. `null` is "no filter" — not a "keep only
73949
+ * these" statement — so it prunes nothing.
73950
+ */
73951
+ async selectModels(ids) {
73952
+ return await this.exclusive(async () => {
73953
+ const current = await this.read();
73954
+ return await this.writeUnlocked(ids === null ? {
73955
+ ...current,
73956
+ enabledModelIds: null
73957
+ } : {
73958
+ ...current,
73959
+ enabledModelIds: ids,
73960
+ forceVisibleModels: current.forceVisibleModels.filter((id) => ids.includes(id))
73961
+ });
73360
73962
  });
73361
73963
  }
73362
73964
  /** Store or clear (null) one model's context-window override. */
73363
73965
  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
73966
+ return await this.exclusive(async () => {
73967
+ const current = await this.read();
73968
+ const overrides = { ...current.contextWindowOverrides };
73969
+ if (contextWindow === null) delete overrides[model];
73970
+ else overrides[model] = contextWindow;
73971
+ return await this.writeUnlocked({
73972
+ ...current,
73973
+ contextWindowOverrides: overrides
73974
+ });
73371
73975
  });
73372
73976
  }
73373
73977
  };
@@ -73521,80 +74125,6 @@ var OpenDesignAccountPool = class extends AccountPoolCore {
73521
74125
  }
73522
74126
  };
73523
74127
  //#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
74128
  //#region src/host/opendesign/client.ts
73599
74129
  /**
73600
74130
  * Transport for the OpenDesign link gateway. Transport only: no account or
@@ -75019,27 +75549,6 @@ var OpenDesignAdapter = class extends LlmAdapter {
75019
75549
  settings() {
75020
75550
  return this.modelSettings.read();
75021
75551
  }
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
75552
  /** One model's resolved descriptor, from the shipped table plus live data. */
75044
75553
  async modelInfo(modelId) {
75045
75554
  const settings = await this.settings();
@@ -75054,7 +75563,7 @@ var OpenDesignAdapter = class extends LlmAdapter {
75054
75563
  const prov = provider || "opendesign";
75055
75564
  const settings = await this.settings();
75056
75565
  if (settings.enabled === false) return [];
75057
- const ids = this.visibleModelIds(settings);
75566
+ const ids = openDesignVisibleModelIds(settings);
75058
75567
  return await Promise.all(ids.map(async (id) => {
75059
75568
  const info = await this.modelInfo(id);
75060
75569
  return {
@@ -75131,28 +75640,33 @@ var OpenDesignAdapter = class extends LlmAdapter {
75131
75640
  try {
75132
75641
  let response;
75133
75642
  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;
75643
+ for (let transportTry = 0; transportTry <= TRANSPORT_RETRIES; transportTry += 1) {
75644
+ const headerDeadline = withAttemptTimeout(upstream, this.options.headerTimeoutMs);
75645
+ try {
75646
+ response = await openChatStream$1({
75647
+ fetchFn: this.options.fetchFn,
75648
+ baseURL,
75649
+ apiKey,
75650
+ body,
75651
+ signal: headerDeadline.signal
75652
+ });
75653
+ headerDeadline.clear();
75654
+ break;
75655
+ } catch (error) {
75656
+ headerDeadline.clear();
75657
+ if (signal.aborted) throw new LlmError("OpenDesign request aborted by caller", "ABORTED", { cause: error });
75658
+ transportError = error;
75659
+ if (transportTry < TRANSPORT_RETRIES && isPreConnectionFailure(error)) {
75660
+ try {
75661
+ await setTimeout$1(TRANSPORT_RETRY_BASE_MS * (transportTry + 1), void 0, { signal: upstream });
75662
+ } catch (waitError) {
75663
+ if (signal.aborted) throw new LlmError("OpenDesign request aborted by caller", "ABORTED", { cause: waitError });
75664
+ throw waitError;
75665
+ }
75666
+ continue;
75152
75667
  }
75153
- continue;
75668
+ break;
75154
75669
  }
75155
- break;
75156
75670
  }
75157
75671
  if (response === void 0) {
75158
75672
  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 +75734,31 @@ var OpenDesignAdapter = class extends LlmAdapter {
75220
75734
  await this.accountPool.markCooldown(accountId, POOL_COOLDOWN_MS$1, `${PROVIDER_NAME$2} credits exhausted`).catch(() => void 0);
75221
75735
  }
75222
75736
  };
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)]);
75737
+ /**
75738
+ * A caller signal combined with the attempt's CLEARABLE header deadline.
75739
+ *
75740
+ * The clear is not a nicety, it is the difference between a header deadline and
75741
+ * a request deadline. `AbortSignal.timeout` is absolute and cannot be disarmed,
75742
+ * so attaching one to the fetch signal and leaving it there keeps counting
75743
+ * through the whole response body — a model that thinks for more than
75744
+ * `ATTEMPT_TIMEOUT_MS` then dies mid-answer with "The operation was aborted due
75745
+ * to timeout", is retried by the host, and dies again at the same wall clock.
75746
+ * That is exactly what happened here: two attempts streamed reasoning for 155s
75747
+ * and 173s and both were cut 180s after their request went out.
75748
+ *
75749
+ * Cleared after the headers (see the transport loop), at which point the stream
75750
+ * idle watchdog owns liveness: it watches for SILENCE and resets on every
75751
+ * chunk, so a long thinking pass is fine and a dead socket is still caught.
75752
+ */
75753
+ function withAttemptTimeout(signal, timeoutMs = ATTEMPT_TIMEOUT_MS) {
75754
+ const deadline = new AbortController();
75755
+ const timer = setTimeout(() => {
75756
+ deadline.abort(new DOMException("The operation was aborted due to timeout", "TimeoutError"));
75757
+ }, timeoutMs);
75758
+ return {
75759
+ signal: AbortSignal.any([signal, deadline.signal]),
75760
+ clear: () => clearTimeout(timer)
75761
+ };
75226
75762
  }
75227
75763
  /**
75228
75764
  * Whether a transport failure happened before any request was sent.
@@ -75540,6 +76076,15 @@ const OPENDESIGN_ROUTE_PREFIX = "/opendesign/api";
75540
76076
  const OPENDESIGN_CONSOLE_URL = "https://open-design.ai/cloud";
75541
76077
  //#endregion
75542
76078
  //#region src/host/opendesign/routes.ts
76079
+ /**
76080
+ * Accounts one catalog sync asks at most.
76081
+ *
76082
+ * Each account costs up to two upstream reads, so this is what stands between a
76083
+ * twenty-key pool and forty requests behind one button. It bounds the SCAN, not
76084
+ * the pool: an account beyond the cap contributes nothing to the union, because
76085
+ * the cap is a hard bound on the measurement rather than a rotating window.
76086
+ */
76087
+ const MAX_SYNC_ACCOUNTS = 8;
75543
76088
  function sendJson(response, status, body) {
75544
76089
  response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
75545
76090
  response.end(JSON.stringify(body));
@@ -75592,17 +76137,20 @@ function formatModelHint(contextWindow, input, reasoning, efforts, pricing) {
75592
76137
  const base = formatModelCapacity(contextWindow) + " · " + input.join("+") + (ladder === "" ? "" : " · " + ladder);
75593
76138
  return pricing === void 0 ? base : `${base} · $${pricing.input}/$${pricing.output}`;
75594
76139
  }
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. */
76140
+ /**
76141
+ * Every model the card should render, whether or not it is advertised.
76142
+ *
76143
+ * `enabled` is the state of the card's checklist row, and it is read from the ONE
76144
+ * visibility rule (`openDesignVisibleModelIds` in ./line.ts) — the very function
76145
+ * the adapter offers the picker with. A row that reads "checked" while no picker
76146
+ * offers it is the contradiction this projection must not be able to produce, so
76147
+ * the two are not two calculations: a model the plan withholds reads as unchecked
76148
+ * until the user forces it, because that is what the picker does with it.
76149
+ */
75602
76150
  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);
76151
+ const known = openDesignKnownModelIds(settings);
76152
+ const visible = new Set(openDesignVisibleModelIds(settings));
76153
+ const forcedVisible = new Set(settings.forceVisibleModels);
75606
76154
  return known.map((id) => {
75607
76155
  const base = resolveModelInfo(id, {
75608
76156
  synced: settings.catalogModels,
@@ -75615,6 +76163,7 @@ function projectModels(settings) {
75615
76163
  defaultMaxTokens: settings.defaultMaxTokens,
75616
76164
  contextWindowOverride: settings.contextWindowOverrides[id]
75617
76165
  });
76166
+ const served = isModelServed(id, settings.servedModelIds);
75618
76167
  return {
75619
76168
  id,
75620
76169
  name: info.displayName,
@@ -75624,7 +76173,9 @@ function projectModels(settings) {
75624
76173
  maxTokens: info.maxTokens,
75625
76174
  reasoning: info.reasoning,
75626
76175
  input: [...info.input],
75627
- enabled: (enabledSet === null || enabledSet.has(id)) && !hidden.has(id),
76176
+ enabled: visible.has(id),
76177
+ served,
76178
+ forcedVisible: forcedVisible.has(id),
75628
76179
  ...info.pricing === void 0 ? {} : { pricing: info.pricing },
75629
76180
  ...settings.contextWindowOverrides[id] === void 0 ? {} : { contextWindowOverride: settings.contextWindowOverrides[id] }
75630
76181
  };
@@ -75717,16 +76268,22 @@ function registerOpenDesignRoutes(ctx, options) {
75717
76268
  };
75718
76269
  const status = async () => await getOpenDesignWebStatus(options, usageStore);
75719
76270
  /**
75720
- * Sync the live catalog with the first usable account's key.
76271
+ * Sync the live catalog with every usable account's key.
75721
76272
  *
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.
76273
+ * The gateway's listing is the authority on what a key's plan serves, and a plan
76274
+ * belongs to an ACCOUNT rather than to the line: two pooled keys can serve two
76275
+ * different model sets, so every account is asked and the union is what gets
76276
+ * remembered. That union is persisted as `servedModelIds` — the fact the picker
76277
+ * needs and the one this sync used to throw away — and it is written only when a
76278
+ * scan really succeeded, so a network failure cannot switch the filter off.
76279
+ *
76280
+ * A failed or refused read changes nothing (the shipped table still stands in),
76281
+ * and the sync's answer says which happened. Announces only a real change: the
76282
+ * picker rebuilds on `llm/adapters-updated`, so a no-change sync must stay
76283
+ * silent.
75727
76284
  */
75728
76285
  const syncCatalog = async () => {
75729
- const known = uniqueModels(await settingsStore.read());
76286
+ const known = openDesignKnownModelIds(await settingsStore.read());
75730
76287
  const data = await accountPool.read().catch(() => null);
75731
76288
  const now = Date.now();
75732
76289
  const accounts = (data?.accounts ?? []).filter((account) => isUsable(account, now));
@@ -75737,27 +76294,22 @@ function registerOpenDesignRoutes(ctx, options) {
75737
76294
  };
75738
76295
  const scans = [];
75739
76296
  let controlRefused = false;
75740
- for (const account of accounts) {
76297
+ const settings = await settingsStore.read();
76298
+ for (const account of accounts.slice(0, MAX_SYNC_ACCOUNTS)) {
75741
76299
  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;
76300
+ if (controlKey !== void 0) {
76301
+ const scan = await fetchControlPlaneCatalog({
76302
+ fetchFn,
76303
+ controlKey,
76304
+ knownIds: known
76305
+ });
76306
+ if (scan === null) controlRefused = true;
76307
+ else if (scan.ids.length > 0) scans.push(scan);
75751
76308
  }
75752
- if (scan.ids.length > 0) scans.push(scan);
75753
- break;
75754
- }
75755
- {
75756
- const settings = await settingsStore.read();
75757
76309
  const gateway = await fetchGatewayCatalog$1({
75758
76310
  fetchFn,
75759
76311
  baseURL: settings.baseURL,
75760
- apiKey: bearerFor(accounts[0].credentials)
76312
+ apiKey: bearerFor(account.credentials)
75761
76313
  });
75762
76314
  if (gateway !== null && gateway.ids.length > 0) scans.push(gateway);
75763
76315
  }
@@ -75787,9 +76339,10 @@ function registerOpenDesignRoutes(ctx, options) {
75787
76339
  const merged = [...existing.values()].filter((entry) => {
75788
76340
  return !current.catalogModels.some((old) => old.id === entry.id) || syncedIds.has(entry.id);
75789
76341
  });
76342
+ const served = [...new Set(scans.flatMap((scan) => scan.ids))];
75790
76343
  const fingerprintBefore = current.catalogModels.map((entry) => entry.id).join("\n");
75791
76344
  const fingerprintAfter = merged.map((entry) => entry.id).join("\n");
75792
- await settingsStore.storeCatalog(merged);
76345
+ await settingsStore.storeCatalog(merged, served);
75793
76346
  if (fingerprintBefore !== fingerprintAfter) emitModelsUpdated();
75794
76347
  return {
75795
76348
  added,
@@ -76033,8 +76586,7 @@ function registerOpenDesignRoutes(ctx, options) {
76033
76586
  const body = await readRequestJson(request).catch(() => ({}));
76034
76587
  let touched = false;
76035
76588
  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 });
76589
+ await settingsStore.selectModels(body.enabledModelIds === null ? null : body.enabledModelIds.filter((id) => typeof id === "string"));
76038
76590
  touched = true;
76039
76591
  }
76040
76592
  if (typeof body.enabled === "boolean") {
@@ -76055,16 +76607,24 @@ function registerOpenDesignRoutes(ctx, options) {
76055
76607
  });
76056
76608
  const body = await readRequestJson(request).catch(() => ({}));
76057
76609
  const current = await settingsStore.read();
76058
- const known = uniqueModels(current);
76610
+ const known = openDesignKnownModelIds(current);
76059
76611
  const hidden = new Set(current.hiddenModels);
76612
+ const forced = new Set(current.forceVisibleModels);
76060
76613
  if (path === "models/visibility") {
76061
76614
  const model = typeof body.model === "string" ? body.model.trim() : "";
76062
76615
  if (!known.includes(model)) return sendJson(response, 400, {
76063
76616
  ok: false,
76064
76617
  error: `${JSON.stringify(model)} is not one of this route's models`
76065
76618
  });
76066
- if (body.visible === false) hidden.add(model);
76067
- else hidden.delete(model);
76619
+ const served = isModelServed(model, current.servedModelIds);
76620
+ if (body.visible === false) {
76621
+ if (served) hidden.add(model);
76622
+ forced.delete(model);
76623
+ } else {
76624
+ hidden.delete(model);
76625
+ if (served) forced.delete(model);
76626
+ else forced.add(model);
76627
+ }
76068
76628
  } else {
76069
76629
  const mode = body.mode;
76070
76630
  if (mode !== "all" && mode !== "none" && mode !== "invert") return sendJson(response, 400, {
@@ -76077,7 +76637,11 @@ function registerOpenDesignRoutes(ctx, options) {
76077
76637
  const wasHidden = current.hiddenModels.includes(id);
76078
76638
  return mode === "all" ? false : mode === "none" ? true : !wasHidden;
76079
76639
  }) : [...hidden];
76080
- await settingsStore.update({ hiddenModels: nextHidden });
76640
+ const nextForced = path !== "models/visibility-all" ? [...forced] : body.mode === "invert" ? current.forceVisibleModels : [];
76641
+ await settingsStore.update({
76642
+ hiddenModels: nextHidden,
76643
+ forceVisibleModels: nextForced
76644
+ });
76081
76645
  emitModelsUpdated();
76082
76646
  return sendJson(response, 200, {
76083
76647
  ok: true,
@@ -76285,18 +76849,30 @@ function openDesignKnownModelIds(settings) {
76285
76849
  return [...ids];
76286
76850
  }
76287
76851
  /**
76288
- * Every id the line advertises right now.
76852
+ * Every id the line advertises right now — THE one visibility rule.
76853
+ *
76854
+ * The adapter (what the picker is offered), the status route (what each checklist
76855
+ * row reports as checked) and the hub card (how many models the line advertises)
76856
+ * all read this function and nothing else. Three private copies is how the hub
76857
+ * card ended up counting models the picker refused to show, which is exactly the
76858
+ * contradiction this rule exists to make impossible.
76859
+ *
76860
+ * Three layers, applied in this order:
76289
76861
  *
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.
76862
+ * 1. the user's hidden list;
76863
+ * 2. the user's tick list — unset means every known model, an explicit `[]` means
76864
+ * everything off (the tick list and the shipped table speak the same wire ids,
76865
+ * so a cold-start intersection cannot empty the picker);
76866
+ * 3. the PLAN, once somebody measured it: an id the last successful scan did not
76867
+ * list is not served, and only a forced reveal (`forceVisibleModels`) keeps it.
76868
+ * Before that first scan `servedModelIds` is `null` and this layer hides
76869
+ * nothing — an unmeasured plan is not a denial.
76295
76870
  */
76296
76871
  function openDesignVisibleModelIds(settings) {
76297
76872
  const hidden = new Set(settings.hiddenModels);
76298
76873
  const enabled = settings.enabledModelIds === null ? null : new Set(settings.enabledModelIds);
76299
- return openDesignKnownModelIds(settings).filter((id) => !hidden.has(id) && (enabled === null || enabled.has(id)));
76874
+ const forced = new Set(settings.forceVisibleModels);
76875
+ return openDesignKnownModelIds(settings).filter((id) => !hidden.has(id) && (enabled === null || enabled.has(id)) && (isModelServed(id, settings.servedModelIds) || forced.has(id)));
76300
76876
  }
76301
76877
  /**
76302
76878
  * Build the line.