@eddyskywalker/dsh-chatgpt-subscription 0.57.0 → 0.59.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 (60) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +3 -3
  3. package/lib/client.js +136 -23
  4. package/lib/client.js.map +1 -1
  5. package/lib/codemode.js +1565 -0
  6. package/lib/index.js +1121 -530
  7. package/lib/tool-result-pruner.js +1 -1
  8. package/lib/types/client/CodexImageToolView.d.ts.map +1 -1
  9. package/lib/types/client/minimax-code/commands-contract.d.ts +1 -1
  10. package/lib/types/client/progress-updates/progress-row.d.ts.map +1 -1
  11. package/lib/types/client/styles.d.ts.map +1 -1
  12. package/lib/types/compat.d.ts +10 -1
  13. package/lib/types/compat.d.ts.map +1 -1
  14. package/lib/types/entry-codemode.d.ts +37 -0
  15. package/lib/types/entry-codemode.d.ts.map +1 -0
  16. package/lib/types/entry-tool-result-pruner.d.ts +1 -1
  17. package/lib/types/host/agent-preset.d.ts +24 -2
  18. package/lib/types/host/agent-preset.d.ts.map +1 -1
  19. package/lib/types/host/antigravity/image-tool.d.ts +10 -2
  20. package/lib/types/host/antigravity/image-tool.d.ts.map +1 -1
  21. package/lib/types/host/cache-warmup/install.d.ts +10 -4
  22. package/lib/types/host/cache-warmup/install.d.ts.map +1 -1
  23. package/lib/types/host/cache-warmup/projection.d.ts +17 -0
  24. package/lib/types/host/cache-warmup/projection.d.ts.map +1 -1
  25. package/lib/types/host/codemode/discovery.d.ts +33 -0
  26. package/lib/types/host/codemode/discovery.d.ts.map +1 -0
  27. package/lib/types/host/codemode/executor.d.ts +140 -0
  28. package/lib/types/host/codemode/executor.d.ts.map +1 -0
  29. package/lib/types/host/codemode/retention.d.ts +34 -0
  30. package/lib/types/host/codemode/retention.d.ts.map +1 -0
  31. package/lib/types/host/codemode/rules.d.ts +183 -0
  32. package/lib/types/host/codemode/rules.d.ts.map +1 -0
  33. package/lib/types/host/codemode/scope.d.ts +59 -0
  34. package/lib/types/host/codemode/scope.d.ts.map +1 -0
  35. package/lib/types/host/codemode/transport.d.ts +258 -0
  36. package/lib/types/host/codemode/transport.d.ts.map +1 -0
  37. package/lib/types/host/codex-account-pool.d.ts.map +1 -1
  38. package/lib/types/host/codex-adopt.d.ts.map +1 -1
  39. package/lib/types/host/codex-catalog.d.ts +5 -3
  40. package/lib/types/host/codex-catalog.d.ts.map +1 -1
  41. package/lib/types/host/codex-identity.d.ts +130 -0
  42. package/lib/types/host/codex-identity.d.ts.map +1 -0
  43. package/lib/types/host/codex-images.d.ts +12 -0
  44. package/lib/types/host/codex-images.d.ts.map +1 -1
  45. package/lib/types/host/kimi-code/repeat-breaker.d.ts +1 -1
  46. package/lib/types/host/oauth-service.d.ts +0 -16
  47. package/lib/types/host/oauth-service.d.ts.map +1 -1
  48. package/lib/types/host/token-store.d.ts +9 -0
  49. package/lib/types/host/token-store.d.ts.map +1 -1
  50. package/lib/types/host/usage-service.d.ts.map +1 -1
  51. package/lib/types/host/workbuddy/adapter.d.ts.map +1 -1
  52. package/lib/types/host/workbuddy/client.d.ts.map +1 -1
  53. package/lib/types/host/workbuddy/model-catalog.d.ts +56 -44
  54. package/lib/types/host/workbuddy/model-catalog.d.ts.map +1 -1
  55. package/lib/types/host/workbuddy/routes.d.ts.map +1 -1
  56. package/lib/types/shared/workbuddy-contracts.d.ts +8 -8
  57. package/package.json +54 -46
  58. package/presets/dispatch/README.md +12 -3
  59. package/presets/dispatch/agent.cordis.yml +10 -9
  60. package/presets/dispatch/preset.yml +1 -1
package/lib/index.js CHANGED
@@ -9,7 +9,7 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
9
9
  import { z } from "zod";
10
10
  import z$1 from "@deepseek-ai/schemastery";
11
11
  import fs, { chmod, lstat, mkdir, open, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
12
- import path, { basename, dirname, join, relative, resolve } from "node:path";
12
+ import path, { basename, dirname, extname, join, relative, resolve } from "node:path";
13
13
  import os, { homedir, platform, userInfo } from "node:os";
14
14
  import { constants, createCipheriv, createDecipheriv, createHash, createHmac, publicEncrypt, randomBytes, randomUUID } from "node:crypto";
15
15
  import { isDeepStrictEqual } from "node:util";
@@ -807,11 +807,20 @@ const ROUTE_PREFIX$5 = "/api/dsh-chatgpt-subscription";
807
807
  * `test/responses-client.test.ts` asserts the two agree, so a release that
808
808
  * forgets this constant fails the suite instead of shipping quietly.
809
809
  */
810
- const PLUGIN_VERSION = "0.57.0";
810
+ const PLUGIN_VERSION = "0.59.0";
811
811
  const CODEX_CHATGPT_PROVIDER_ID = "codex-chatgpt";
812
812
  const CODEX_API_BASE = "https://chatgpt.com/backend-api/codex";
813
813
  const CODEX_RESPONSES_URL = `${CODEX_API_BASE}/responses`;
814
814
  const CODEX_IMAGE_GENERATION_URL = `${CODEX_API_BASE}/images/generations`;
815
+ /**
816
+ * The edit route: the same image surface, addressed for reference-image input.
817
+ *
818
+ * A generation has nothing to modify and goes to `/images/generations`; a
819
+ * request that names reference images is an EDIT and goes here, with those
820
+ * images carried inline in a JSON `images` array. It is a separate route, not a
821
+ * flag on the generation body, so the two are stated as two constants.
822
+ */
823
+ const CODEX_IMAGE_EDIT_URL = `${CODEX_API_BASE}/images/edits`;
815
824
  const CODEX_SEARCH_URL = `${CODEX_API_BASE}/alpha/search`;
816
825
  const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
817
826
  /**
@@ -1345,6 +1354,144 @@ async function rehydrateCatalogCache(name, parseModels, accept, currentFetchedAt
1345
1354
  return snapshot.fetchedAt;
1346
1355
  }
1347
1356
  //#endregion
1357
+ //#region src/host/codex-identity.ts
1358
+ /**
1359
+ * Who one ChatGPT credential belongs to, and what makes two sign-ins the same
1360
+ * account.
1361
+ *
1362
+ * TWO IDENTITIES, AND THEY ARE NOT THE SAME THING.
1363
+ *
1364
+ * - The WORKSPACE identity is `chatgpt_account_id`, carried as
1365
+ * `credentials.accountId` and sent upstream as `chatgpt-account-id`. It
1366
+ * decides which workspace a request is billed to and what its listing serves,
1367
+ * and every member of that workspace shares it.
1368
+ * - The LOCAL LOGIN identity is (workspace, user): that same workspace id plus a
1369
+ * stable id for the person who signed in. Two members of one workspace are two
1370
+ * accounts here — separate tokens, separate rows, separately selectable — and
1371
+ * re-authorizing the same person updates the row that person already owns.
1372
+ *
1373
+ * Keying the account pool on the workspace alone is the bug this module exists to
1374
+ * prevent. Adding a second member of a workspace replaced the first: the card read
1375
+ * "1 account signed in", the earlier sign-in was gone from the list, and nothing in
1376
+ * the credential was wrong — the pool simply believed both sign-ins were one
1377
+ * account, because that is what the key said.
1378
+ *
1379
+ * The user id is read from the tokens themselves, most explicit claim first:
1380
+ * `chatgpt_user_id` (the field upstream states for exactly this purpose), then
1381
+ * the OIDC `sub` that an id token always carries, then the address. Only when none
1382
+ * of the three is present does the workspace stand in on its own, because nothing
1383
+ * better exists then and merging is what every row written before this module
1384
+ * already did. The workspace id alone is NEVER treated as a user identity while
1385
+ * some user claim is available.
1386
+ */
1387
+ /** Namespace of the ChatGPT auth claims inside an id or access token. */
1388
+ const CODEX_AUTH_CLAIMS_KEY = "https://api.openai.com/auth";
1389
+ /**
1390
+ * The claims of a JWT, decoded in process, or nothing.
1391
+ *
1392
+ * The payload is the second base64url segment; the signature is never touched and
1393
+ * the result is used only for the non-secret facts above. Never sent anywhere: a
1394
+ * token handed to a decoder service is a token handed away.
1395
+ *
1396
+ * No verification is claimed and none is possible here. What this does instead is
1397
+ * refuse to invent: a token that does not decode is treated as stating nothing,
1398
+ * which leaves the caller on its own fallback rather than on a guessed identity.
1399
+ */
1400
+ function parseJwtClaims(token) {
1401
+ if (token === void 0) return void 0;
1402
+ const parts = token.split(".");
1403
+ if (parts.length !== 3) return void 0;
1404
+ try {
1405
+ const value = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
1406
+ return typeof value === "object" && value !== null ? value : void 0;
1407
+ } catch {
1408
+ return;
1409
+ }
1410
+ }
1411
+ /**
1412
+ * What the tokens state about their owner.
1413
+ *
1414
+ * The id token is read before the access token, because it is the document that
1415
+ * describes the sign-in; the access token is the fallback for credentials stored
1416
+ * without one. First statement wins, so a value the id token states is never
1417
+ * overwritten by the access token's copy of it.
1418
+ */
1419
+ function extractIdentity(credentials) {
1420
+ const result = {};
1421
+ let subject;
1422
+ for (const token of [credentials.idToken, credentials.accessToken]) {
1423
+ const claims = parseJwtClaims(token);
1424
+ if (claims === void 0) continue;
1425
+ const nested = claims[CODEX_AUTH_CLAIMS_KEY];
1426
+ result.email ??= stringClaim(claims.email);
1427
+ result.planType ??= stringClaim(claims.chatgpt_plan_type) ?? stringClaim(nested?.chatgpt_plan_type);
1428
+ result.userId ??= stringClaim(claims.chatgpt_user_id) ?? stringClaim(nested?.chatgpt_user_id);
1429
+ result.accountId ??= stringClaim(claims.chatgpt_account_id) ?? stringClaim(nested?.chatgpt_account_id) ?? stringClaim(claims.organizations?.[0]?.id) ?? stringClaim(nested?.organizations?.[0]?.id);
1430
+ subject ??= stringClaim(claims.sub);
1431
+ }
1432
+ result.userId ??= subject;
1433
+ return result;
1434
+ }
1435
+ /**
1436
+ * The stable id of the human behind a credential, or nothing.
1437
+ *
1438
+ * Explicit claim first, then `sub`, and deliberately NOT the email: an address is
1439
+ * the caller's fallback (see {@link codexAccountIdentity}), while this function
1440
+ * answers the narrower question "does this credential name its user outright?".
1441
+ * A persisted `userId` wins over anything re-derived from the tokens, so an opaque
1442
+ * access token handed back by a rotation cannot make an account look like a
1443
+ * different one.
1444
+ */
1445
+ function codexUserId(credentials) {
1446
+ const stored = nonEmpty$3(credentials.userId);
1447
+ if (stored !== void 0) return stored;
1448
+ return extractIdentity(credentials).userId;
1449
+ }
1450
+ /**
1451
+ * One opaque key per local login: the pool's dedupe key, and the scope of every
1452
+ * per-account cache this line keeps.
1453
+ *
1454
+ * A digest of an EXPLICIT serialization rather than a readable join, for three
1455
+ * reasons that are really one: the string is a label, not data.
1456
+ *
1457
+ * - A separator can be forged. `workspace:user` joins two values that may
1458
+ * themselves contain the separator, so `a` + `b:c` and `a:b` + `c` would have to
1459
+ * be the same key. A JSON array has no such ambiguity: quotes and escapes are
1460
+ * the boundary, and they cannot be re-cut by a value.
1461
+ * - The key becomes a FILE NAME. `catalogSnapshotName` folds everything outside
1462
+ * `[a-z0-9]` to `_`, so a readable key would collide with its neighbours after
1463
+ * folding *and* leave an email address on disk. Hex survives the fold unchanged.
1464
+ * - It is compared for equality only, so nothing is lost by making it opaque.
1465
+ *
1466
+ * `v1` is part of the serialized tuple, not decoration: a future change to the
1467
+ * composition has to produce different keys, and a version tag is what lets that
1468
+ * change be told apart from a corrupted row instead of silently merging two
1469
+ * accounts that the new rule would separate.
1470
+ *
1471
+ * Returns `undefined` when the credential states nothing at all about who it is.
1472
+ * The pool then mints its own `acc_<random>` and ADDS the row, which is the honest
1473
+ * answer: a credential nothing identifies cannot be recognized again, and merging
1474
+ * it with an unrelated one would be worse than a duplicate.
1475
+ */
1476
+ function codexAccountIdentity(credentials) {
1477
+ const workspace = nonEmpty$3(credentials.accountId);
1478
+ const user = codexUserId(credentials) ?? nonEmpty$3(credentials.email);
1479
+ if (workspace === void 0 && user === void 0) return void 0;
1480
+ const serialized = JSON.stringify([
1481
+ "v1",
1482
+ workspace ?? "",
1483
+ user ?? ""
1484
+ ]);
1485
+ return "v1-" + createHash("sha256").update(serialized).digest("hex").slice(0, 32);
1486
+ }
1487
+ function stringClaim(value) {
1488
+ return typeof value === "string" && value !== "" ? value : void 0;
1489
+ }
1490
+ function nonEmpty$3(value) {
1491
+ const trimmed = value?.trim();
1492
+ return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
1493
+ }
1494
+ //#endregion
1348
1495
  //#region src/host/wire-auth.ts
1349
1496
  /**
1350
1497
  * Turn-state header the Codex backend hands back on a responses response.
@@ -1441,18 +1588,36 @@ const CATALOG_TTL_MS = 15 * 6e4;
1441
1588
  * scope a rehydrated listing from one account would answer another's picker.
1442
1589
  */
1443
1590
  const SNAPSHOT_SCOPE = "chatgpt-codex";
1444
- let catalogCache$2 = null;
1591
+ /**
1592
+ * How many accounts' listings are held at once.
1593
+ *
1594
+ * One slot was enough while a workspace id was mistaken for an account, because
1595
+ * every member of a workspace then shared one. It is not enough now: a pool
1596
+ * rotates between accounts, and a single slot would have each rotation evict the
1597
+ * other account's listing and re-fetch it, which turns the 15-minute TTL into
1598
+ * "no cache at all" for exactly the multi-account setup this line supports. The
1599
+ * bound is the pool's own order of magnitude, so a real composition always fits
1600
+ * and a pathological one only loses the coldest entry.
1601
+ */
1602
+ const CATALOG_CACHE_ACCOUNTS = 8;
1603
+ /** Listings held per login identity; see {@link CATALOG_CACHE_ACCOUNTS}. */
1604
+ const catalogCache$2 = /* @__PURE__ */ new Map();
1605
+ /** The entry {@link getCachedCatalog} answers with: the one a load last wrote. */
1606
+ let latestKey = null;
1445
1607
  const catalogInFlight$6 = /* @__PURE__ */ new Map();
1446
- let snapshotRehydrated = null;
1608
+ /** Snapshots already read back from disk, per identity. */
1609
+ const snapshotRehydrated = /* @__PURE__ */ new Map();
1447
1610
  /**
1448
1611
  * The listing currently held, or undefined before the first load.
1449
1612
  *
1450
- * Callers that need a catalog with no credential read the shipped table
1451
- * directly; returning it here under a different entry type would force every
1452
- * one of them to convert it.
1613
+ * "Currently held" is the entry a load wrote last: a picker asks what the
1614
+ * account serving it can call, and that is the account whose listing was fetched
1615
+ * most recently. Callers that need a catalog with no credential read the shipped
1616
+ * table directly; returning it here under a different entry type would force
1617
+ * every one of them to convert it.
1453
1618
  */
1454
1619
  function getCachedCatalog$3() {
1455
- return catalogCache$2?.models;
1620
+ return latestKey === null ? void 0 : catalogCache$2.get(latestKey)?.models;
1456
1621
  }
1457
1622
  /**
1458
1623
  * Read the models this subscription can call.
@@ -1470,8 +1635,11 @@ function getCachedCatalog$3() {
1470
1635
  function loadCodexCatalog(credentials, options = {}) {
1471
1636
  const key = accountKeyFor$1(credentials);
1472
1637
  if (options.force !== true) {
1473
- const cached = catalogCache$2;
1474
- if (cached !== null && cached.key === key && Date.now() - cached.at < CATALOG_TTL_MS) return Promise.resolve(cached.models);
1638
+ const cached = catalogCache$2.get(key);
1639
+ if (cached !== void 0 && Date.now() - cached.at < CATALOG_TTL_MS) {
1640
+ latestKey = key;
1641
+ return Promise.resolve(cached.models);
1642
+ }
1475
1643
  }
1476
1644
  const pending = catalogInFlight$6.get(key);
1477
1645
  if (pending !== void 0) return pending;
@@ -1488,10 +1656,17 @@ function loadCodexCatalog(credentials, options = {}) {
1488
1656
  async function performCatalogLoad$1(credentials, options) {
1489
1657
  const key = accountKeyFor$1(credentials);
1490
1658
  const fetchFn = options.fetchFn ?? fetch;
1491
- snapshotRehydrated ??= rehydrateSnapshot(key);
1492
- await snapshotRehydrated;
1493
- const cached = catalogCache$2;
1494
- if (cached !== null && cached.key === key && Date.now() - cached.at < CATALOG_TTL_MS) return cached.models;
1659
+ let rehydrated = snapshotRehydrated.get(key);
1660
+ if (rehydrated === void 0) {
1661
+ rehydrated = rehydrateSnapshot(key);
1662
+ snapshotRehydrated.set(key, rehydrated);
1663
+ }
1664
+ await rehydrated;
1665
+ const cached = catalogCache$2.get(key);
1666
+ if (cached !== void 0 && Date.now() - cached.at < CATALOG_TTL_MS) {
1667
+ latestKey = key;
1668
+ return cached.models;
1669
+ }
1495
1670
  let models;
1496
1671
  let live = false;
1497
1672
  try {
@@ -1510,18 +1685,34 @@ async function performCatalogLoad$1(credentials, options) {
1510
1685
  live = true;
1511
1686
  writeCatalogSnapshot(catalogSnapshotName(SNAPSHOT_SCOPE, key), [...models], Date.now());
1512
1687
  } catch {
1513
- models = getCachedCatalog$3() ?? [];
1688
+ models = catalogCache$2.get(key)?.models ?? [];
1514
1689
  live = false;
1515
1690
  }
1516
- catalogCache$2 = {
1691
+ remember(key, {
1517
1692
  at: Date.now(),
1518
1693
  models,
1519
1694
  key,
1520
1695
  live
1521
- };
1696
+ });
1522
1697
  return models;
1523
1698
  }
1524
1699
  /**
1700
+ * File one listing under its identity, keeping the map bounded.
1701
+ *
1702
+ * Eviction is oldest-fetch-first, which is the entry a rotation is least likely to
1703
+ * ask for next; the bound exists so a composition that signs in far more accounts
1704
+ * than the pool holds cannot grow this map without end.
1705
+ */
1706
+ function remember(key, entry) {
1707
+ catalogCache$2.set(key, entry);
1708
+ latestKey = key;
1709
+ while (catalogCache$2.size > CATALOG_CACHE_ACCOUNTS) {
1710
+ const oldest = [...catalogCache$2.entries()].filter(([candidate]) => candidate !== key).sort((left, right) => left[1].at - right[1].at)[0];
1711
+ if (oldest === void 0) break;
1712
+ catalogCache$2.delete(oldest[0]);
1713
+ }
1714
+ }
1715
+ /**
1525
1716
  * Seed the in-memory cache from the last persisted listing.
1526
1717
  *
1527
1718
  * Best effort by construction: a missing or malformed snapshot leaves the cache
@@ -1530,13 +1721,13 @@ async function performCatalogLoad$1(credentials, options) {
1530
1721
  async function rehydrateSnapshot(key) {
1531
1722
  const snapshot = await readCatalogSnapshot(catalogSnapshotName(SNAPSHOT_SCOPE, key), (value) => Array.isArray(value) ? value.flatMap((item) => parseListingEntry$1(item)) : void 0).catch(() => void 0);
1532
1723
  if (snapshot === void 0 || snapshot.models.length === 0) return;
1533
- if (catalogCache$2 !== null) return;
1534
- catalogCache$2 = {
1724
+ if (catalogCache$2.has(key)) return;
1725
+ remember(key, {
1535
1726
  at: snapshot.fetchedAt,
1536
1727
  models: snapshot.models,
1537
1728
  key,
1538
1729
  live: true
1539
- };
1730
+ });
1540
1731
  }
1541
1732
  /**
1542
1733
  * Read the listing payload.
@@ -1605,12 +1796,19 @@ function parseEfforts(value) {
1605
1796
  /**
1606
1797
  * Cache identity for one credential.
1607
1798
  *
1608
- * The account id is the entitlement boundary: two accounts of the same user can
1609
- * see different listings, and a pool rotates between them, so the key has to
1610
- * follow the account rather than the person.
1799
+ * The listing is served per PLAN, and a plan belongs to the local login — the
1800
+ * workspace id alone is not an identity here either: two members of one workspace
1801
+ * can hold different plans, so filing both listings under the workspace would let
1802
+ * one member's picker answer with models the other member's plan does not serve.
1803
+ * The key is therefore the same composite identity the pool dedupes on, and its
1804
+ * digest form is what keeps the persisted snapshot's file name both distinct after
1805
+ * `catalogSnapshotName` folds it and free of an email address.
1806
+ *
1807
+ * `'default'` answers a credential that states nothing about itself, which is the
1808
+ * single pre-sign-in case this loader ever sees.
1611
1809
  */
1612
1810
  function accountKeyFor$1(credentials) {
1613
- return credentials.accountId || "default";
1811
+ return codexAccountIdentity(credentials) ?? "default";
1614
1812
  }
1615
1813
  function asRecord$20(value) {
1616
1814
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
@@ -1729,6 +1927,7 @@ function parseStoredCredentials(value) {
1729
1927
  expiresAt: record.expiresAt,
1730
1928
  idToken: optional("idToken"),
1731
1929
  accountId: optional("accountId"),
1930
+ userId: optional("userId"),
1732
1931
  email: optional("email"),
1733
1932
  planType: optional("planType")
1734
1933
  };
@@ -4498,17 +4697,6 @@ function buildAuthorizationUrl(verifier, state) {
4498
4697
  originator: OAUTH_ORIGINATOR
4499
4698
  }).toString()}`;
4500
4699
  }
4501
- function parseJwtClaims(token) {
4502
- if (token === void 0) return void 0;
4503
- const parts = token.split(".");
4504
- if (parts.length !== 3) return void 0;
4505
- try {
4506
- const value = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
4507
- return typeof value === "object" && value !== null ? value : void 0;
4508
- } catch {
4509
- return;
4510
- }
4511
- }
4512
4700
  function publicError(error, fallback = "internal") {
4513
4701
  if (error instanceof OAuthServiceError) return {
4514
4702
  code: error.code,
@@ -4535,28 +4723,25 @@ function credentialsFromTokenResponse(response, now, previous) {
4535
4723
  return {
4536
4724
  ...base,
4537
4725
  accountId: identity.accountId ?? previous?.accountId,
4726
+ userId: identity.userId ?? previous?.userId,
4538
4727
  email: identity.email ?? previous?.email,
4539
4728
  planType: identity.planType ?? previous?.planType
4540
4729
  };
4541
4730
  }
4542
- function extractIdentity(credentials) {
4543
- const result = {};
4544
- for (const token of [credentials.idToken, credentials.accessToken]) {
4545
- const claims = parseJwtClaims(token);
4546
- if (claims === void 0) continue;
4547
- const nested = claims["https://api.openai.com/auth"];
4548
- result.email ??= stringClaim(claims.email);
4549
- result.planType ??= stringClaim(claims.chatgpt_plan_type) ?? stringClaim(nested?.chatgpt_plan_type);
4550
- result.accountId ??= stringClaim(claims.chatgpt_account_id) ?? stringClaim(nested?.chatgpt_account_id) ?? stringClaim(claims.organizations?.[0]?.id) ?? stringClaim(nested?.organizations?.[0]?.id);
4551
- }
4552
- return result;
4553
- }
4554
- /** Identity one refresh is keyed by, so a rotation is never redeemed twice. */
4731
+ /**
4732
+ * Identity one refresh is keyed by, so a rotation is never redeemed twice.
4733
+ *
4734
+ * The SAME key the pool dedupes on, and deliberately so: it has to separate two
4735
+ * members of one workspace, because their grants are unrelated and sharing one
4736
+ * in-flight refresh between them would hand one account's rotated token to the
4737
+ * other. A workspace id alone cannot do that — it is what both members have in
4738
+ * common — so this is the composite local login identity, with the rotating
4739
+ * refresh token as the fallback for a credential that states nothing else. That
4740
+ * fallback is the conservative direction: a token that differs always gets its
4741
+ * own flight, and a flight nobody joined simply runs by itself.
4742
+ */
4555
4743
  function refreshKey(credentials) {
4556
- return credentials.accountId ?? credentials.email ?? credentials.refreshToken;
4557
- }
4558
- function stringClaim(value) {
4559
- return typeof value === "string" && value !== "" ? value : void 0;
4744
+ return codexAccountIdentity(credentials) ?? credentials.refreshToken;
4560
4745
  }
4561
4746
  function maskEmail(email) {
4562
4747
  if (email === void 0) return null;
@@ -4851,8 +5036,6 @@ const CODEX_CLI_CREDENTIAL_FILE = "auth.json";
4851
5036
  const CODEX_CLI_TOKENS_KEY = "tokens";
4852
5037
  /** Where an adopted snapshot came from, for the card and for the pool record. */
4853
5038
  const CODEX_CLI_CREDENTIAL_SOURCE = "codex";
4854
- /** Namespace the id token carries the ChatGPT account claims under. */
4855
- const CODEX_AUTH_CLAIMS_KEY = "https://api.openai.com/auth";
4856
5039
  /**
4857
5040
  * What the user is told when an adopted credential has aged out.
4858
5041
  *
@@ -5256,7 +5439,7 @@ var CodexAccountPool = class extends AccountPoolCore {
5256
5439
  poolFile: codexPoolPath(),
5257
5440
  keychainService: "dsh-chatgpt-subscription-pool",
5258
5441
  parsePoolData: parseCodexPoolData,
5259
- dedupeKey: (credentials) => credentials.accountId ?? credentials.email,
5442
+ dedupeKey: (credentials) => codexAccountIdentity(credentials),
5260
5443
  defaultAlias: (credentials, position) => credentials.email ?? `账号 ${position}`,
5261
5444
  createAccount: ({ id, alias, credentials, addedAt, isPrimary }) => {
5262
5445
  const adopted = isAdoptedCodexPoolCredential(credentials);
@@ -5925,6 +6108,33 @@ function normalizeGenerateOptions(options) {
5925
6108
  }
5926
6109
  //#endregion
5927
6110
  //#region src/host/codex-images.ts
6111
+ /**
6112
+ * Output canvases the Codex image endpoint accepts in `size`.
6113
+ *
6114
+ * These are the official client's own three canvases plus the default `auto`,
6115
+ * which lets the service pick. A size outside this set is not negotiated: the
6116
+ * backend answers it with a 400, so the tool refuses it before the request.
6117
+ */
6118
+ const CODEX_IMAGE_SIZES = [
6119
+ "auto",
6120
+ "1024x1024",
6121
+ "1536x1024",
6122
+ "1024x1536"
6123
+ ];
6124
+ /** Render effort the endpoint accepts in `quality`; `auto` is the service's own default. */
6125
+ const CODEX_IMAGE_QUALITIES = [
6126
+ "auto",
6127
+ "low",
6128
+ "medium",
6129
+ "high"
6130
+ ];
6131
+ /** Frame budget of an animated GIF the endpoint accepts. */
6132
+ const GIF_MIN_FRAMES = 2;
6133
+ const GIF_MAX_FRAMES = 16;
6134
+ const GIF_MIN_FRAME_MS = 20;
6135
+ const GIF_MAX_FRAME_MS = 2e3;
6136
+ /** How much of an upstream error body is quoted back to the caller. */
6137
+ const MAX_UPSTREAM_EXCERPT_CHARS = 300;
5928
6138
  const PNG_SIGNATURE = [
5929
6139
  137,
5930
6140
  80,
@@ -5935,16 +6145,78 @@ const PNG_SIGNATURE = [
5935
6145
  26,
5936
6146
  10
5937
6147
  ];
6148
+ /**
6149
+ * Magic-byte checks per media type.
6150
+ *
6151
+ * An extension is a claim about a file's name; the bytes are the claim the file
6152
+ * itself makes. Both are checked, because sending a file whose declared type the
6153
+ * service cannot decode returns a plain upstream error rather than an image.
6154
+ */
6155
+ const SIGNATURES$1 = {
6156
+ "image/png": (bytes) => bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71,
6157
+ "image/jpeg": (bytes) => bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255,
6158
+ "image/webp": (bytes) => bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80,
6159
+ "image/gif": (bytes) => bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70
6160
+ };
6161
+ /**
6162
+ * Extensions a reference image may carry.
6163
+ *
6164
+ * Deliberately narrower than {@link SIGNATURES}: an edit accepts stills only.
6165
+ * Animated GIF input is not part of the edit wire contract, so it is refused
6166
+ * here instead of being sent to an endpoint that does not take it.
6167
+ */
6168
+ const REFERENCE_EXTENSIONS = {
6169
+ ".png": "image/png",
6170
+ ".jpg": "image/jpeg",
6171
+ ".jpeg": "image/jpeg",
6172
+ ".webp": "image/webp"
6173
+ };
5938
6174
  function createCodexImageTool(oauth, attachments, options = {}) {
5939
6175
  const fetchFn = options.fetchFn ?? fetch;
5940
6176
  return defineTool({
5941
6177
  name: CODEX_IMAGE_TOOL_NAME,
5942
- description: "Generate a PNG image using the signed-in ChatGPT subscription-backed Codex image endpoint.",
5943
- parameters: { prompt: {
5944
- type: "string",
5945
- required: true,
5946
- description: "A detailed image generation prompt."
5947
- } },
6178
+ description: "Generate or edit a PNG or GIF image with the signed-in ChatGPT subscription-backed Codex image endpoint. Describe the subject, style, composition and any text to render in the prompt; pass referenceImagePaths to edit local PNG/JPEG/WebP images, and gif to animate the result. Scheduling: call this tool yourself for a single generation, an edit, or a retry - never create a subagent just to generate one image. Delegate only when the creative task needs several candidate images or is large enough to be a multi-step task of its own.",
6179
+ parameters: {
6180
+ prompt: {
6181
+ type: "string",
6182
+ required: true,
6183
+ description: "A detailed image generation prompt."
6184
+ },
6185
+ size: {
6186
+ type: "string",
6187
+ enum: CODEX_IMAGE_SIZES,
6188
+ description: "Output canvas. Omit or pass auto to let the service choose. 1024x1024 is square, 1536x1024 landscape and 1024x1536 portrait."
6189
+ },
6190
+ quality: {
6191
+ type: "string",
6192
+ enum: CODEX_IMAGE_QUALITIES,
6193
+ description: "Render effort. Omit or pass auto for the service default; low is fastest and high is most detailed."
6194
+ },
6195
+ transparentBackground: {
6196
+ type: "boolean",
6197
+ description: "true for a transparent background (PNG or GIF alpha), false for an explicitly opaque one. Omit it to leave the background decision to the service."
6198
+ },
6199
+ referenceImagePaths: {
6200
+ type: "array",
6201
+ items: { type: "string" },
6202
+ description: "Up to 5 local PNG, JPEG or WebP files to edit. Absolute paths recommended. The request becomes an edit against those images; nothing else on disk is read."
6203
+ },
6204
+ gif: {
6205
+ type: "object",
6206
+ additionalProperties: false,
6207
+ description: "Ask for an animated GIF. An empty object is allowed and lets the service choose the frames.",
6208
+ properties: {
6209
+ frameCount: {
6210
+ type: "integer",
6211
+ description: "Number of frames, 2 to 16."
6212
+ },
6213
+ frameDurationMs: {
6214
+ type: "integer",
6215
+ description: "Milliseconds each frame is shown, 20 to 2000."
6216
+ }
6217
+ }
6218
+ }
6219
+ },
5948
6220
  output: {
5949
6221
  schema: {
5950
6222
  type: "object",
@@ -5969,7 +6241,7 @@ function createCodexImageTool(oauth, attachments, options = {}) {
5969
6241
  },
5970
6242
  mediaType: {
5971
6243
  type: "string",
5972
- enum: ["image/png"],
6244
+ enum: ["image/png", "image/gif"],
5973
6245
  required: true
5974
6246
  },
5975
6247
  bytes: {
@@ -6016,34 +6288,52 @@ function createCodexImageTool(oauth, attachments, options = {}) {
6016
6288
  async execute(args, exec) {
6017
6289
  const prompt = args.prompt.trim();
6018
6290
  if (prompt === "") throw new HarnessError("Image prompt cannot be empty.", "CODEX_IMAGE_INVALID_PROMPT");
6019
- if (!attachments.imageLimits.mediaTypes.includes("image/png")) throw new HarnessError("PNG image attachments are not enabled in this DSH environment.", "CODEX_IMAGE_ATTACHMENT_UNSUPPORTED");
6291
+ const gif = readGifOptions(args.gif);
6292
+ const references = await readReferenceImages(args.referenceImagePaths ?? [], attachments);
6293
+ assertOutputTypeSupported(attachments, gif === void 0 ? "image/png" : "image/gif");
6294
+ const body = buildImageBody({
6295
+ prompt,
6296
+ size: args.size ?? "auto",
6297
+ quality: args.quality ?? "auto",
6298
+ ...args.transparentBackground !== void 0 ? { transparentBackground: args.transparentBackground } : {},
6299
+ ...gif !== void 0 ? { gif } : {},
6300
+ references
6301
+ });
6302
+ const url = references.length > 0 ? CODEX_IMAGE_EDIT_URL : CODEX_IMAGE_GENERATION_URL;
6303
+ const turnId = String(exec.callId);
6020
6304
  let credentials = await imageCredentials(oauth);
6021
- let response = await requestImage$1(fetchFn, credentials, prompt, String(exec.callId), exec.signal);
6305
+ let response = await requestImage$1(fetchFn, credentials, url, body, turnId, exec.signal);
6022
6306
  if (response.status === 401) {
6023
6307
  await response.body?.cancel().catch(() => void 0);
6024
6308
  credentials = await imageCredentials(oauth, true);
6025
- response = await requestImage$1(fetchFn, credentials, prompt, String(exec.callId), exec.signal);
6309
+ response = await requestImage$1(fetchFn, credentials, url, body, turnId, exec.signal);
6026
6310
  }
6027
6311
  if (response.status === 429) {
6028
- await response.body?.cancel().catch(() => void 0);
6029
- throw new HarnessError("Codex image generation was rate limited.", "CODEX_IMAGE_RATE_LIMITED");
6312
+ const detail = upstreamExcerpt(await response.text().catch(() => ""));
6313
+ throw new HarnessError(`Codex image generation was rate limited (429).${detail === "" ? "" : ` ${detail}`}`, "CODEX_IMAGE_RATE_LIMITED");
6314
+ }
6315
+ if (response.status === 400) {
6316
+ const detail = upstreamExcerpt(await response.text().catch(() => ""));
6317
+ throw new HarnessError(`Codex rejected the image request (400). The parameters this call stated were: ${statedParameterSummary(args, gif, references.length)}. One of them is not accepted as given, and no field was dropped to retry.` + (detail === "" ? "" : ` Upstream said: ${detail}`), "CODEX_IMAGE_REQUEST_REJECTED");
6030
6318
  }
6031
6319
  if (!response.ok) {
6032
- await response.body?.cancel().catch(() => void 0);
6033
- throw new HarnessError(`Codex image generation failed (${response.status}).`, "CODEX_IMAGE_FAILED");
6320
+ const detail = upstreamExcerpt(await response.text().catch(() => ""));
6321
+ throw new HarnessError(`Codex image generation failed (${response.status}).${detail === "" ? "" : ` ${detail}`}`, "CODEX_IMAGE_FAILED");
6034
6322
  }
6035
- const bytes = decodeImageBytes(readBase64Image(await response.json()), maxGeneratedImageBytes(attachments));
6323
+ const decoded = decodeImageBytes(readBase64Image(await response.json()), maxGeneratedImageBytes(attachments));
6324
+ if (gif !== void 0 && decoded.mediaType !== "image/gif") throw new HarnessError(`Codex was asked for an animated GIF but returned ${decoded.mediaType}. Nothing was saved: the answer is not the animation that was requested. Omit the gif parameter to accept a still image.`, "CODEX_IMAGE_RESPONSE_INVALID");
6325
+ if (!attachments.imageLimits.mediaTypes.includes(decoded.mediaType)) throw new HarnessError(`Codex returned ${decoded.mediaType} but this DSH environment does not accept that attachment type.`, "CODEX_IMAGE_ATTACHMENT_UNSUPPORTED");
6036
6326
  const image = await attachments.saveImage({
6037
- data: bytes,
6038
- mediaType: "image/png",
6039
- name: "codex-generated-image.png"
6327
+ data: decoded.data,
6328
+ mediaType: decoded.mediaType,
6329
+ name: `codex-generated-image.${decoded.mediaType === "image/gif" ? "gif" : "png"}`
6040
6330
  });
6041
6331
  const output = {
6042
6332
  prompt,
6043
6333
  model: CODEX_IMAGE_MODEL,
6044
6334
  image: {
6045
6335
  attachmentId: image.attachmentId,
6046
- mediaType: "image/png",
6336
+ mediaType: decoded.mediaType,
6047
6337
  bytes: image.bytes,
6048
6338
  width: image.width,
6049
6339
  height: image.height,
@@ -6065,6 +6355,124 @@ function createCodexImageTool(oauth, attachments, options = {}) {
6065
6355
  }
6066
6356
  });
6067
6357
  }
6358
+ /**
6359
+ * Validate the frame options of a GIF request.
6360
+ *
6361
+ * An empty object is legal: it asks for an animation and leaves both numbers to
6362
+ * the service. A stated number is checked here rather than by the parameter
6363
+ * schema because the schema DSL carries no numeric bounds, and because
6364
+ * `execute` is reachable directly.
6365
+ */
6366
+ function readGifOptions(gif) {
6367
+ if (gif === void 0) return void 0;
6368
+ const result = {};
6369
+ const frameCount = gif.frameCount;
6370
+ if (frameCount !== void 0) {
6371
+ if (!Number.isInteger(frameCount) || frameCount < GIF_MIN_FRAMES || frameCount > GIF_MAX_FRAMES) throw new HarnessError(`gif.frameCount must be a whole number from ${GIF_MIN_FRAMES} to ${GIF_MAX_FRAMES} (got ${String(frameCount)}).`, "CODEX_IMAGE_GIF_INVALID");
6372
+ result.frameCount = frameCount;
6373
+ }
6374
+ const frameDurationMs = gif.frameDurationMs;
6375
+ if (frameDurationMs !== void 0) {
6376
+ if (!Number.isInteger(frameDurationMs) || frameDurationMs < GIF_MIN_FRAME_MS || frameDurationMs > GIF_MAX_FRAME_MS) throw new HarnessError(`gif.frameDurationMs must be a whole number of milliseconds from ${GIF_MIN_FRAME_MS} to ${GIF_MAX_FRAME_MS} (got ${String(frameDurationMs)}).`, "CODEX_IMAGE_GIF_INVALID");
6377
+ result.frameDurationMs = frameDurationMs;
6378
+ }
6379
+ return result;
6380
+ }
6381
+ /** Refuse an output type this deployment cannot store, before anything is requested. */
6382
+ function assertOutputTypeSupported(attachments, outputType) {
6383
+ if (attachments.imageLimits.mediaTypes.includes(outputType)) return;
6384
+ throw new HarnessError(outputType === "image/gif" ? "GIF output was requested, but this DSH environment does not accept GIF attachments, so nothing was requested. Omit the gif parameter to generate a PNG instead." : "PNG image attachments are not enabled in this DSH environment.", "CODEX_IMAGE_ATTACHMENT_UNSUPPORTED");
6385
+ }
6386
+ /**
6387
+ * Build the wire body of one request.
6388
+ *
6389
+ * Field order and the defaults are deliberate: a call with no new options
6390
+ * produces exactly `prompt/background auto/model/quality auto/size auto`, which
6391
+ * is the body this tool has always sent. Every new field appears only when the
6392
+ * caller stated it, so an old call site keeps its old bytes on the wire.
6393
+ */
6394
+ function buildImageBody(request) {
6395
+ const body = {
6396
+ prompt: request.prompt,
6397
+ background: request.transparentBackground === true ? "transparent" : request.transparentBackground === false ? "opaque" : "auto",
6398
+ model: CODEX_IMAGE_MODEL,
6399
+ quality: request.quality,
6400
+ size: request.size
6401
+ };
6402
+ if (request.gif !== void 0) {
6403
+ body.output_format = "gif";
6404
+ if (request.gif.frameCount !== void 0) body.frame_count = request.gif.frameCount;
6405
+ if (request.gif.frameDurationMs !== void 0) body.frame_duration_ms = request.gif.frameDurationMs;
6406
+ }
6407
+ if (request.references.length > 0) body.images = request.references.map((image) => ({ image_url: `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}` }));
6408
+ return body;
6409
+ }
6410
+ /**
6411
+ * The optional parameters one request actually stated, in words.
6412
+ *
6413
+ * A 400 says only that the request was invalid, which is useless on a tool with
6414
+ * seven parameters: the caller has to be told WHICH ones it sent, or it has no
6415
+ * way to know what to change. Only stated options are named - a default that the
6416
+ * tool has always sent cannot be the field to fix - and each GIF sub-field is
6417
+ * named separately, because `gif: {}` is a legal call that stated neither number.
6418
+ */
6419
+ function statedParameterSummary(args, gif, referenceCount) {
6420
+ const stated = [];
6421
+ if (args.size !== void 0) stated.push(`size=${args.size}`);
6422
+ if (args.quality !== void 0) stated.push(`quality=${args.quality}`);
6423
+ if (args.transparentBackground !== void 0) stated.push(`transparentBackground=${String(args.transparentBackground)}`);
6424
+ if (referenceCount > 0) stated.push(`referenceImagePaths (${referenceCount} image${referenceCount === 1 ? "" : "s"})`);
6425
+ if (gif !== void 0) {
6426
+ const frames = [];
6427
+ if (gif.frameCount !== void 0) frames.push(`frameCount=${String(gif.frameCount)}`);
6428
+ if (gif.frameDurationMs !== void 0) frames.push(`frameDurationMs=${String(gif.frameDurationMs)}`);
6429
+ stated.push(frames.length === 0 ? "gif (frame numbers left to the service)" : `gif (${frames.join(", ")})`);
6430
+ }
6431
+ return stated.length === 0 ? "no optional parameter was stated" : stated.join(", ");
6432
+ }
6433
+ /**
6434
+ * Read and validate the reference images one edit request names.
6435
+ *
6436
+ * Every named file is checked as a local still image: the path must not be a
6437
+ * URL, the extension must be PNG/JPEG/WebP, the file must be a regular non-empty
6438
+ * file inside the attachment byte limits, and its leading bytes must match the
6439
+ * declared type. Nothing else on disk is read, and the returned bytes are the
6440
+ * ONLY image data that reaches the request.
6441
+ */
6442
+ async function readReferenceImages(files, attachments) {
6443
+ if (files.length === 0) return [];
6444
+ const limit = Math.min(5, attachments.imageLimits.maxImagesPerMessage);
6445
+ if (files.length > limit) throw new HarnessError(`At most ${limit} reference images are accepted; ${files.length} were given.`, "CODEX_IMAGE_REFERENCE_LIMIT");
6446
+ const perImageBytes = attachments.imageLimits.maxImageBytes;
6447
+ const totalBytes = attachments.imageLimits.maxMessageImageBytes;
6448
+ const images = [];
6449
+ let sentBytes = 0;
6450
+ for (const [index, raw] of files.entries()) {
6451
+ const label = `referenceImagePaths[${index}]`;
6452
+ const value = raw.trim();
6453
+ if (value === "") throw new HarnessError(`${label} is empty; every reference image must name a local file.`, "CODEX_IMAGE_REFERENCE_INVALID_PATH");
6454
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value) || /^data:/i.test(value)) throw new HarnessError(`${label} is a URL (${value}). Reference images must be local PNG, JPEG or WebP files; nothing is fetched.`, "CODEX_IMAGE_REFERENCE_INVALID_PATH");
6455
+ const extension = extname(value).toLowerCase();
6456
+ const mediaType = REFERENCE_EXTENSIONS[extension];
6457
+ if (mediaType === void 0) throw new HarnessError(extension === ".gif" ? `${label} (${value}) is a GIF. Reference images must be still PNG, JPEG or WebP files; animated input is not part of the edit request.` : `${label} (${value}) is not a PNG, JPEG or WebP file (extension ${extension === "" ? "(none)" : `"${extension}"`}).`, "CODEX_IMAGE_REFERENCE_UNSUPPORTED");
6458
+ if (!attachments.imageLimits.mediaTypes.includes(mediaType)) throw new HarnessError(`${label} is ${mediaType}, which this DSH environment does not accept as an attachment.`, "CODEX_IMAGE_ATTACHMENT_UNSUPPORTED");
6459
+ const absolute = resolve(value);
6460
+ const stats = await stat(absolute).catch(() => void 0);
6461
+ if (stats === void 0) throw new HarnessError(`${label} (${value}) does not exist or cannot be read.`, "CODEX_IMAGE_REFERENCE_NOT_FOUND");
6462
+ if (!stats.isFile()) throw new HarnessError(`${label} (${value}) is not a regular file; pass a path to one PNG, JPEG or WebP file.`, "CODEX_IMAGE_REFERENCE_NOT_A_FILE");
6463
+ if (stats.size === 0) throw new HarnessError(`${label} (${value}) is empty.`, "CODEX_IMAGE_REFERENCE_EMPTY");
6464
+ if (stats.size > perImageBytes) throw new HarnessError(`${label} (${value}) is ${stats.size} bytes, above the ${perImageBytes} byte limit for one attachment.`, "CODEX_IMAGE_REFERENCE_TOO_LARGE");
6465
+ const data = new Uint8Array(await readFile(absolute));
6466
+ if (!SIGNATURES$1[mediaType](data)) throw new HarnessError(`${label} (${value}) does not start like a ${mediaType} file, so its bytes contradict its extension.`, "CODEX_IMAGE_REFERENCE_INVALID_CONTENT");
6467
+ sentBytes += data.byteLength;
6468
+ if (sentBytes > totalBytes) throw new HarnessError(`The reference images total ${sentBytes} bytes, above the ${totalBytes} byte limit for one message.`, "CODEX_IMAGE_REFERENCE_TOO_LARGE");
6469
+ images.push({
6470
+ mediaType,
6471
+ data
6472
+ });
6473
+ }
6474
+ return images;
6475
+ }
6068
6476
  async function imageCredentials(oauth, force = false) {
6069
6477
  try {
6070
6478
  return await oauth.credentials(force, { purpose: "tool" });
@@ -6072,8 +6480,8 @@ async function imageCredentials(oauth, force = false) {
6072
6480
  throw new HarnessError(`ChatGPT subscription credentials are required for Codex image generation.${error instanceof Error && error.message !== "" ? ` (${error.message})` : ""}`, "CODEX_IMAGE_CREDENTIAL_MISSING", { cause: error });
6073
6481
  }
6074
6482
  }
6075
- function requestImage$1(fetchFn, credentials, prompt, turnId, signal) {
6076
- return fetchFn(CODEX_IMAGE_GENERATION_URL, {
6483
+ function requestImage$1(fetchFn, credentials, url, body, turnId, signal) {
6484
+ return fetchFn(url, {
6077
6485
  method: "POST",
6078
6486
  headers: {
6079
6487
  ...codexHeaders(credentials),
@@ -6081,16 +6489,19 @@ function requestImage$1(fetchFn, credentials, prompt, turnId, signal) {
6081
6489
  "content-type": "application/json",
6082
6490
  "x-codex-image-turn-id": turnId
6083
6491
  },
6084
- body: JSON.stringify({
6085
- prompt,
6086
- background: "auto",
6087
- model: CODEX_IMAGE_MODEL,
6088
- quality: "auto",
6089
- size: "auto"
6090
- }),
6492
+ body: JSON.stringify(body),
6091
6493
  signal
6092
6494
  });
6093
6495
  }
6496
+ /**
6497
+ * The upstream error body, short enough to read in a message and stripped of
6498
+ * anything that must never be echoed: bearer credentials, base64 image payloads
6499
+ * and any long base64-shaped run.
6500
+ */
6501
+ function upstreamExcerpt(text) {
6502
+ const collapsed = text.replace(/data:[a-z0-9.+-]+\/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]*/gi, "data:<redacted>").replace(/Bearer\s+[A-Za-z0-9._~+/=-]{6,}/gi, "Bearer <redacted>").replace(/[A-Za-z0-9+/]{200,}={0,2}/g, "<redacted>").replace(/\s+/g, " ").trim();
6503
+ return collapsed.length > MAX_UPSTREAM_EXCERPT_CHARS ? `${collapsed.slice(0, MAX_UPSTREAM_EXCERPT_CHARS)}…` : collapsed;
6504
+ }
6094
6505
  function readBase64Image(value) {
6095
6506
  const root = record$7(value);
6096
6507
  const data = Array.isArray(root?.data) ? root.data[0] : null;
@@ -6098,12 +6509,27 @@ function readBase64Image(value) {
6098
6509
  if (image === null) throw new HarnessError("Codex image response did not include image data.", "CODEX_IMAGE_RESPONSE_INVALID");
6099
6510
  return image;
6100
6511
  }
6512
+ /**
6513
+ * Decode one returned image and identify it by its own magic bytes.
6514
+ *
6515
+ * The endpoint answers a requested GIF with GIF bytes in the same `b64_json`
6516
+ * field it uses for PNG, and states nothing about which one it produced, so the
6517
+ * bytes are the only reliable source for the media type – and storing a GIF
6518
+ * under `image/png` would fail at save time or, worse, mislabel the file.
6519
+ */
6101
6520
  function decodeImageBytes(base64, maxBytes) {
6102
6521
  if (!/^[A-Za-z0-9+/]+={0,2}$/.test(base64) || base64.length % 4 !== 0) throw new HarnessError("Codex image response was not valid base64.", "CODEX_IMAGE_RESPONSE_INVALID");
6103
6522
  const buffer = Buffer.from(base64, "base64");
6104
6523
  if (buffer.length > maxBytes) throw new HarnessError("Generated image exceeds the configured attachment size limit.", "CODEX_IMAGE_TOO_LARGE");
6105
- for (const [index, byte] of PNG_SIGNATURE.entries()) if (buffer[index] !== byte) throw new HarnessError("Codex image response was not a PNG image.", "CODEX_IMAGE_RESPONSE_INVALID");
6106
- return buffer;
6524
+ if (SIGNATURES$1["image/gif"](buffer)) return {
6525
+ mediaType: "image/gif",
6526
+ data: buffer
6527
+ };
6528
+ for (const [index, byte] of PNG_SIGNATURE.entries()) if (buffer[index] !== byte) throw new HarnessError("Codex image response was neither PNG nor GIF image data.", "CODEX_IMAGE_RESPONSE_INVALID");
6529
+ return {
6530
+ mediaType: "image/png",
6531
+ data: buffer
6532
+ };
6107
6533
  }
6108
6534
  function maxGeneratedImageBytes(attachments) {
6109
6535
  return Math.min(attachments.imageLimits.maxImageBytes, attachments.imageLimits.maxMessageImageBytes);
@@ -10277,6 +10703,9 @@ var AntigravityAdapter = class extends LlmAdapter {
10277
10703
  /**
10278
10704
  * Aspect ratios Gemini image models accept in `imageConfig.aspectRatio`.
10279
10705
  * Omitted, the model picks one itself, which is what the request then does.
10706
+ *
10707
+ * The last four are the extreme strips: a wider or taller frame than the square
10708
+ * family covers, useful for banners, headers and phone wallpapers.
10280
10709
  */
10281
10710
  const ANTIGRAVITY_IMAGE_ASPECT_RATIOS = [
10282
10711
  "1:1",
@@ -10288,7 +10717,21 @@ const ANTIGRAVITY_IMAGE_ASPECT_RATIOS = [
10288
10717
  "5:4",
10289
10718
  "9:16",
10290
10719
  "16:9",
10291
- "21:9"
10720
+ "21:9",
10721
+ "1:4",
10722
+ "4:1",
10723
+ "1:8",
10724
+ "8:1"
10725
+ ];
10726
+ /**
10727
+ * Image sizes Gemini image models accept in `imageConfig.imageSize`.
10728
+ * Omitted, the model picks one itself, which is what the request then does.
10729
+ */
10730
+ const ANTIGRAVITY_IMAGE_SIZES = [
10731
+ "0.5K",
10732
+ "1K",
10733
+ "2K",
10734
+ "4K"
10292
10735
  ];
10293
10736
  /** Images one call keeps; the model normally returns one. */
10294
10737
  const MAX_IMAGES_PER_CALL = 4;
@@ -10302,7 +10745,11 @@ const MAX_IMAGES_PER_CALL = 4;
10302
10745
  * Antigravity clients send for image runtimes; it has not been captured from
10303
10746
  * the official client.
10304
10747
  */
10305
- function buildImageRequest(prompt, projectId, aspectRatio) {
10748
+ function buildImageRequest(prompt, projectId, aspectRatio, imageSize) {
10749
+ const imageConfig = {
10750
+ ...aspectRatio ? { aspectRatio } : {},
10751
+ ...imageSize ? { imageSize } : {}
10752
+ };
10306
10753
  return {
10307
10754
  project: projectId,
10308
10755
  model: ANTIGRAVITY_IMAGE_MODEL,
@@ -10313,7 +10760,7 @@ function buildImageRequest(prompt, projectId, aspectRatio) {
10313
10760
  }],
10314
10761
  generationConfig: {
10315
10762
  responseModalities: ["TEXT", "IMAGE"],
10316
- ...aspectRatio ? { imageConfig: { aspectRatio } } : {}
10763
+ ...Object.keys(imageConfig).length > 0 ? { imageConfig } : {}
10317
10764
  }
10318
10765
  },
10319
10766
  requestType: "image_gen",
@@ -10425,7 +10872,7 @@ function createAntigravityImageTool(accountPool, store, attachments, options = {
10425
10872
  const fetchFn = options.fetchFn ?? fetch;
10426
10873
  return defineTool({
10427
10874
  name: ANTIGRAVITY_IMAGE_TOOL_NAME,
10428
- description: "Generate an image with Gemini image generation through the signed-in Antigravity account. Describe the subject, style, composition and any text to render in the prompt.",
10875
+ description: "Generate an image with Gemini image generation through the signed-in Antigravity account. Describe the subject, style, composition and any text to render in the prompt. Scheduling: a single generation, a change to an image already generated, and a retry of a failed attempt are direct calls from the agent that needs the image — do not create a subagent just to generate one image. Delegate only a task that genuinely needs several alternative directions or complex creative planning, and pass the prompt that comes back to this tool.",
10429
10876
  parameters: {
10430
10877
  prompt: {
10431
10878
  type: "string",
@@ -10435,7 +10882,12 @@ function createAntigravityImageTool(accountPool, store, attachments, options = {
10435
10882
  aspectRatio: {
10436
10883
  type: "string",
10437
10884
  enum: ANTIGRAVITY_IMAGE_ASPECT_RATIOS,
10438
- description: "Optional aspect ratio. Omit it to let the model choose."
10885
+ description: "Optional aspect ratio, one of the listed values. Omit it to let the model choose. If the upstream rejects the value, re-issue the call with another value from the list rather than the same one."
10886
+ },
10887
+ imageSize: {
10888
+ type: "string",
10889
+ enum: ANTIGRAVITY_IMAGE_SIZES,
10890
+ description: "Optional output size, one of 0.5K, 1K, 2K or 4K. Omit it to let the model choose. It is sent only when given, so it never overrides an aspect ratio the caller did not set."
10439
10891
  }
10440
10892
  },
10441
10893
  output: {
@@ -10519,7 +10971,7 @@ function createAntigravityImageTool(accountPool, store, attachments, options = {
10519
10971
  const prompt = args.prompt.trim();
10520
10972
  if (prompt === "") throw new HarnessError("Image prompt cannot be empty.", "ANTIGRAVITY_IMAGE_INVALID_PROMPT");
10521
10973
  if (options.isEnabled?.() === false) throw new HarnessError("The Antigravity line is switched off; enable it in Settings → Antigravity to generate images.", "ANTIGRAVITY_IMAGE_DISABLED");
10522
- const response = await requestImage(accountPool, store, fetchFn, prompt, args.aspectRatio, exec.signal);
10974
+ const response = await requestImage(accountPool, store, fetchFn, prompt, args.aspectRatio, args.imageSize, exec.signal);
10523
10975
  if (response.images.length === 0) throw new HarnessError(noImageMessage(response), "ANTIGRAVITY_IMAGE_EMPTY");
10524
10976
  const keep = Math.min(MAX_IMAGES_PER_CALL, attachments.imageLimits.maxImagesPerMessage, response.images.length);
10525
10977
  const saved = [];
@@ -10578,12 +11030,37 @@ async function acquireAccount(accountPool, store, tried, fetchFn) {
10578
11030
  }
10579
11031
  }
10580
11032
  }
10581
- async function requestImage(accountPool, store, fetchFn, prompt, aspectRatio, signal) {
11033
+ /**
11034
+ * Whether a status means the upstream refused the SHAPE of the request rather
11035
+ * than the account, the allowance or the transient state of the service.
11036
+ */
11037
+ const PARAMETER_REJECTION_STATUSES = /* @__PURE__ */ new Set([400, 422]);
11038
+ /**
11039
+ * What a failed image request reports: the status, a bounded excerpt of what
11040
+ * the upstream said, and — when the refusal was about the request — which
11041
+ * optional fields were sent.
11042
+ *
11043
+ * A parameter rejection is never retried without the field it names. Dropping
11044
+ * `aspectRatio` or `imageSize` would answer with an image that silently has a
11045
+ * shape nobody asked for, so the caller is told which fields to change and
11046
+ * makes that call itself.
11047
+ */
11048
+ function imageFailureMessage(status, errorText, sent) {
11049
+ const parts = [`Antigravity image generation failed (${status ?? "no response"}).`];
11050
+ if (status !== void 0 && PARAMETER_REJECTION_STATUSES.has(status)) {
11051
+ const fields = [sent.aspectRatio === void 0 ? void 0 : `aspectRatio "${sent.aspectRatio}"`, sent.imageSize === void 0 ? void 0 : `imageSize "${sent.imageSize}"`].filter((field) => field !== void 0);
11052
+ parts.push(fields.length > 0 ? `The request itself was refused, and these optional fields were what it carried: ${fields.join(", ")}. It is not retried without them: re-issue the call with a different candidate value, or omit the field, because the upstream refused this request shape.` : "The request itself was refused while no aspectRatio or imageSize was set, so a different value for either cannot fix it; change the prompt or check the account before retrying.");
11053
+ }
11054
+ const excerpt = upstreamBodyExcerpt(errorText);
11055
+ if (excerpt !== "") parts.push(`Upstream said: ${excerpt}`);
11056
+ return parts.join(" ");
11057
+ }
11058
+ async function requestImage(accountPool, store, fetchFn, prompt, aspectRatio, imageSize, signal) {
10582
11059
  const tried = /* @__PURE__ */ new Set();
10583
11060
  while (true) {
10584
11061
  const { account, token, projectId } = await acquireAccount(accountPool, store, tried, fetchFn);
10585
11062
  tried.add(account.id);
10586
- const body = JSON.stringify(buildImageRequest(prompt, projectId || "antigravity-default", aspectRatio));
11063
+ const body = JSON.stringify(buildImageRequest(prompt, projectId || "antigravity-default", aspectRatio, imageSize));
10587
11064
  let response;
10588
11065
  let firstResponse;
10589
11066
  for (const endpoint of endpointCandidates()) try {
@@ -10615,7 +11092,10 @@ async function requestImage(accountPool, store, fetchFn, prompt, aspectRatio, si
10615
11092
  }
10616
11093
  throw new HarnessError(`Antigravity image generation was rate limited (429) without a stated reset time, so it is handled as a transient rate limit rather than a spent quota. ${upstreamBodyExcerpt(errorText)}`.trim(), "ANTIGRAVITY_IMAGE_RATE_LIMITED");
10617
11094
  }
10618
- throw new HarnessError(`Antigravity image generation failed (${status ?? "no response"}).${errorText ? ` ${errorText}` : ""}`, "ANTIGRAVITY_IMAGE_FAILED");
11095
+ throw new HarnessError(imageFailureMessage(status, errorText, {
11096
+ aspectRatio,
11097
+ imageSize
11098
+ }), "ANTIGRAVITY_IMAGE_FAILED");
10619
11099
  }
10620
11100
  }
10621
11101
  //#endregion
@@ -15123,8 +15603,17 @@ function readableLimitName(value) {
15123
15603
  function isWindow(value) {
15124
15604
  return value !== null;
15125
15605
  }
15606
+ /**
15607
+ * The identity a reading is filed under when no pool row names one.
15608
+ *
15609
+ * The composite local login identity, for the same reason the pool dedupes on it:
15610
+ * with several accounts in one composition, a workspace id alone would make two
15611
+ * members of that workspace share a slot — each showing the other's numbers. The
15612
+ * trailing fallbacks are for a credential that states nothing about itself, which
15613
+ * is the single-account case, where there is nobody to mix up.
15614
+ */
15126
15615
  function identityKey(credentials) {
15127
- return credentials.accountId ?? credentials.email ?? credentials.planType ?? "signed-in";
15616
+ return codexAccountIdentity(credentials) ?? credentials.planType ?? "signed-in";
15128
15617
  }
15129
15618
  //#endregion
15130
15619
  //#region src/host/routes.ts
@@ -27788,6 +28277,25 @@ const TOOL_SOURCE = /(?:^|[-_/])(?:read|view|cat|open|edit|write|patch|apply_pat
27788
28277
  /** 独立开关覆盖的检索工具;只有明确导航任务才允许进入新投影。 */
27789
28278
  const TOOL_RETRIEVAL = /^(?:read|web_fetch|grep|glob)$/;
27790
28279
  /**
28280
+ * PTC 家族的**程序传输**工具名(一个集合,两个名字)。
28281
+ *
28282
+ * 「模型写一段程序、工具调用发生在程序内部」是同一种传输,只是不同预设给主工具起
28283
+ * 了不同名字:PTC 预设叫 `run_code`,Code Mode 预设叫 `code_run`。凡是按工具名判断
28284
+ * 「这条结果来自程序传输」的地方都必须认这**整个集合**——名字只写一个,预设一换,
28285
+ * 大结果的 `ref://` 预览与精确补读就会静默失效。
28286
+ */
28287
+ const CACHE_WARMUP_PTC_TRANSPORT_TOOLS = /* @__PURE__ */ new Set(["run_code", "code_run"]);
28288
+ /**
28289
+ * 这个名字是否属于 PTC 家族的程序传输工具。
28290
+ *
28291
+ * 只做**精确**匹配:`code_runner` 之类的近似名不算,宁可少投影也不能误伤。
28292
+ * @param name - 工具名。
28293
+ * @returns 属于 {@link CACHE_WARMUP_PTC_TRANSPORT_TOOLS} 时为 true。
28294
+ */
28295
+ function isPtcTransportName(name) {
28296
+ return CACHE_WARMUP_PTC_TRANSPORT_TOOLS.has(name);
28297
+ }
28298
+ /**
27791
28299
  * 多节/多读输出进入新投影的最小长度门槛(字符)。
27792
28300
  *
27793
28301
  * 短于这个长度、又没有 spill 的普通源码与契约读一律逐字保留:
@@ -29083,7 +29591,7 @@ function planProjection(text, toolName, navigation, retrievalOnly, spilled = fal
29083
29591
  kind: "verbatim",
29084
29592
  strategy: "verbatim-contract"
29085
29593
  };
29086
- if (toolName === "run_code" && navigation === true) {
29594
+ if (isPtcTransportName(toolName) && navigation === true) {
29087
29595
  const outline = planRetrievalOutline(text, "read");
29088
29596
  if (outline !== void 0) return outline;
29089
29597
  if (retrievalOnly === true) return {
@@ -92344,6 +92852,20 @@ const PRESET_REGISTRY_PACKAGE = "@deepseek-ai/dsh-agent-preset";
92344
92852
  const WORKING_DIRECTORY_PACKAGE = "@deepseek-ai/dsh-working-directory";
92345
92853
  /** The standalone PTC executor introduced when tool presentation split into executor plugins. */
92346
92854
  const PTC_EXECUTOR_PACKAGE = "@deepseek-ai/dsh-tool-executor-ptc";
92855
+ /**
92856
+ * The native executor this package delegates Code Mode execution to.
92857
+ *
92858
+ * A program sub-call must run through the harness's own policy, approval, and
92859
+ * post-execute pipeline; re-implementing that pipeline is how a plugin silently
92860
+ * drops policy. A profile plugin cannot resolve the host's kernel packages, so
92861
+ * this package carries the executor as a real dependency and drives the host
92862
+ * registry with it.
92863
+ */
92864
+ const NATIVE_EXECUTOR_PACKAGE = "@deepseek-ai/dsh-tool-executor-native";
92865
+ /** This package's Code Mode row: the preset's own tools stay native, the rest is program-only. */
92866
+ const CODEMODE_PACKAGE = "@eddyskywalker/dsh-chatgpt-subscription/codemode";
92867
+ /** The deprecated presentation row generations without a native executor keep. */
92868
+ const AGENT_TOOL_PRESENTATION_PACKAGE = "@deepseek-ai/dsh-agent-tool-presentation";
92347
92869
  /** The id the `dispatch` preset is selectable under. */
92348
92870
  const DISPATCH_PRESET_ID = "dispatch";
92349
92871
  /**
@@ -92359,7 +92881,7 @@ const DISPATCH_PRESET_ID = "dispatch";
92359
92881
  const DISPATCH_PRESET = {
92360
92882
  id: DISPATCH_PRESET_ID,
92361
92883
  name: "调度模式",
92362
- description: "基于 PTC 模式的调度 Agent:先分诊任务复杂度,L2 任务在规划前先做澄清访谈,再由主代理规划、派发子代理(显式选择子代理模型)、审查与验收。",
92884
+ description: "基于 Code Mode 的调度 Agent:先分诊任务复杂度,L2 任务在规划前先做澄清访谈,再由主代理规划、派发子代理(显式选择子代理模型)、审查与验收。",
92363
92885
  order: 5,
92364
92886
  plugins: [
92365
92887
  {
@@ -92406,6 +92928,7 @@ L0/L1: execute directly (or 1 subagent for L1). L2: R-G, R9, and R6 are mandator
92406
92928
  - Lead-Core: tightly coupled cross-module/contract changes or sequential chains where splitting duplicates reads or causes hand_back loops. -> Implement directly in continuous steps (track in todo_write, no self-briefs) and run R6 verification.
92407
92929
  - Hybrid: unsettled shared contracts/seams plus orthogonal downstream work. -> Implement & freeze core seams yourself first, then delegate orthogonal adapters/tests/noisy exploration per R2/R5 (or subagent_fork for warm-context isolation).
92408
92930
  - Fan-out: >=3 orthogonal subtasks with disjoint files, settled interfaces, and clear parallel/routing gain. -> Coordinate (plan, R2/R5 dispatch, R6 review/acceptance, docs, non-delegable or disjoint parallel modules); never edit an active child's Boundary.
92931
+ - [Media generation] Generate/revise/retry images directly; delegate complex creative work, never a bare call.
92409
92932
 
92410
92933
  ## R2 · Model Routing (Check Availability First, Then Decide How to Dispatch)
92411
92934
  Check whether list_subagent_models exists (in PTC mode: tools.list_subagent_models). The tool row and provider/model/reasoning_effort parameters are mounted statically, so they are present even in a session that authorized no route at all. Read that fact instead of inferring it from the tool's output:
@@ -92650,9 +93173,8 @@ Fallback is a task-scoped, non-session-wide exception for delegated briefs: it c
92650
93173
  }
92651
93174
  },
92652
93175
  {
92653
- id: "tool-presentation",
92654
- name: "@deepseek-ai/dsh-agent-tool-presentation",
92655
- config: { mode: "ptc" }
93176
+ id: "code-mode",
93177
+ name: CODEMODE_PACKAGE
92656
93178
  },
92657
93179
  {
92658
93180
  id: "present",
@@ -92676,8 +93198,16 @@ Fallback is a task-scoped, non-session-wide exception for delegated briefs: it c
92676
93198
  * the export is absent; older harnesses keep it because their main entry does not
92677
93199
  * register the tool.
92678
93200
  *
92679
- * When the standalone PTC executor exists, it replaces the deprecated
92680
- * agent-tool-presentation compatibility row and its `{ mode: 'ptc' }` config.
93201
+ * Code Mode needs the native tool executor (it is the pipeline the program's
93202
+ * sub-calls are delegated to), so that package's presence decides the
93203
+ * presentation rows:
93204
+ *
93205
+ * - resolvable: the `code-mode` row mounts as-is and no PTC executor row exists
93206
+ * beside it (one executor per scope);
93207
+ * - absent: the row falls back to the deprecated agent-tool-presentation row and,
93208
+ * when the standalone PTC executor exists, to that executor instead — the
93209
+ * behaviour of every generation before Code Mode.
93210
+ *
92681
93211
  * @param resolves - whether a bare specifier resolves in this installation.
92682
93212
  * @returns the preset definition for the running harness generation.
92683
93213
  */
@@ -92685,7 +93215,8 @@ function resolveDispatchPreset(resolves = resolvesFromHere) {
92685
93215
  const hasWorkingDirectory = resolves(WORKING_DIRECTORY_PACKAGE);
92686
93216
  const hasLegacyListAgents = resolves("@deepseek-ai/dsh-tool-subagent-control/list-agents");
92687
93217
  const hasPtcExecutor = resolves(PTC_EXECUTOR_PACKAGE);
92688
- if (!hasWorkingDirectory && hasLegacyListAgents && !hasPtcExecutor) return DISPATCH_PRESET;
93218
+ const hasNativeExecutor = resolves(NATIVE_EXECUTOR_PACKAGE);
93219
+ if (!hasWorkingDirectory && hasLegacyListAgents && !hasPtcExecutor && hasNativeExecutor) return DISPATCH_PRESET;
92689
93220
  return {
92690
93221
  ...DISPATCH_PRESET,
92691
93222
  plugins: DISPATCH_PRESET.plugins.map((row) => {
@@ -92693,14 +93224,14 @@ function resolveDispatchPreset(resolves = resolvesFromHere) {
92693
93224
  ...row,
92694
93225
  config: row.config.filter((nested) => nested.id !== "tool-subagent-list-agents")
92695
93226
  };
92696
- if (row.id === "tool-presentation" && hasPtcExecutor) {
92697
- const { config: _omitted, ...rest } = row;
92698
- return {
92699
- ...rest,
92700
- id: "tool-executor-ptc",
92701
- name: PTC_EXECUTOR_PACKAGE
92702
- };
92703
- }
93227
+ if (row.id === "code-mode" && !hasNativeExecutor) return hasPtcExecutor ? {
93228
+ id: "tool-executor-ptc",
93229
+ name: PTC_EXECUTOR_PACKAGE
93230
+ } : {
93231
+ id: "tool-presentation",
93232
+ name: AGENT_TOOL_PRESENTATION_PACKAGE,
93233
+ config: { mode: "ptc" }
93234
+ };
92704
93235
  if (row.id !== "persona" || !hasWorkingDirectory || typeof row.config !== "object" || row.config === null) return row;
92705
93236
  const { suffix: _omitted, ...rest } = row.config;
92706
93237
  return {
@@ -94577,12 +95108,13 @@ const CACHE_WARMUP_PROVIDER = "kimi-code";
94577
95108
  /**
94578
95109
  * 允许进入结果投影的工具集合。
94579
95110
  *
94580
- * 除了 PTC 的 `run_code`,宿主的独立 Shell 与后台任务输出工具(`win_bash`、
94581
- * `bash`、`pwsh`、`job_output`)同样经常吐出整屏测试/构建日志。源码读取/编辑类
94582
- * 工具(`read`、`edit`、`grep`、`glob`、CodeGraph 等)不在此列,保持 100% 原样。
95111
+ * 除了 PTC 家族的程序传输(`run_code` 与 Code Mode 的 `code_run`),宿主的独立
95112
+ * Shell 与后台任务输出工具(`win_bash`、`bash`、`pwsh`、`job_output`)同样经常
95113
+ * 吐出整屏测试/构建日志。源码读取/编辑类工具(`read`、`edit`、`grep`、`glob`、
95114
+ * CodeGraph 等)不在此列,保持 100% 原样。
94583
95115
  */
94584
95116
  const CACHE_WARMUP_SOURCE_TOOLS = /* @__PURE__ */ new Set([
94585
- "run_code",
95117
+ ...CACHE_WARMUP_PTC_TRANSPORT_TOOLS,
94586
95118
  "win_bash",
94587
95119
  "bash",
94588
95120
  "pwsh",
@@ -94599,7 +95131,7 @@ const CACHE_WARMUP_SECTION_NAME = "plugin:cache-warmup";
94599
95131
  const CACHE_WARMUP_GUIDANCE = [
94600
95132
  "## Result previews",
94601
95133
  "",
94602
- "On large `run_code` results this line may replace the output with a preview: a short description plus one or more `ref://<session>/<sha256>` handles.",
95134
+ "On large `run_code`/`code_run` results this line may replace the output with a preview: a short description plus one or more `ref://<session>/<sha256>` handles.",
94603
95135
  "",
94604
95136
  "- A preview is **not** the full result. It states `complete`, `omitted` and `nextOffset` explicitly; treat an incomplete preview as incomplete data, not as the whole output.",
94605
95137
  "- To read the original, call `cache_warmup_result` with `action: \"page\"` and that `refId`, then follow `nextOffset` until `complete` is true.",
@@ -94818,7 +95350,8 @@ function installCacheWarmup(ctx, options) {
94818
95350
  *
94819
95351
  * 决策取舍写在代码里,因为它全是「什么时候**不动**」:
94820
95352
  * - 不是 `accept`(`block` 的 feedback 是别人的东西);
94821
- * - 工具名不是 `run_code`、线路不是本线、会话拿不到;
95353
+ * - 工具名不在源工具集合里(`run_code`/`code_run`/Shell/`job_output`)、线路不是
95354
+ * 本线、会话拿不到;
94822
95355
  * - 没有存储、内容为空。
94823
95356
  * 上面任何一条成立就原样返回同一个决策对象。
94824
95357
  *
@@ -94836,7 +95369,7 @@ function installCacheWarmup(ctx, options) {
94836
95369
  const retrievalTool = CACHE_WARMUP_RETRIEVAL_TOOLS.has(exec.name);
94837
95370
  if (!sourceTool && !retrievalTool) return decision;
94838
95371
  const settings = options.settings();
94839
- const ptcRetrieval = exec.name === "run_code" && settings.retrievalEnabled === true;
95372
+ const ptcRetrieval = isPtcTransportName(exec.name) && settings.retrievalEnabled === true;
94840
95373
  if (sourceTool && !settings.enabled && !ptcRetrieval) return decision;
94841
95374
  if (retrievalTool && settings.retrievalEnabled !== true) return decision;
94842
95375
  const navigation = retrievalTool || ptcRetrieval ? cacheWarmupNavigationIntentOf(exec.agent) : void 0;
@@ -95345,14 +95878,14 @@ const WORKBUDDY_STANDARD_EFFORTS = [
95345
95878
  * Snap a level onto the nearest rung a model's ladder actually exposes.
95346
95879
  *
95347
95880
  * The gateway names a default from a wider vocabulary than the ladder it
95348
- * publishes for the same model: an effort-only entry (minimax-m3, kimi-k3,
95349
- * `glm-5.3` in the cn region) reports `effort: 'medium'` while exposing only
95350
- * `low`/`high`/`max`. Letting that value through is not cosmetic — a default
95351
- * outside the ladder is discarded by the level-resolution step, the request
95352
- * then carries no `reasoning_effort`, and the model returns an EMPTY
95353
- * `reasoning_content` (measured on minimax-m3: 0 characters across three
95354
- * samples, versus 282-659 with the field). Ties resolve upward, which is the
95355
- * mapping the sibling Kimi line documents for `medium`. A ladder that names
95881
+ * publishes for the same model: an effort-only entry — the shape `kimi-k3`
95882
+ * publishes — names a single default while exposing only `low`/`high`/`max`.
95883
+ * Letting an unlisted value through is not cosmetic — a default outside the
95884
+ * ladder is discarded by the level-resolution step, the request then carries no
95885
+ * `reasoning_effort`, and the model returns an EMPTY `reasoning_content`
95886
+ * (measured on `minimax-m3` while that id was still served: 0 characters across
95887
+ * three samples, versus 282-659 with the field). Ties resolve upward, which is
95888
+ * the mapping the sibling Kimi line documents for `medium`. A ladder that names
95356
95889
  * no level (a text-only model) yields nothing to converge to.
95357
95890
  */
95358
95891
  function convergeWorkBuddyEffort(effort, ladder) {
@@ -95635,96 +96168,27 @@ function refreshSourceForDomain(domain) {
95635
96168
  /**
95636
96169
  * Models the gateway serves but does not publish in `/v3/config`.
95637
96170
  *
95638
- * The catalog is a published list, not the served one: measured on the
95639
- * international backend, `gpt-6-sol`, `gpt-6-luna` and `gemini-3.8-flash` all
95640
- * answer a normal streaming completion with 200 while being absent from
96171
+ * The catalog is a published list, not the served one: measured 2026-10-09 on
96172
+ * the international backend, `gpt-6-sol`, `gpt-6-luna` and `gemini-3.8-flash`
96173
+ * all answered a normal streaming completion with 200 while being absent from
95641
96174
  * `/v3/config` entirely, and a reachable gateway *replaces* the shipped table
95642
- * with that list. Without these rows the picker would simply never show them, so
95643
- * they are declared here and merged into a live catalog too
95644
- * ({@link withUnpublishedModels}) rather than living in the fallback alone.
95645
- *
95646
- * What each row records, and how it was established, differs by field:
95647
- *
95648
- * - **Existence, the reasoning ladder, image support** were measured against the
95649
- * live endpoint, not inferred from the name. A rejected level answers 400
95650
- * `code 11133` with `extError` `400002`; `gpt-6-sol` takes exactly the ladder
95651
- * `gpt-6-astra` publishes (`minimal` refused, `low`/`medium`/`high`/`xhigh`/
95652
- * `max` accepted) and both accept a 1x1 PNG.
95653
- * - **Context window, output cap and `canDisableThinking` are inherited from the
95654
- * published sibling in the same family** — `gpt-6-astra` for `gpt-6-sol` and
95655
- * `gpt-6-luna`, `gemini-3.5-flash` for `gemini-3.8-flash`. They are marked
95656
- * here rather than measured because the gateway publishes nothing for these
95657
- * ids and offers no cheap way to read the real figure: it accepts a
95658
- * `max_tokens` larger than any published window (even for `gpt-6-astra`, whose
95659
- * own catalog caps at 128000), so the number is neither enforced nor
95660
- * discoverable from the request side.
95661
- *
95662
- * Every field stays overridable per model from the settings card, and an
95663
- * inherited window errs low rather than high: over-declaring overflows the
95664
- * gateway mid-session, while under-declaring only compresses earlier.
95665
- */
95666
- const UNPUBLISHED_MODELS = [
95667
- {
95668
- id: "gpt-6-sol",
95669
- name: "GPT-6-Sol",
95670
- contextWindow: 4e5,
95671
- maxContextWindow: 1e6,
95672
- maxTokens: 128e3,
95673
- regions: ["intl"],
95674
- supportsImage: true,
95675
- reasoningEfforts: [
95676
- "low",
95677
- "medium",
95678
- "high",
95679
- "xhigh",
95680
- "max"
95681
- ],
95682
- defaultReasoningEffort: "high",
95683
- canDisableThinking: true,
95684
- description: "OpenAI 旗舰模型,擅长复杂推理与长程任务"
95685
- },
95686
- {
95687
- id: "gpt-6-luna",
95688
- name: "GPT-6-Luna",
95689
- contextWindow: 4e5,
95690
- maxContextWindow: 1e6,
95691
- maxTokens: 128e3,
95692
- regions: ["intl"],
95693
- supportsImage: true,
95694
- reasoningEfforts: [
95695
- "low",
95696
- "medium",
95697
- "high",
95698
- "xhigh",
95699
- "max"
95700
- ],
95701
- defaultReasoningEffort: "high",
95702
- canDisableThinking: true,
95703
- description: "OpenAI 轻量模型,响应快速,适合日常任务"
95704
- },
95705
- {
95706
- id: "gemini-3.8-flash",
95707
- name: "Gemini-3.8-Flash",
95708
- contextWindow: 1e6,
95709
- maxContextWindow: 1e6,
95710
- maxTokens: 65536,
95711
- regions: ["intl"],
95712
- supportsImage: true,
95713
- reasoningEfforts: [
95714
- "low",
95715
- "high",
95716
- "max"
95717
- ],
95718
- defaultReasoningEffort: "medium",
95719
- canDisableThinking: false,
95720
- description: "能力均衡,适合日常使用"
95721
- }
95722
- ];
96175
+ * with that list — so a row living only in the fallback would drop out of the
96176
+ * picker the moment the catalog loaded.
96177
+ *
96178
+ * The list is empty as of the 2026-11-04 read: all three ids are published now,
96179
+ * with figures measured on the wire, and their rows therefore live in
96180
+ * {@link FALLBACK_MODELS} like any other. The seam stays rather than being
96181
+ * deleted because the gap it covers belongs to the gateway, not to those three
96182
+ * models: the next id it serves without listing belongs here, and
96183
+ * {@link withUnpublishedModels} is what keeps such a row offered.
96184
+ */
96185
+ const UNPUBLISHED_MODELS = [];
95723
96186
  /**
95724
96187
  * Offline fallback catalog, transcribed from a live `/v3/config` read.
95725
96188
  *
95726
- * Used when the gateway cannot be reached (first run before sign-in, or a
95727
- * network failure); a reachable gateway always wins.
96189
+ * Last transcribed 2026-11-04 from both backends. Used when the gateway cannot
96190
+ * be reached (first run before sign-in, or a network failure); a reachable
96191
+ * gateway always wins.
95728
96192
  */
95729
96193
  const FALLBACK_MODELS$1 = [
95730
96194
  {
@@ -95740,20 +96204,6 @@ const FALLBACK_MODELS$1 = [
95740
96204
  canDisableThinking: false,
95741
96205
  description: "优秀的编码模型,适合日常使用"
95742
96206
  },
95743
- {
95744
- id: "default",
95745
- name: "Default",
95746
- contextWindow: 56e3,
95747
- maxContextWindow: 56e3,
95748
- maxTokens: 24e3,
95749
- regions: ["cn"],
95750
- credits: { cn: "2.00" },
95751
- supportsImage: false,
95752
- reasoningEfforts: [],
95753
- defaultReasoningEffort: null,
95754
- canDisableThinking: false,
95755
- description: ""
95756
- },
95757
96207
  {
95758
96208
  id: "fast-model",
95759
96209
  name: "Fast",
@@ -95768,7 +96218,7 @@ const FALLBACK_MODELS$1 = [
95768
96218
  "high",
95769
96219
  "max"
95770
96220
  ],
95771
- defaultReasoningEffort: "medium",
96221
+ defaultReasoningEffort: "high",
95772
96222
  canDisableThinking: false,
95773
96223
  description: "响应快,适合简单任务"
95774
96224
  },
@@ -95786,7 +96236,7 @@ const FALLBACK_MODELS$1 = [
95786
96236
  "high",
95787
96237
  "max"
95788
96238
  ],
95789
- defaultReasoningEffort: "medium",
96239
+ defaultReasoningEffort: "high",
95790
96240
  canDisableThinking: false,
95791
96241
  description: "速度与质量兼顾,日常工作首选"
95792
96242
  },
@@ -95809,20 +96259,65 @@ const FALLBACK_MODELS$1 = [
95809
96259
  description: "高质量输出,胜任复杂任务"
95810
96260
  },
95811
96261
  {
95812
- id: "deep-model",
95813
- name: "Deep",
95814
- contextWindow: 2e5,
95815
- maxContextWindow: 2e5,
95816
- maxTokens: 24e3,
96262
+ id: "deepseek-v4.1-flash",
96263
+ name: "Deepseek-V4.1-Flash",
96264
+ contextWindow: 3e5,
96265
+ maxContextWindow: 1e6,
96266
+ maxTokens: 128e3,
96267
+ regions: ["intl", "cn"],
96268
+ credits: {
96269
+ cn: "0.11",
96270
+ intl: "0.00"
96271
+ },
96272
+ supportsImage: true,
96273
+ reasoningEfforts: [
96274
+ "low",
96275
+ "high",
96276
+ "max"
96277
+ ],
96278
+ defaultReasoningEffort: "high",
96279
+ canDisableThinking: false,
96280
+ description: "DeepSeek 旗舰模型,支持 1M 上下文窗口,原生多模态"
96281
+ },
96282
+ {
96283
+ id: "deepseek-v4.1-flash-sg",
96284
+ name: "Deepseek-V4.1-Flash",
96285
+ contextWindow: 3e5,
96286
+ maxContextWindow: 1e6,
96287
+ maxTokens: 128e3,
95817
96288
  regions: ["intl"],
95818
- credits: { intl: "3.33" },
96289
+ credits: { intl: "0.03" },
95819
96290
  supportsImage: true,
95820
- reasoningEfforts: [],
95821
- defaultReasoningEffort: null,
96291
+ reasoningEfforts: [
96292
+ "low",
96293
+ "high",
96294
+ "max"
96295
+ ],
96296
+ defaultReasoningEffort: "high",
95822
96297
  canDisableThinking: false,
95823
- description: "深度推理,适合深度分析与难题"
96298
+ description: "DeepSeek 旗舰模型,支持 1M 上下文窗口,原生多模态"
96299
+ },
96300
+ {
96301
+ id: "glm-5.3-flash",
96302
+ name: "GLM-5.3-Flash",
96303
+ contextWindow: 3e5,
96304
+ maxContextWindow: 1e6,
96305
+ maxTokens: 32e3,
96306
+ regions: ["intl", "cn"],
96307
+ credits: {
96308
+ cn: "0.08",
96309
+ intl: "0.08"
96310
+ },
96311
+ supportsImage: true,
96312
+ reasoningEfforts: [
96313
+ "low",
96314
+ "high",
96315
+ "max"
96316
+ ],
96317
+ defaultReasoningEffort: "high",
96318
+ canDisableThinking: false,
96319
+ description: "原生多模态模型,擅长视觉理解与专业任务"
95824
96320
  },
95825
- ...UNPUBLISHED_MODELS.filter((model) => model.id === "gpt-6-sol" || model.id === "gpt-6-luna"),
95826
96321
  {
95827
96322
  id: "gpt-6-astra",
95828
96323
  name: "GPT-6-Astra",
@@ -95843,6 +96338,37 @@ const FALLBACK_MODELS$1 = [
95843
96338
  canDisableThinking: true,
95844
96339
  description: "OpenAI 旗舰模型,擅长复杂推理与长程任务"
95845
96340
  },
96341
+ {
96342
+ id: "hy4-preview",
96343
+ name: "Hy4 Preview",
96344
+ contextWindow: 2e5,
96345
+ maxContextWindow: 1e6,
96346
+ maxTokens: 64e3,
96347
+ regions: ["intl"],
96348
+ credits: { intl: "0.29" },
96349
+ supportsImage: true,
96350
+ reasoningEfforts: ["high"],
96351
+ defaultReasoningEffort: "high",
96352
+ canDisableThinking: false,
96353
+ description: "混元思考模型,具有增强的推理能力"
96354
+ },
96355
+ {
96356
+ id: "hy3",
96357
+ name: "Hy3",
96358
+ contextWindow: 192e3,
96359
+ maxContextWindow: 192e3,
96360
+ maxTokens: 64e3,
96361
+ regions: ["intl", "cn"],
96362
+ credits: {
96363
+ cn: "0.00",
96364
+ intl: "0.00"
96365
+ },
96366
+ supportsImage: true,
96367
+ reasoningEfforts: ["low", "high"],
96368
+ defaultReasoningEffort: "high",
96369
+ canDisableThinking: false,
96370
+ description: "混元思考模型,具有增强的推理能力"
96371
+ },
95846
96372
  {
95847
96373
  id: "gpt-5.6-sol",
95848
96374
  name: "GPT-5.6-Sol",
@@ -95949,8 +96475,8 @@ const FALLBACK_MODELS$1 = [
95949
96475
  maxTokens: 48e3,
95950
96476
  regions: ["intl", "cn"],
95951
96477
  credits: {
95952
- cn: "0.79",
95953
- intl: "0.79"
96478
+ cn: "0.69",
96479
+ intl: "0.69"
95954
96480
  },
95955
96481
  supportsImage: true,
95956
96482
  reasoningEfforts: [
@@ -95959,30 +96485,9 @@ const FALLBACK_MODELS$1 = [
95959
96485
  "max"
95960
96486
  ],
95961
96487
  defaultReasoningEffort: "high",
95962
- canDisableThinking: true,
96488
+ canDisableThinking: false,
95963
96489
  description: "能力均衡,适合日常使用"
95964
96490
  },
95965
- {
95966
- id: "glm-5.3-flash",
95967
- name: "GLM-5.3-Flash",
95968
- contextWindow: 3e5,
95969
- maxContextWindow: 1e6,
95970
- maxTokens: 32e3,
95971
- regions: ["intl", "cn"],
95972
- credits: {
95973
- cn: "0.06",
95974
- intl: "0.06"
95975
- },
95976
- supportsImage: true,
95977
- reasoningEfforts: [
95978
- "low",
95979
- "high",
95980
- "max"
95981
- ],
95982
- defaultReasoningEffort: "high",
95983
- canDisableThinking: true,
95984
- description: "原生多模态模型,擅长视觉理解与专业任务"
95985
- },
95986
96491
  {
95987
96492
  id: "glm-5.2",
95988
96493
  name: "GLM-5.2",
@@ -95995,101 +96500,63 @@ const FALLBACK_MODELS$1 = [
95995
96500
  intl: "0.79"
95996
96501
  },
95997
96502
  supportsImage: true,
95998
- reasoningEfforts: ["high", "xhigh"],
96503
+ reasoningEfforts: ["high"],
95999
96504
  defaultReasoningEffort: "high",
96000
- canDisableThinking: true,
96505
+ canDisableThinking: false,
96001
96506
  description: "1M 上下文,擅长长程任务"
96002
96507
  },
96003
96508
  {
96004
- id: "glm-5.1",
96005
- name: "GLM-5.1",
96006
- contextWindow: 2e5,
96007
- maxContextWindow: 2e5,
96008
- maxTokens: 48e3,
96009
- regions: ["cn"],
96010
- credits: { cn: "0.79" },
96509
+ id: "minimax-m3.1-flash",
96510
+ name: "MiniMax-M3.1-Flash",
96511
+ contextWindow: 3e5,
96512
+ maxContextWindow: 1e6,
96513
+ maxTokens: 128e3,
96514
+ regions: ["intl", "cn"],
96515
+ credits: {
96516
+ cn: "0.08",
96517
+ intl: "0.08"
96518
+ },
96011
96519
  supportsImage: true,
96012
96520
  reasoningEfforts: [
96013
96521
  "low",
96522
+ "medium",
96014
96523
  "high",
96524
+ "xhigh",
96015
96525
  "max"
96016
96526
  ],
96017
- defaultReasoningEffort: "medium",
96527
+ defaultReasoningEffort: "max",
96018
96528
  canDisableThinking: false,
96019
- description: ""
96529
+ description: "原生多模态,擅长office、agentic、编程任务,推理速度优异"
96020
96530
  },
96021
96531
  {
96022
- id: "glm-5v-turbo",
96023
- name: "GLM-5v-Turbo",
96024
- contextWindow: 2e5,
96025
- maxContextWindow: 2e5,
96026
- maxTokens: 64e3,
96027
- regions: ["cn"],
96028
- credits: { cn: "0.71" },
96532
+ id: "kimi-k3.1-pro",
96533
+ name: "Kimi-K3.1-Pro",
96534
+ contextWindow: 3e5,
96535
+ maxContextWindow: 1e6,
96536
+ maxTokens: 32e3,
96537
+ regions: ["intl", "cn"],
96538
+ credits: {
96539
+ cn: "0.97",
96540
+ intl: "0.97"
96541
+ },
96029
96542
  supportsImage: true,
96030
96543
  reasoningEfforts: [
96031
96544
  "low",
96032
96545
  "high",
96033
96546
  "max"
96034
96547
  ],
96035
- defaultReasoningEffort: "medium",
96036
- canDisableThinking: false,
96037
- description: ""
96038
- },
96039
- {
96040
- id: "glm-5.0",
96041
- name: "GLM-5.0",
96042
- contextWindow: 2e5,
96043
- maxContextWindow: 2e5,
96044
- maxTokens: 48e3,
96045
- regions: ["cn"],
96046
- credits: { cn: "0.80" },
96047
- supportsImage: false,
96048
- reasoningEfforts: [],
96049
- defaultReasoningEffort: null,
96050
- canDisableThinking: false,
96051
- description: ""
96052
- },
96053
- {
96054
- id: "glm-5.0-turbo",
96055
- name: "GLM-5.0-Turbo",
96056
- contextWindow: 2e5,
96057
- maxContextWindow: 2e5,
96058
- maxTokens: 48e3,
96059
- regions: ["cn"],
96060
- credits: { cn: "0.95" },
96061
- supportsImage: false,
96062
- reasoningEfforts: [
96063
- "low",
96064
- "high",
96065
- "max"
96066
- ],
96067
- defaultReasoningEffort: "medium",
96068
- canDisableThinking: false,
96069
- description: ""
96070
- },
96071
- {
96072
- id: "glm-4.7",
96073
- name: "GLM-4.7",
96074
- contextWindow: 2e5,
96075
- maxContextWindow: 2e5,
96076
- maxTokens: 48e3,
96077
- regions: ["cn"],
96078
- credits: { cn: "0.23" },
96079
- supportsImage: false,
96080
- reasoningEfforts: [],
96081
- defaultReasoningEffort: null,
96548
+ defaultReasoningEffort: "high",
96082
96549
  canDisableThinking: false,
96083
- description: ""
96550
+ description: "Kimi 全新旗舰模型,3万亿级参数。办公、代码、3D 创作与长程 Agent 能力全面升级。"
96084
96551
  },
96085
96552
  {
96086
- id: "glm-4.6v",
96087
- name: "GLM-4.6V",
96088
- contextWindow: 128e3,
96089
- maxContextWindow: 128e3,
96553
+ id: "kimi-k3.1-pro-highspeed",
96554
+ name: "Kimi-K3.1-Pro-Highspeed",
96555
+ contextWindow: 3e5,
96556
+ maxContextWindow: 1e6,
96090
96557
  maxTokens: 32e3,
96091
- regions: ["cn"],
96092
- credits: { cn: "0.11" },
96558
+ regions: ["intl"],
96559
+ credits: { intl: "1.56" },
96093
96560
  supportsImage: true,
96094
96561
  reasoningEfforts: [
96095
96562
  "low",
@@ -96098,21 +96565,7 @@ const FALLBACK_MODELS$1 = [
96098
96565
  ],
96099
96566
  defaultReasoningEffort: "high",
96100
96567
  canDisableThinking: false,
96101
- description: ""
96102
- },
96103
- {
96104
- id: "glm-4.6",
96105
- name: "GLM-4.6",
96106
- contextWindow: 168e3,
96107
- maxContextWindow: 168e3,
96108
- maxTokens: 32e3,
96109
- regions: ["cn"],
96110
- credits: { cn: "0.23" },
96111
- supportsImage: false,
96112
- reasoningEfforts: [],
96113
- defaultReasoningEffort: null,
96114
- canDisableThinking: false,
96115
- description: ""
96568
+ description: "K3.1 Pro 高速版本,响应速度提升至 K3.1 Pro 的 2–3 倍,交互更流畅。"
96116
96569
  },
96117
96570
  {
96118
96571
  id: "kimi-k3",
@@ -96128,166 +96581,121 @@ const FALLBACK_MODELS$1 = [
96128
96581
  "high",
96129
96582
  "max"
96130
96583
  ],
96131
- defaultReasoningEffort: "medium",
96584
+ defaultReasoningEffort: "high",
96132
96585
  canDisableThinking: false,
96133
96586
  description: "擅长处理复杂的长程自主任务,前端开发能力突出,同时在知识工作与科研推理上表现出色。"
96134
96587
  },
96135
96588
  {
96136
- id: "kimi-k3-1",
96137
- name: "Kimi-K3",
96138
- contextWindow: 1e6,
96139
- maxContextWindow: 1e6,
96140
- maxTokens: 32e3,
96141
- regions: ["cn"],
96142
- credits: { cn: "1.62" },
96589
+ id: "grok-4.7",
96590
+ name: "Grok-4.7",
96591
+ contextWindow: 2e5,
96592
+ maxContextWindow: 5e5,
96593
+ maxTokens: 128e3,
96594
+ regions: ["intl"],
96595
+ credits: { intl: "1.90" },
96143
96596
  supportsImage: true,
96144
96597
  reasoningEfforts: [
96145
96598
  "low",
96599
+ "medium",
96146
96600
  "high",
96147
- "max"
96601
+ "xhigh"
96148
96602
  ],
96149
- defaultReasoningEffort: "medium",
96603
+ defaultReasoningEffort: "high",
96150
96604
  canDisableThinking: false,
96151
- description: "擅长处理复杂的长程自主任务,前端开发能力突出,同时在知识工作与科研推理上表现出色。"
96605
+ description: "SpaceXAI 打造的前沿模型,专为编程、智能体任务及知识型工作而设计。"
96152
96606
  },
96153
96607
  {
96154
- id: "kimi-k2.8-preview",
96155
- name: "Kimi-K2.8-Preview",
96608
+ id: "gemini-3.8-flash",
96609
+ name: "Gemini-3.8-Flash",
96156
96610
  contextWindow: 3e5,
96157
96611
  maxContextWindow: 1e6,
96158
- maxTokens: 32e3,
96159
- regions: ["intl", "cn"],
96160
- credits: {
96161
- cn: "0.77",
96162
- intl: "0.77"
96163
- },
96164
- supportsImage: true,
96165
- reasoningEfforts: [
96166
- "low",
96167
- "high",
96168
- "max"
96169
- ],
96170
- defaultReasoningEffort: "high",
96171
- canDisableThinking: true,
96172
- description: "擅长处理复杂的长程自主任务,前端开发能力突出,同时在知识工作与科研推理上表现出色。"
96173
- },
96174
- {
96175
- id: "kimi-k2.7",
96176
- name: "Kimi-K2.7-Code",
96177
- contextWindow: 256e3,
96178
- maxContextWindow: 256e3,
96179
- maxTokens: 32e3,
96180
- regions: ["cn"],
96181
- credits: { cn: "0.57" },
96182
- supportsImage: true,
96183
- reasoningEfforts: [
96184
- "low",
96185
- "high",
96186
- "max"
96187
- ],
96188
- defaultReasoningEffort: "medium",
96189
- canDisableThinking: false,
96190
- description: ""
96191
- },
96192
- {
96193
- id: "kimi-k2.6",
96194
- name: "Kimi-K2.6",
96195
- contextWindow: 256e3,
96196
- maxContextWindow: 256e3,
96197
- maxTokens: 32e3,
96198
- regions: ["intl", "cn"],
96199
- credits: {
96200
- cn: "0.52",
96201
- intl: "0.52"
96202
- },
96612
+ maxTokens: 65536,
96613
+ regions: ["intl"],
96614
+ credits: { intl: "0.95" },
96203
96615
  supportsImage: true,
96204
96616
  reasoningEfforts: [
96205
96617
  "low",
96206
- "high",
96207
- "max"
96618
+ "medium",
96619
+ "high"
96208
96620
  ],
96209
96621
  defaultReasoningEffort: "medium",
96210
96622
  canDisableThinking: false,
96211
- description: "多模态模型,适合日常任务"
96623
+ description: "能力均衡,适合日常使用"
96212
96624
  },
96213
96625
  {
96214
- id: "kimi-k2.5",
96215
- name: "Kimi-K2.5",
96216
- contextWindow: 164e3,
96217
- maxContextWindow: 164e3,
96218
- maxTokens: 32e3,
96219
- regions: ["cn"],
96220
- credits: { cn: "0.45" },
96626
+ id: "gpt-6-sol",
96627
+ name: "GPT-6-Sol",
96628
+ contextWindow: 1e6,
96629
+ maxContextWindow: 1e6,
96630
+ maxTokens: 128e3,
96631
+ regions: ["intl"],
96632
+ credits: { intl: "1.33" },
96221
96633
  supportsImage: true,
96222
96634
  reasoningEfforts: [
96223
96635
  "low",
96636
+ "medium",
96224
96637
  "high",
96225
- "max"
96226
- ],
96227
- defaultReasoningEffort: "high",
96228
- canDisableThinking: false,
96229
- description: ""
96230
- },
96231
- {
96232
- id: "kimi-k2-thinking",
96233
- name: "Kimi-K2-Thinking",
96234
- contextWindow: 164e3,
96235
- maxContextWindow: 164e3,
96236
- maxTokens: 32e3,
96237
- regions: ["cn"],
96238
- credits: { cn: "0.54" },
96239
- supportsImage: false,
96240
- reasoningEfforts: [
96241
- "low",
96242
- "high",
96243
- "max"
96638
+ "xhigh"
96244
96639
  ],
96245
96640
  defaultReasoningEffort: "high",
96246
96641
  canDisableThinking: false,
96247
- description: ""
96642
+ description: "专为复杂编码和智能体工作流构建的模型"
96248
96643
  },
96249
96644
  {
96250
- id: "deepseek-v4.1-flash",
96251
- name: "Deepseek-V4.1-Flash",
96252
- contextWindow: 3e5,
96645
+ id: "gpt-6-luna",
96646
+ name: "GPT-6-Luna",
96647
+ contextWindow: 1e6,
96253
96648
  maxContextWindow: 1e6,
96254
96649
  maxTokens: 128e3,
96255
- regions: ["intl", "cn"],
96256
- credits: {
96257
- cn: "0.11",
96258
- intl: "0.00"
96259
- },
96650
+ regions: ["intl"],
96651
+ credits: { intl: "0.07" },
96260
96652
  supportsImage: true,
96261
96653
  reasoningEfforts: [
96262
96654
  "low",
96655
+ "medium",
96263
96656
  "high",
96264
- "max"
96657
+ "xhigh"
96265
96658
  ],
96266
96659
  defaultReasoningEffort: "high",
96267
96660
  canDisableThinking: false,
96268
- description: "DeepSeek 旗舰模型,支持 1M 上下文窗口,原生多模态"
96661
+ description: "专为单一聚焦、高吞吐量任务打造的高效模型"
96269
96662
  },
96270
96663
  {
96271
- id: "deepseek-v4.1-flash-sg",
96272
- name: "Deepseek-V4.1-Flash",
96273
- contextWindow: 3e5,
96664
+ id: "gpt-6.1-sol",
96665
+ name: "GPT-6.1-Sol",
96666
+ contextWindow: 272e3,
96274
96667
  maxContextWindow: 1e6,
96275
96668
  maxTokens: 128e3,
96276
96669
  regions: ["intl"],
96277
- credits: { intl: "0.03" },
96670
+ credits: { intl: "1.33" },
96278
96671
  supportsImage: true,
96279
96672
  reasoningEfforts: [
96280
96673
  "low",
96674
+ "medium",
96281
96675
  "high",
96282
- "max"
96676
+ "xhigh"
96283
96677
  ],
96284
96678
  defaultReasoningEffort: "high",
96285
96679
  canDisableThinking: false,
96286
- description: "DeepSeek 旗舰模型,支持 1M 上下文窗口,原生多模态"
96680
+ description: "专为复杂编码和智能体工作流构建的模型"
96681
+ },
96682
+ {
96683
+ id: "default",
96684
+ name: "Default",
96685
+ contextWindow: 56e3,
96686
+ maxContextWindow: 56e3,
96687
+ maxTokens: 24e3,
96688
+ regions: ["cn"],
96689
+ credits: { cn: "2.00" },
96690
+ supportsImage: false,
96691
+ reasoningEfforts: [],
96692
+ defaultReasoningEffort: null,
96693
+ canDisableThinking: false,
96694
+ description: ""
96287
96695
  },
96288
96696
  {
96289
96697
  id: "deepseek-v4-pro",
96290
- name: "Deepseek-V4-Pro",
96698
+ name: "DeepSeek-V4-Pro",
96291
96699
  contextWindow: 1e6,
96292
96700
  maxContextWindow: 1e6,
96293
96701
  maxTokens: 128e3,
@@ -96340,52 +96748,190 @@ const FALLBACK_MODELS$1 = [
96340
96748
  description: ""
96341
96749
  },
96342
96750
  {
96343
- id: "hy4-preview",
96344
- name: "Hy4 preview",
96751
+ id: "minimax-m2.5",
96752
+ name: "MiniMax-M2.5",
96345
96753
  contextWindow: 2e5,
96346
- maxContextWindow: 1e6,
96347
- maxTokens: 64e3,
96348
- regions: ["intl", "cn"],
96349
- credits: {
96350
- cn: "0.29",
96351
- intl: "0.29"
96352
- },
96754
+ maxContextWindow: 2e5,
96755
+ maxTokens: 48e3,
96756
+ regions: ["cn"],
96757
+ credits: { cn: "0.18" },
96758
+ supportsImage: false,
96759
+ reasoningEfforts: [
96760
+ "low",
96761
+ "high",
96762
+ "max"
96763
+ ],
96764
+ defaultReasoningEffort: "high",
96765
+ canDisableThinking: false,
96766
+ description: ""
96767
+ },
96768
+ {
96769
+ id: "minimax-m2.7",
96770
+ name: "MiniMax-M2.7",
96771
+ contextWindow: 2e5,
96772
+ maxContextWindow: 2e5,
96773
+ maxTokens: 48e3,
96774
+ regions: ["cn"],
96775
+ credits: { cn: "0.19" },
96353
96776
  supportsImage: true,
96354
- reasoningEfforts: ["high"],
96777
+ reasoningEfforts: [
96778
+ "low",
96779
+ "high",
96780
+ "max"
96781
+ ],
96355
96782
  defaultReasoningEffort: "high",
96356
96783
  canDisableThinking: false,
96357
- description: "混元思考模型,具有增强的推理能力"
96784
+ description: ""
96358
96785
  },
96359
96786
  {
96360
- id: "hy4-preview-f",
96361
- name: "Hy4 preview",
96787
+ id: "glm-5.0",
96788
+ name: "GLM-5.0",
96789
+ contextWindow: 2e5,
96790
+ maxContextWindow: 2e5,
96791
+ maxTokens: 48e3,
96792
+ regions: ["cn"],
96793
+ credits: { cn: "0.80" },
96794
+ supportsImage: false,
96795
+ reasoningEfforts: [],
96796
+ defaultReasoningEffort: null,
96797
+ canDisableThinking: false,
96798
+ description: ""
96799
+ },
96800
+ {
96801
+ id: "glm-5.0-turbo",
96802
+ name: "GLM-5.0-Turbo",
96803
+ contextWindow: 2e5,
96804
+ maxContextWindow: 2e5,
96805
+ maxTokens: 48e3,
96806
+ regions: ["cn"],
96807
+ credits: { cn: "0.95" },
96808
+ supportsImage: false,
96809
+ reasoningEfforts: [
96810
+ "low",
96811
+ "high",
96812
+ "max"
96813
+ ],
96814
+ defaultReasoningEffort: "high",
96815
+ canDisableThinking: false,
96816
+ description: ""
96817
+ },
96818
+ {
96819
+ id: "glm-4.7",
96820
+ name: "GLM-4.7",
96821
+ contextWindow: 2e5,
96822
+ maxContextWindow: 2e5,
96823
+ maxTokens: 48e3,
96824
+ regions: ["cn"],
96825
+ credits: { cn: "0.23" },
96826
+ supportsImage: false,
96827
+ reasoningEfforts: [],
96828
+ defaultReasoningEffort: null,
96829
+ canDisableThinking: false,
96830
+ description: ""
96831
+ },
96832
+ {
96833
+ id: "glm-4.6",
96834
+ name: "GLM-4.6",
96835
+ contextWindow: 168e3,
96836
+ maxContextWindow: 168e3,
96837
+ maxTokens: 32e3,
96838
+ regions: ["cn"],
96839
+ credits: { cn: "0.23" },
96840
+ supportsImage: false,
96841
+ reasoningEfforts: [],
96842
+ defaultReasoningEffort: null,
96843
+ canDisableThinking: false,
96844
+ description: ""
96845
+ },
96846
+ {
96847
+ id: "glm-4.6v",
96848
+ name: "GLM-4.6V",
96849
+ contextWindow: 128e3,
96850
+ maxContextWindow: 128e3,
96851
+ maxTokens: 32e3,
96852
+ regions: ["cn"],
96853
+ credits: { cn: "0.11" },
96854
+ supportsImage: true,
96855
+ reasoningEfforts: [
96856
+ "low",
96857
+ "high",
96858
+ "max"
96859
+ ],
96860
+ defaultReasoningEffort: "high",
96861
+ canDisableThinking: false,
96862
+ description: ""
96863
+ },
96864
+ {
96865
+ id: "kimi-k3-1",
96866
+ name: "Kimi-K3",
96362
96867
  contextWindow: 1e6,
96363
96868
  maxContextWindow: 1e6,
96364
- maxTokens: 64e3,
96869
+ maxTokens: 32e3,
96365
96870
  regions: ["cn"],
96366
- credits: { cn: "0.00" },
96871
+ credits: { cn: "1.62" },
96367
96872
  supportsImage: true,
96368
- reasoningEfforts: ["high"],
96873
+ reasoningEfforts: [
96874
+ "low",
96875
+ "high",
96876
+ "max"
96877
+ ],
96369
96878
  defaultReasoningEffort: "high",
96370
96879
  canDisableThinking: false,
96371
- description: "混元思考模型,具有增强的推理能力"
96880
+ description: "擅长处理复杂的长程自主任务,前端开发能力突出,同时在知识工作与科研推理上表现出色。"
96372
96881
  },
96373
96882
  {
96374
- id: "hy3",
96375
- name: "Hy3",
96376
- contextWindow: 192e3,
96377
- maxContextWindow: 192e3,
96883
+ id: "kimi-k2.8-preview",
96884
+ name: "Kimi-K2.8-Preview",
96885
+ contextWindow: 1e6,
96886
+ maxContextWindow: 1e6,
96378
96887
  maxTokens: 64e3,
96379
- regions: ["intl", "cn"],
96380
- credits: {
96381
- cn: "0.00",
96382
- intl: "0.00"
96383
- },
96888
+ regions: ["cn"],
96889
+ credits: { cn: "0.77" },
96384
96890
  supportsImage: true,
96385
- reasoningEfforts: ["low", "high"],
96891
+ reasoningEfforts: [
96892
+ "low",
96893
+ "high",
96894
+ "max"
96895
+ ],
96896
+ defaultReasoningEffort: "high",
96897
+ canDisableThinking: true,
96898
+ description: "Moonshot AI · 擅长复杂长程自主任务、知识工作与科研推理,前端开发突出"
96899
+ },
96900
+ {
96901
+ id: "kimi-k2.5",
96902
+ name: "Kimi-K2.5",
96903
+ contextWindow: 164e3,
96904
+ maxContextWindow: 164e3,
96905
+ maxTokens: 32e3,
96906
+ regions: ["cn"],
96907
+ credits: { cn: "0.45" },
96908
+ supportsImage: true,
96909
+ reasoningEfforts: [
96910
+ "low",
96911
+ "high",
96912
+ "max"
96913
+ ],
96386
96914
  defaultReasoningEffort: "high",
96387
96915
  canDisableThinking: false,
96388
- description: "混元思考模型,具有增强的推理能力"
96916
+ description: ""
96917
+ },
96918
+ {
96919
+ id: "kimi-k2-thinking",
96920
+ name: "Kimi-K2-Thinking",
96921
+ contextWindow: 164e3,
96922
+ maxContextWindow: 164e3,
96923
+ maxTokens: 32e3,
96924
+ regions: ["cn"],
96925
+ credits: { cn: "0.54" },
96926
+ supportsImage: false,
96927
+ reasoningEfforts: [
96928
+ "low",
96929
+ "high",
96930
+ "max"
96931
+ ],
96932
+ defaultReasoningEffort: "high",
96933
+ canDisableThinking: false,
96934
+ description: ""
96389
96935
  },
96390
96936
  {
96391
96937
  id: "hy3-x",
@@ -96405,6 +96951,20 @@ const FALLBACK_MODELS$1 = [
96405
96951
  canDisableThinking: false,
96406
96952
  description: "混元思考模型,具有增强的推理能力"
96407
96953
  },
96954
+ {
96955
+ id: "hy4-preview-x",
96956
+ name: "Hy4 preview",
96957
+ contextWindow: 1e6,
96958
+ maxContextWindow: 1e6,
96959
+ maxTokens: 64e3,
96960
+ regions: ["cn"],
96961
+ credits: { cn: "0.29" },
96962
+ supportsImage: true,
96963
+ reasoningEfforts: ["high"],
96964
+ defaultReasoningEffort: "high",
96965
+ canDisableThinking: false,
96966
+ description: "混元思考模型,具有增强的推理能力"
96967
+ },
96408
96968
  {
96409
96969
  id: "hunyuan-chat",
96410
96970
  name: "Hunyuan-Turbos",
@@ -96440,77 +97000,68 @@ const FALLBACK_MODELS$1 = [
96440
97000
  description: "推理速度极快,编码能力强劲,并支持原生多模态输入的匿名大模型"
96441
97001
  },
96442
97002
  {
96443
- id: "minimax-m3",
96444
- name: "MiniMax-M3",
96445
- contextWindow: 512e3,
96446
- maxContextWindow: 512e3,
97003
+ id: "step-5-preview",
97004
+ name: "Step-5-Preview",
97005
+ contextWindow: 1e6,
97006
+ maxContextWindow: 1e6,
96447
97007
  maxTokens: 64e3,
96448
97008
  regions: ["cn"],
96449
- credits: { cn: "0.25" },
97009
+ credits: { cn: "0.33" },
96450
97010
  supportsImage: true,
96451
97011
  reasoningEfforts: [
96452
97012
  "low",
96453
- "high",
96454
- "max"
97013
+ "medium",
97014
+ "high"
96455
97015
  ],
96456
- defaultReasoningEffort: "medium",
97016
+ defaultReasoningEffort: "high",
96457
97017
  canDisableThinking: false,
96458
- description: "原生多模态,擅长代码、智能体任务"
97018
+ description: "阶跃 StepFun 面向真实任务的新一代旗舰基模"
96459
97019
  },
96460
97020
  {
96461
- id: "minimax-m2.7",
96462
- name: "MiniMax-M2.7",
96463
- contextWindow: 2e5,
96464
- maxContextWindow: 2e5,
96465
- maxTokens: 48e3,
97021
+ id: "mimo-v2.6-pro",
97022
+ name: "Xiaomi MiMo-V2.6-Pro",
97023
+ contextWindow: 1e6,
97024
+ maxContextWindow: 1e6,
97025
+ maxTokens: 128e3,
96466
97026
  regions: ["cn"],
96467
- credits: { cn: "0.19" },
97027
+ credits: { cn: "0.13" },
96468
97028
  supportsImage: true,
96469
97029
  reasoningEfforts: [
96470
97030
  "low",
96471
97031
  "high",
96472
97032
  "max"
96473
97033
  ],
96474
- defaultReasoningEffort: "medium",
97034
+ defaultReasoningEffort: "high",
96475
97035
  canDisableThinking: false,
96476
- description: ""
97036
+ description: "Xiaomi · 旗舰推理模型,面向专业、复杂长程工作流,兼顾高阶科研与网络安全"
96477
97037
  },
96478
97038
  {
96479
- id: "minimax-m2.5",
96480
- name: "MiniMax-M2.5",
96481
- contextWindow: 2e5,
96482
- maxContextWindow: 2e5,
96483
- maxTokens: 48e3,
97039
+ id: "hy4free",
97040
+ name: "Hy4 Free",
97041
+ contextWindow: 1e6,
97042
+ maxContextWindow: 1e6,
97043
+ maxTokens: 64e3,
96484
97044
  regions: ["cn"],
96485
- credits: { cn: "0.18" },
96486
- supportsImage: false,
96487
- reasoningEfforts: [
96488
- "low",
96489
- "high",
96490
- "max"
96491
- ],
97045
+ credits: { cn: "0.00" },
97046
+ supportsImage: true,
97047
+ reasoningEfforts: ["high"],
96492
97048
  defaultReasoningEffort: "high",
96493
97049
  canDisableThinking: false,
96494
- description: ""
97050
+ description: "最新旗舰模型,Agent与多模专长"
96495
97051
  },
96496
- ...UNPUBLISHED_MODELS.filter((model) => model.id === "gemini-3.8-flash"),
96497
97052
  {
96498
- id: "gemini-3.5-flash",
96499
- name: "Gemini-3.5-Flash",
97053
+ id: "hy4std",
97054
+ name: "Hy4",
96500
97055
  contextWindow: 1e6,
96501
97056
  maxContextWindow: 1e6,
96502
- maxTokens: 65536,
96503
- regions: ["intl"],
96504
- credits: { intl: "0.99" },
97057
+ maxTokens: 64e3,
97058
+ regions: ["cn"],
97059
+ credits: { cn: "0.29" },
96505
97060
  supportsImage: true,
96506
- reasoningEfforts: [
96507
- "low",
96508
- "high",
96509
- "max"
96510
- ],
96511
- defaultReasoningEffort: "medium",
97061
+ reasoningEfforts: ["high"],
97062
+ defaultReasoningEffort: "high",
96512
97063
  canDisableThinking: false,
96513
- description: "能力均衡,适合日常使用"
97064
+ description: "最新旗舰模型,Agent与多模专长"
96514
97065
  }
96515
97066
  ];
96516
97067
  /**
@@ -96522,11 +97073,41 @@ const FALLBACK_MODELS$1 = [
96522
97073
  * the account's region can actually call.
96523
97074
  */
96524
97075
  const DEFAULT_VISIBLE_MODEL_IDS$1 = [
97076
+ "glm-5.3",
97077
+ "deepseek-v4.1-flash",
97078
+ "hy4std",
97079
+ "kimi-k3.1-pro"
97080
+ ];
97081
+ /**
97082
+ * Default selections this table used to ship.
97083
+ *
97084
+ * "Never edited" is recognised by matching the CURRENT shipped set, so replacing
97085
+ * the set outright would turn every existing install into what looks like a user
97086
+ * who hand-picked a short list — and the picker would quietly narrow to it,
97087
+ * because the 2026-11-04 refresh dropped two of the retired ids outright
97088
+ * (`kimi-k2.6` is gone from both regions, `hy4-preview` is intl-only now).
97089
+ * Keeping the retired sets here is what lets the default move without that.
97090
+ */
97091
+ const RETIRED_DEFAULT_SELECTIONS = [[
96525
97092
  "glm-5.3",
96526
97093
  "deepseek-v4.1-flash",
96527
97094
  "hy4-preview",
96528
97095
  "kimi-k2.6"
96529
- ];
97096
+ ]];
97097
+ /**
97098
+ * Whether a stored selection is still a shipped default, i.e. the user never
97099
+ * touched the picker.
97100
+ *
97101
+ * Same size and same members, not order-sensitive: the card writes the list in
97102
+ * catalog order, which one default can spell two ways.
97103
+ */
97104
+ function isUntouchedModelSelection(stored) {
97105
+ if (stored.length === 0) return false;
97106
+ return [DEFAULT_VISIBLE_MODEL_IDS$1, ...RETIRED_DEFAULT_SELECTIONS].some((shipped) => {
97107
+ const known = new Set(shipped);
97108
+ return stored.length === known.size && stored.every((id) => known.has(id));
97109
+ });
97110
+ }
96530
97111
  /** Every id the fallback knows, in declaration order. */
96531
97112
  const WORKBUDDY_MODEL_IDS = FALLBACK_MODELS$1.map((model) => model.id);
96532
97113
  /**
@@ -96631,6 +97212,18 @@ const WORKBUDDY_VENDOR_RULES = [
96631
97212
  re: /^minimax-/,
96632
97213
  family: "minimax"
96633
97214
  },
97215
+ {
97216
+ re: /^mimo-/,
97217
+ family: "xiaomi"
97218
+ },
97219
+ {
97220
+ re: /^step-/,
97221
+ family: "stepfun"
97222
+ },
97223
+ {
97224
+ re: /^grok-/,
97225
+ family: "xai"
97226
+ },
96634
97227
  {
96635
97228
  re: /^space-bunny$/,
96636
97229
  family: "space-bunny"
@@ -100552,8 +101145,7 @@ function isPoolPausedError(error) {
100552
101145
  */
100553
101146
  function resolveEnabledModelIds$2(stored, available, enabled = true) {
100554
101147
  if (!enabled) return [];
100555
- const shippedDefaults = new Set(DEFAULT_VISIBLE_MODEL_IDS$1);
100556
- if (stored.length > 0 && stored.length === shippedDefaults.size && stored.every((id) => shippedDefaults.has(id))) return [...available];
101148
+ if (isUntouchedModelSelection(stored)) return [...available];
100557
101149
  const known = new Set(available);
100558
101150
  return stored.filter((id) => known.has(id));
100559
101151
  }
@@ -102220,8 +102812,7 @@ async function readRequestJson(request) {
102220
102812
  */
102221
102813
  function resolveEnabledModelIds$1(stored, available, enabled = true) {
102222
102814
  if (!enabled) return [];
102223
- const shippedDefaults = new Set(DEFAULT_VISIBLE_MODEL_IDS$1);
102224
- if (stored.length > 0 && stored.length === shippedDefaults.size && stored.every((id) => shippedDefaults.has(id))) return [...available];
102815
+ if (isUntouchedModelSelection(stored)) return [...available];
102225
102816
  const known = new Set(available);
102226
102817
  return stored.filter((id) => known.has(id));
102227
102818
  }