dsh-connect-trae 3.7.0 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -129,9 +129,16 @@ function traeInputModalities(model) {
129
129
  * (wire resolution, callable-key filtering) must keep matching the
130
130
  * undecorated name; only the model rows handed to DSH (adapter catalog and
131
131
  * model discovery) use this decorated name.
132
+ *
133
+ * `withRate` is false for the ENTERPRISE panel. The multiplier is a
134
+ * consumer-pool figure — personal credits per token, read from the consumer
135
+ * directory — and the tenant pool does not bill against it, so printing it in
136
+ * the model picker would price a model the tenant is not charged for. The rate
137
+ * is part of the NAME that reaches DSH, which is why this is a parameter here
138
+ * and not a rendering decision downstream.
132
139
  */
133
- function traeModelDisplayName(model) {
134
- return model.creditMultiplier === void 0 ? model.name : `${model.name} · x${model.creditMultiplier.toFixed(2)}`;
140
+ function traeModelDisplayName(model, withRate = true) {
141
+ return !withRate || model.creditMultiplier === void 0 ? model.name : `${model.name} · x${model.creditMultiplier.toFixed(2)}`;
135
142
  }
136
143
  /**
137
144
  * Whether one model should be offered image input.
@@ -499,24 +506,61 @@ function readManifest(directory) {
499
506
  /** Provider route this bundle owns for the domestic (CN) gateway. */
500
507
  const TRAE_PROVIDER = "trae";
501
508
  /**
509
+ * Provider route this bundle owns for the CN **enterprise (tenant)** gateway.
510
+ *
511
+ * A separate provider id, not a mode switch, because the two pools bill
512
+ * differently: a tenant account (`tob_online`) is served by BOTH gateways with
513
+ * the SAME credential, and only the enterprise one draws on the enterprise
514
+ * pool. Exposing one id would force a per-call guess about who pays.
515
+ *
516
+ * It never falls back to the consumer pool: picking this provider is the user
517
+ * saying "bill the tenant", so a refusal is reported as an error instead of
518
+ * being silently re-run against the personal quota.
519
+ */
520
+ const TRAE_ENTERPRISE_PROVIDER = "trae-enterprise";
521
+ /**
502
522
  * Provider route this bundle owns for the international (trae.ai) gateway.
503
523
  * Named `-global` to match the plugin family's convention (`workbuddy-global`);
504
524
  * the internal region bucket stays `ai`, which is the gateway/protocol name.
505
525
  */
506
526
  const TRAE_AI_PROVIDER = "trae-global";
507
- /** The provider id each region registers as. */
527
+ /** The provider ids one region publishes, in model-picker order. */
528
+ const TRAE_PROVIDER_IDS = {
529
+ cn: [TRAE_PROVIDER, TRAE_ENTERPRISE_PROVIDER],
530
+ ai: [TRAE_AI_PROVIDER]
531
+ };
532
+ /** Each region's default (first) provider id. */
508
533
  const TRAE_PROVIDERS = {
509
534
  cn: TRAE_PROVIDER,
510
535
  ai: TRAE_AI_PROVIDER
511
536
  };
537
+ const TRAE_PROVIDER_ROUTES = {
538
+ [TRAE_PROVIDER]: "solo",
539
+ [TRAE_ENTERPRISE_PROVIDER]: "enterprise",
540
+ [TRAE_AI_PROVIDER]: "solo"
541
+ };
542
+ /** Chat pool a provider id bills against; `undefined` for an unknown id. */
543
+ function routeOfTraeProvider(provider) {
544
+ return TRAE_PROVIDER_ROUTES[provider];
545
+ }
512
546
  /** Human-readable provider names, shown in the DSH model picker. */
513
547
  const TRAE_PROVIDER_DISPLAY_NAMES = {
514
548
  cn: "Trae",
515
549
  ai: "Trae Global"
516
550
  };
551
+ /** Display name per provider id (the region table above only names the default). */
552
+ const TRAE_PROVIDER_ID_DISPLAY_NAMES = {
553
+ [TRAE_PROVIDER]: "Trae",
554
+ [TRAE_ENTERPRISE_PROVIDER]: "Trae 企业版",
555
+ [TRAE_AI_PROVIDER]: "Trae Global"
556
+ };
557
+ /** Display name of a provider id. */
558
+ function displayNameOfTraeProvider(provider) {
559
+ return TRAE_PROVIDER_ID_DISPLAY_NAMES[provider] ?? provider;
560
+ }
517
561
  /** Region a provider route id belongs to. */
518
562
  function regionOfTraeProvider(provider) {
519
- for (const [region, id] of Object.entries(TRAE_PROVIDERS)) if (id === provider) return region;
563
+ for (const [region, ids] of Object.entries(TRAE_PROVIDER_IDS)) if (ids.includes(provider)) return region;
520
564
  }
521
565
  const TRAE_STREAM_IDLE_TIMEOUT_MS = 3e5;
522
566
  const INERT_AUTH = {
@@ -549,10 +593,10 @@ const REQUEST_IMAGE_BUDGETS = {
549
593
  * failing the entire provider route (`INVALID_MODEL_CONTEXT`).
550
594
  */
551
595
  const FALLBACK_CONTEXT_WINDOW = 2e5;
552
- function toPiModel(info, baseUrl, providerId) {
596
+ function toPiModel(info, baseUrl, providerId, withRate = true) {
553
597
  return {
554
598
  id: info.id,
555
- name: traeModelDisplayName(info),
599
+ name: traeModelDisplayName(info, withRate),
556
600
  api: "openai-completions",
557
601
  provider: providerId,
558
602
  baseUrl,
@@ -770,7 +814,12 @@ function createTraeAdapter(options) {
770
814
  const providerId = options.provider ?? "trae";
771
815
  const providerName = options.displayName ?? "Trae";
772
816
  const generation = options.piAiGeneration ?? piAiRuntimeInfo().generation;
773
- const buildModels = () => options.catalog.current().map((info) => toPiModel(info, `${options.shim.baseUrl()}/v1`, providerId));
817
+ /**
818
+ * The enterprise provider shows no rate: the multiplier belongs to the
819
+ * consumer pool, and this provider bills the tenant pool.
820
+ */
821
+ const showRate = options.provider !== TRAE_ENTERPRISE_PROVIDER;
822
+ const buildModels = () => options.catalog.current().map((info) => toPiModel(info, `${options.shim.baseUrl()}/v1`, providerId, showRate));
774
823
  const provider = {
775
824
  ...createProvider({
776
825
  id: providerId,
@@ -806,7 +855,7 @@ function createTraeAdapter(options) {
806
855
  adapter: new PiAiAdapter({
807
856
  profiles: () => profiles,
808
857
  auth: INERT_AUTH,
809
- resolveApiKey: async () => options.shim.token(),
858
+ resolveApiKey: async () => options.secret ?? options.shim.token(),
810
859
  ...options.resolveAttachments === void 0 ? {} : { resolveAttachments: options.resolveAttachments }
811
860
  }),
812
861
  invalidate() {
@@ -815,7 +864,113 @@ function createTraeAdapter(options) {
815
864
  };
816
865
  }
817
866
  //#endregion
867
+ //#region src/region.ts
868
+ /** Every panel, in card tab order. */
869
+ const TRAE_PANELS = [
870
+ "cn",
871
+ "enterprise",
872
+ "ai"
873
+ ];
874
+ /** The region whose credential/account pool a panel draws on. */
875
+ function regionOfPanel(panel) {
876
+ return panel === "ai" ? "ai" : "cn";
877
+ }
878
+ /** The provider id a panel publishes. */
879
+ function providerOfPanel(panel) {
880
+ if (panel === "enterprise") return "trae-enterprise";
881
+ return panel === "ai" ? "trae-global" : "trae";
882
+ }
883
+ /** The panel that owns a provider id, or undefined for an unknown id. */
884
+ function panelOfProvider(provider) {
885
+ return TRAE_PANELS.find((panel) => providerOfPanel(panel) === provider);
886
+ }
887
+ /** Narrow a stored settings key to a panel. */
888
+ function panelOfKey(value) {
889
+ return TRAE_PANELS.includes(value) ? value : void 0;
890
+ }
891
+ /**
892
+ * Verified gateway bases per region (docs/INTL_SG_EVIDENCE.md §2).
893
+ * The AI gateway is shared by both international installs (Trae desktop and
894
+ * TRAE SOLO); its mchost shards (`api16/api22-normal-alisg.mchost.guru`)
895
+ * answer the same bytes but stay internal — `coresg-normal.trae.ai` is the
896
+ * single stable entry point.
897
+ */
898
+ const REGION_GATEWAYS = {
899
+ cn: {
900
+ chat: "https://trae-api-cn.mchost.guru",
901
+ remote: "https://solo.trae.cn/api/remote/v1",
902
+ pay: "https://api.trae.cn"
903
+ },
904
+ ai: {
905
+ chat: "https://coresg-normal.trae.ai",
906
+ remote: "https://coresg-normal.trae.ai/api/remote/v1",
907
+ pay: "https://growsg-normal.trae.ai"
908
+ }
909
+ };
910
+ /**
911
+ * Enterprise (tenant) chat gateway for a CN credential that belongs to a
912
+ * `tob_online` tenant.
913
+ *
914
+ * This is a SECOND chat gateway on the same credential, not a second region:
915
+ * the roster, the credential and the protocol are the same as `cn`, but a
916
+ * tenant account is billed against the enterprise pool when the request goes
917
+ * here. Measured 2026-10-17 on a live `Trae CN` tenant account: the same model
918
+ * served from this host does not move the personal `consumed_amount`, while
919
+ * the same call to `trae-api-cn.mchost.guru` charges it.
920
+ *
921
+ * Two wire differences, both load-bearing:
922
+ * - the request must carry a per-call `session_id` (the consumer gateway
923
+ * rejects it with `4001`), and
924
+ * - a refused call answers **HTTP 200** whose FIRST SSE frame is
925
+ * `event:error` with `code: 4001` — not an HTTP error status, so the
926
+ * refusal is only visible if the response body is inspected.
927
+ */
928
+ const TRAE_ENTERPRISE_HOST = "https://console.enterprise.trae.cn";
929
+ /** Region for an edition label: the international installs belong to `ai`. */
930
+ function regionOfEdition(edition) {
931
+ return edition === "sg" || edition === "solo-sg" ? "ai" : "cn";
932
+ }
933
+ /**
934
+ * Region from the credential's `userRegion` claim. The desktop storage spells
935
+ * it as an object (`{"region":"CN","_aiRegion":"CN"}`); the app logs also
936
+ * spell the bare value lowercase (`"sg"`). Both are accepted, case-blind.
937
+ */
938
+ function regionOfUserRegion(value) {
939
+ const raw = typeof value === "object" && value !== null && !Array.isArray(value) ? value["region"] : value;
940
+ if (typeof raw !== "string") return void 0;
941
+ const lowered = raw.trim().toLowerCase();
942
+ if (lowered === "cn") return "cn";
943
+ if (lowered === "sg" || lowered === "ai") return "ai";
944
+ }
945
+ /** Region from a credential host (`.trae.ai` → ai, `.trae.cn`/`.trae.com.cn` → cn). */
946
+ function regionOfHost(host) {
947
+ if (host === void 0) return void 0;
948
+ const trimmed = host.trim();
949
+ if (trimmed === "") return void 0;
950
+ let hostname;
951
+ try {
952
+ hostname = new URL(trimmed.startsWith("http") ? trimmed : `https://${trimmed}`).hostname;
953
+ } catch {
954
+ return;
955
+ }
956
+ if (hostname === "trae.ai" || hostname.endsWith(".trae.ai")) return "ai";
957
+ if (hostname === "trae.cn" || hostname.endsWith(".trae.cn") || hostname.endsWith(".trae.com.cn")) return "cn";
958
+ }
959
+ /**
960
+ * Region of a credential: the `userRegion` claim wins, the host suffix is the
961
+ * fallback, and the edition label is the last resort. Every level is derived
962
+ * from data the credential itself carries, so no user configuration is needed.
963
+ */
964
+ function regionOfCredential(credential) {
965
+ return regionOfUserRegion(credential.userRegion) ?? regionOfHost(credential.host) ?? regionOfEdition(credential.edition);
966
+ }
967
+ //#endregion
818
968
  //#region src/status-paths.ts
969
+ /**
970
+ * Node-free constants and types shared by the Host and browser halves.
971
+ * Mirrors the `dsh-workbuddy-connect` status-route pattern so the plugin card
972
+ * stays consistent with that project's external presentation.
973
+ */
819
974
  /** Plugin-owned usage endpoint consumed by its browser half. */
820
975
  const TRAE_USAGE_PATH = "/plugins/dsh-connect-trae/usage";
821
976
  /** Plugin-owned live model refresh endpoint. */
@@ -882,8 +1037,8 @@ const TRAE_REGIONS = ["cn", "ai"];
882
1037
  * Address one region's status route. The two regions are separate provider
883
1038
  * stacks; every card request carries the region whose tab the user is on.
884
1039
  */
885
- function withTraeRegion(path, region) {
886
- return `${path}?${TRAE_REGION_PARAM}=${region}`;
1040
+ function withTraeRegion(path, panel) {
1041
+ return `${path}?${TRAE_REGION_PARAM}=${panel}`;
887
1042
  }
888
1043
  /**
889
1044
  * Read the region parameter off a status-route URL. Absent means the domestic
@@ -894,7 +1049,7 @@ function regionOfTraeStatusUrl(url) {
894
1049
  const at = url.indexOf("?");
895
1050
  const value = at === -1 ? null : new URLSearchParams(url.slice(at + 1)).get(TRAE_REGION_PARAM);
896
1051
  if (value === null || value === "") return "cn";
897
- return TRAE_REGIONS.includes(value) ? value : void 0;
1052
+ return TRAE_PANELS.includes(value) ? value : void 0;
898
1053
  }
899
1054
  /**
900
1055
  * Peel ONE `{get(): T}` live reference, the shape DSH 0.1.7 delivers a
@@ -971,11 +1126,11 @@ function followAppLoginEnabled(configured) {
971
1126
  * region's picks. Tolerates any stored shape (absent, non-object) by starting
972
1127
  * from an empty document.
973
1128
  */
974
- function nextRegionSlots(regions, region, slot) {
1129
+ function nextRegionSlots(regions, panel, slot) {
975
1130
  const unwrapped = unwrapVolatileDeep(regions);
976
1131
  return {
977
1132
  ...typeof unwrapped === "object" && unwrapped !== null && !Array.isArray(unwrapped) ? unwrapped : {},
978
- [region]: slot
1133
+ [panel]: slot
979
1134
  };
980
1135
  }
981
1136
  /**
@@ -1000,8 +1155,8 @@ function regionsMapOf(value) {
1000
1155
  return record;
1001
1156
  }
1002
1157
  /** One region's stored slot as a plain object; any other shape reads as empty. */
1003
- function regionSlotOf(value, region) {
1004
- const slot = unwrapVolatile(regionsMapOf(value)[region]);
1158
+ function regionSlotOf(value, panel) {
1159
+ const slot = unwrapVolatile(regionsMapOf(value)[panel]);
1005
1160
  return typeof slot === "object" && slot !== null && !Array.isArray(slot) ? slot : {};
1006
1161
  }
1007
1162
  /**
@@ -1014,8 +1169,10 @@ function regionSlotOf(value, region) {
1014
1169
  * `value` may be the whole settings section or the `regions` map (see
1015
1170
  * {@link regionsMapOf}).
1016
1171
  */
1017
- function regionEnabledOf(value, region) {
1018
- return regionSlotOf(value, region)["enabled"] !== false;
1172
+ function regionEnabledOf(value, panel) {
1173
+ const stored = regionSlotOf(value, panel)["enabled"];
1174
+ if (typeof stored === "boolean") return stored;
1175
+ return panel !== "enterprise";
1019
1176
  }
1020
1177
  /**
1021
1178
  * Build the next `regions` settings value for a provider on/off toggle. ONLY
@@ -1033,9 +1190,9 @@ function regionEnabledOf(value, region) {
1033
1190
  * value is always the `regions` map, i.e. exactly what `settingsScope.set(
1034
1191
  * 'regions', ...)` needs.
1035
1192
  */
1036
- function nextRegionEnabled(value, region, enabled) {
1037
- return nextRegionSlots(regionsMapOf(value), region, {
1038
- ...regionSlotOf(value, region),
1193
+ function nextRegionEnabled(value, panel, enabled) {
1194
+ return nextRegionSlots(regionsMapOf(value), panel, {
1195
+ ...regionSlotOf(value, panel),
1039
1196
  enabled
1040
1197
  });
1041
1198
  }
@@ -1052,8 +1209,8 @@ function nextRegionEnabled(value, region, enabled) {
1052
1209
  * `value` may be the whole settings section or the `regions` map (see
1053
1210
  * {@link regionsMapOf}).
1054
1211
  */
1055
- function regionCreditPositionOf(value, region) {
1056
- return regionSlotOf(value, region)["creditPosition"] === "stats" ? "stats" : "composer";
1212
+ function regionCreditPositionOf(value, panel) {
1213
+ return regionSlotOf(value, panel)["creditPosition"] === "stats" ? "stats" : "composer";
1057
1214
  }
1058
1215
  /**
1059
1216
  * Build the next `regions` settings value for one region's credit seat.
@@ -1066,9 +1223,9 @@ function regionCreditPositionOf(value, region) {
1066
1223
  * value is always the `regions` map, i.e. exactly what `settingsScope.set(
1067
1224
  * 'regions', ...)` needs.
1068
1225
  */
1069
- function nextRegionCreditPosition(value, region, position) {
1070
- return nextRegionSlots(regionsMapOf(value), region, {
1071
- ...regionSlotOf(value, region),
1226
+ function nextRegionCreditPosition(value, panel, position) {
1227
+ return nextRegionSlots(regionsMapOf(value), panel, {
1228
+ ...regionSlotOf(value, panel),
1072
1229
  creditPosition: position
1073
1230
  });
1074
1231
  }
@@ -1564,65 +1721,6 @@ function traeCliCandidates(platform = process.platform, home = homedir(), env =
1564
1721
  return result;
1565
1722
  }
1566
1723
  //#endregion
1567
- //#region src/region.ts
1568
- /**
1569
- * Verified gateway bases per region (docs/INTL_SG_EVIDENCE.md §2).
1570
- * The AI gateway is shared by both international installs (Trae desktop and
1571
- * TRAE SOLO); its mchost shards (`api16/api22-normal-alisg.mchost.guru`)
1572
- * answer the same bytes but stay internal — `coresg-normal.trae.ai` is the
1573
- * single stable entry point.
1574
- */
1575
- const REGION_GATEWAYS = {
1576
- cn: {
1577
- chat: "https://trae-api-cn.mchost.guru",
1578
- remote: "https://solo.trae.cn/api/remote/v1",
1579
- pay: "https://api.trae.cn"
1580
- },
1581
- ai: {
1582
- chat: "https://coresg-normal.trae.ai",
1583
- remote: "https://coresg-normal.trae.ai/api/remote/v1",
1584
- pay: "https://growsg-normal.trae.ai"
1585
- }
1586
- };
1587
- /** Region for an edition label: the international installs belong to `ai`. */
1588
- function regionOfEdition(edition) {
1589
- return edition === "sg" || edition === "solo-sg" ? "ai" : "cn";
1590
- }
1591
- /**
1592
- * Region from the credential's `userRegion` claim. The desktop storage spells
1593
- * it as an object (`{"region":"CN","_aiRegion":"CN"}`); the app logs also
1594
- * spell the bare value lowercase (`"sg"`). Both are accepted, case-blind.
1595
- */
1596
- function regionOfUserRegion(value) {
1597
- const raw = typeof value === "object" && value !== null && !Array.isArray(value) ? value["region"] : value;
1598
- if (typeof raw !== "string") return void 0;
1599
- const lowered = raw.trim().toLowerCase();
1600
- if (lowered === "cn") return "cn";
1601
- if (lowered === "sg" || lowered === "ai") return "ai";
1602
- }
1603
- /** Region from a credential host (`.trae.ai` → ai, `.trae.cn`/`.trae.com.cn` → cn). */
1604
- function regionOfHost(host) {
1605
- if (host === void 0) return void 0;
1606
- const trimmed = host.trim();
1607
- if (trimmed === "") return void 0;
1608
- let hostname;
1609
- try {
1610
- hostname = new URL(trimmed.startsWith("http") ? trimmed : `https://${trimmed}`).hostname;
1611
- } catch {
1612
- return;
1613
- }
1614
- if (hostname === "trae.ai" || hostname.endsWith(".trae.ai")) return "ai";
1615
- if (hostname === "trae.cn" || hostname.endsWith(".trae.cn") || hostname.endsWith(".trae.com.cn")) return "cn";
1616
- }
1617
- /**
1618
- * Region of a credential: the `userRegion` claim wins, the host suffix is the
1619
- * fallback, and the edition label is the last resort. Every level is derived
1620
- * from data the credential itself carries, so no user configuration is needed.
1621
- */
1622
- function regionOfCredential(credential) {
1623
- return regionOfUserRegion(credential.userRegion) ?? regionOfHost(credential.host) ?? regionOfEdition(credential.edition);
1624
- }
1625
- //#endregion
1626
1724
  //#region src/auth.ts
1627
1725
  /** Host used for CLI tokens, which carry no host claim of their own. */
1628
1726
  const CLI_DEFAULT_HOST = "https://api.trae.cn";
@@ -1688,12 +1786,15 @@ function normalizeTraeCredential(raw, edition, source) {
1688
1786
  const refreshExpiresAtMs = timeToMs(value["refreshExpiredAt"] ?? value["refreshExpiresAt"]);
1689
1787
  const refreshToken = optionalString(value["refreshToken"]);
1690
1788
  const userRegion = userRegionOf(value["userRegion"]);
1691
- const accountName = optionalString((typeof value["account"] === "object" && value["account"] !== null && !Array.isArray(value["account"]) ? value["account"] : void 0)?.["username"]);
1789
+ const account = typeof value["account"] === "object" && value["account"] !== null && !Array.isArray(value["account"]) ? value["account"] : void 0;
1790
+ const accountName = optionalString(account?.["username"]);
1791
+ const tenantId = optionalString(account?.["tenant_id"]) ?? (typeof account?.["tenant_id"] === "number" ? String(account["tenant_id"]) : void 0);
1692
1792
  return {
1693
1793
  accessToken,
1694
1794
  ...refreshToken === void 0 ? {} : { refreshToken },
1695
1795
  userId: optionalString(value["userId"]) ?? "",
1696
1796
  ...accountName === void 0 ? {} : { accountName },
1797
+ ...tenantId === void 0 ? {} : { tenantId },
1697
1798
  host: optionalString(value["host"]) ?? "",
1698
1799
  ...userRegion === void 0 ? {} : { userRegion },
1699
1800
  expiresAtMs,
@@ -1702,6 +1803,18 @@ function normalizeTraeCredential(raw, edition, source) {
1702
1803
  source
1703
1804
  };
1704
1805
  }
1806
+ /**
1807
+ * Whether this account belongs to a Trae tenant, i.e. may bill the enterprise
1808
+ * pool.
1809
+ *
1810
+ * Evidence (2026-10-17, live): the tenant's own account carries
1811
+ * `account.tenant_id` / `tenant_name` and `scope: 'saas'`; the personal SOLO
1812
+ * account carries neither and reports `scope: 'marscode'`. Only the tenant one
1813
+ * streams on the enterprise gateway — the other answers `4019` risk control.
1814
+ */
1815
+ function isEnterpriseAccount(credential) {
1816
+ return credential?.tenantId !== void 0 && credential.tenantId.trim() !== "";
1817
+ }
1705
1818
  function traeAccountId(credential) {
1706
1819
  const stable = `${credential.edition}\0${credential.userId || credential.accountName || "unknown"}`;
1707
1820
  return createHash("sha256").update(stable).digest("hex").slice(0, 24);
@@ -1718,7 +1831,10 @@ function credentialOfStored(stored) {
1718
1831
  userId: record["userId"],
1719
1832
  host: record["host"],
1720
1833
  userRegion: record["userRegion"],
1721
- account: record["accountName"] === void 0 ? void 0 : { username: record["accountName"] },
1834
+ account: record["accountName"] === void 0 && record["tenantId"] === void 0 ? void 0 : {
1835
+ ...record["accountName"] === void 0 ? {} : { username: record["accountName"] },
1836
+ ...record["tenantId"] === void 0 ? {} : { tenant_id: record["tenantId"] }
1837
+ },
1722
1838
  expiredAt: record["expiresAtMs"],
1723
1839
  refreshExpiredAt: record["refreshExpiredAtMs"]
1724
1840
  }, edition, "dsh");
@@ -1773,6 +1889,8 @@ var TraeCredentialStore = class {
1773
1889
  edition;
1774
1890
  accountId;
1775
1891
  region;
1892
+ requireTenant;
1893
+ hideTenantAccounts;
1776
1894
  ownPathExplicit;
1777
1895
  legacyOwnPath;
1778
1896
  legacyOwnPathExplicit;
@@ -1784,6 +1902,8 @@ var TraeCredentialStore = class {
1784
1902
  this.edition = options.edition ?? "auto";
1785
1903
  this.accountId = options.accountId;
1786
1904
  this.region = options.region;
1905
+ this.requireTenant = options.requireTenant === true;
1906
+ this.hideTenantAccounts = options.hideTenantAccounts === true;
1787
1907
  this.ownPathExplicit = options.ownPath;
1788
1908
  this.legacyOwnPath = options.legacyOwnPath ?? legacyTraeOwnAuthPath();
1789
1909
  this.legacyOwnPathExplicit = options.legacyOwnPath;
@@ -1867,7 +1987,9 @@ var TraeCredentialStore = class {
1867
1987
  * the plugin performed is never discarded by this preference.
1868
1988
  */
1869
1989
  preferred(credentials) {
1870
- return credentials.find((credential) => credential.source === "desktop") ?? credentials[0];
1990
+ const eligible = this.requireTenant ? credentials.filter((credential) => isEnterpriseAccount(credential)) : credentials;
1991
+ if (eligible.length === 0) return void 0;
1992
+ return eligible.find((credential) => credential.source === "desktop") ?? eligible[0];
1871
1993
  }
1872
1994
  /**
1873
1995
  * The account the Trae app is signed in to right now, when that differs from
@@ -1891,6 +2013,7 @@ var TraeCredentialStore = class {
1891
2013
  id: liveId,
1892
2014
  accountName: live.accountName ?? (live.userId || `${live.edition} account`),
1893
2015
  edition: live.edition,
2016
+ enterprise: isEnterpriseAccount(live),
1894
2017
  region: regionOfCredential(live),
1895
2018
  source: live.source,
1896
2019
  tokenExpiresAtMs: live.expiresAtMs,
@@ -1898,25 +2021,94 @@ var TraeCredentialStore = class {
1898
2021
  selected: false
1899
2022
  };
1900
2023
  }
2024
+ /**
2025
+ * The accounts this store is willing to OFFER.
2026
+ *
2027
+ * Scoped to who this store may bill:
2028
+ * - a consumer store withholds tenant accounts (the enterprise pool is a
2029
+ * separate tab and a separate billing decision), and
2030
+ * - a tenant store withholds personal accounts (they cannot bill that
2031
+ * pool; the tenant gateway answers their chat with 4019).
2032
+ *
2033
+ * Either filter backs off when it would hide EVERYTHING, so a machine whose
2034
+ * only login is on the other side keeps working instead of reading as
2035
+ * "signed out".
2036
+ */
2037
+ offered(discovered) {
2038
+ const eligible = this.requireTenant ? discovered.filter((credential) => isEnterpriseAccount(credential)) : this.hideTenantAccounts ? discovered.filter((credential) => !isEnterpriseAccount(credential)) : discovered;
2039
+ return eligible.length > 0 ? eligible : discovered;
2040
+ }
1901
2041
  async accounts() {
1902
- const credentials = await this.readAll();
1903
- const selectedExists = this.accountId !== void 0 && credentials.some((credential) => traeAccountId(credential) === this.accountId);
1904
- const defaultSelected = this.preferred(credentials);
2042
+ const discovered = await this.readAll();
2043
+ const offered = this.offered(discovered);
2044
+ const inUse = this.inUse(discovered, offered);
2045
+ /**
2046
+ * Never hide the account we are actually billing.
2047
+ *
2048
+ * An explicit binding outranks the display filter (`inUse` honours it even
2049
+ * when the offered list withholds it), and a list whose active row is
2050
+ * invisible is worse than no list: the card's "save the selected account"
2051
+ * press reads `selected`, so a hidden binding would be rewritten to whichever
2052
+ * account merely happened to be DISPLAYED — silently moving the user's
2053
+ * billing to an account they never chose. Re-appending it costs one extra
2054
+ * row and makes the card tell the truth.
2055
+ */
2056
+ const credentials = inUse !== void 0 && !offered.includes(inUse) ? [...offered, inUse] : offered;
2057
+ const inUseId = inUse === void 0 ? void 0 : traeAccountId(inUse);
1905
2058
  return credentials.map((credential) => ({
1906
2059
  id: traeAccountId(credential),
1907
2060
  accountName: credential.accountName ?? (credential.userId || `${credential.edition} account`),
1908
2061
  edition: credential.edition,
2062
+ enterprise: isEnterpriseAccount(credential),
1909
2063
  region: regionOfCredential(credential),
1910
2064
  source: credential.source,
1911
2065
  tokenExpiresAtMs: credential.expiresAtMs,
1912
2066
  ...credential.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs: credential.refreshExpiresAtMs },
1913
- selected: selectedExists ? traeAccountId(credential) === this.accountId : credential === defaultSelected
2067
+ selected: inUseId !== void 0 && traeAccountId(credential) === inUseId
1914
2068
  }));
1915
2069
  }
2070
+ /**
2071
+ * The credential this store bills, given every credential it can see.
2072
+ *
2073
+ * This is the ONE resolution rule, and it exists so that `current()` and
2074
+ * `accounts()` cannot disagree: `current()` applies it to `readAll()`, and
2075
+ * `accounts()` applies the SAME verdict to decide which row reads as
2076
+ * `selected`. Deriving that flag independently is exactly how the two drifted
2077
+ * apart — and a card that marks a different account than the one the chat
2078
+ * bills is worse than no card at all, because a press that "saves the
2079
+ * selected account" then rewrites the binding to the account that was only
2080
+ * ever DISPLAYED.
2081
+ *
2082
+ * Order of business:
2083
+ * 1. No binding — the detected default (`preferred`) is in use, chosen from
2084
+ * the OFFERED list rather than from every login on the machine. A
2085
+ * consumer store that withholds tenant accounts must not default its
2086
+ * billing to the tenant pool merely because that login sorts first;
2087
+ * withholding is a billing decision, not a display detail.
2088
+ * 2. A binding — honour it verbatim, even when the OFFERED list hides that
2089
+ * account. Resolution is never scoped by `hideTenantAccounts`: the user's
2090
+ * explicit choice outranks a display filter.
2091
+ * 3. A tenant-scoped store cannot bill through a personal account, and the
2092
+ * tenant gateway only reveals that at the first chat (`4019`). Honour the
2093
+ * binding when it is eligible; otherwise fall back to the preferred
2094
+ * tenant account, which is the one the user's enterprise tab is meant to
2095
+ * use.
2096
+ * 4. A saved account can disappear when Trae replaces its local login. Do
2097
+ * NOT silently fall back to a different account: that would bill a
2098
+ * different account than the one the user explicitly selected. Return
2099
+ * undefined so the caller surfaces "no signed-in account" and the user
2100
+ * can re-select, instead of the plugin quietly switching accounts behind
2101
+ * their back.
2102
+ */
2103
+ inUse(discovered, offered) {
2104
+ if (this.accountId === void 0) return this.preferred(offered);
2105
+ const selected = discovered.find((credential) => traeAccountId(credential) === this.accountId);
2106
+ if (this.requireTenant && !isEnterpriseAccount(selected)) return this.preferred(offered);
2107
+ return selected;
2108
+ }
1916
2109
  async current() {
1917
- const credentials = await this.readAll();
1918
- if (this.accountId === void 0) return this.preferred(credentials);
1919
- return credentials.find((credential) => traeAccountId(credential) === this.accountId);
2110
+ const discovered = await this.readAll();
2111
+ return this.inUse(discovered, this.offered(discovered));
1920
2112
  }
1921
2113
  /**
1922
2114
  * The stored credential of one specific account, without changing which
@@ -2664,7 +2856,19 @@ function readBody(req) {
2664
2856
  });
2665
2857
  }
2666
2858
  function createTraeShim(options) {
2667
- const secret = randomBytes(32).toString("base64url");
2859
+ /**
2860
+ * Resolve the route table. The single-client form becomes a one-entry table
2861
+ * labelled `solo`, so existing call sites and tests keep one code path.
2862
+ */
2863
+ const routes = options.routes !== void 0 ? options.routes.map((route) => ({
2864
+ ...route,
2865
+ secret: randomBytes(32).toString("base64url")
2866
+ })) : [{
2867
+ label: "solo",
2868
+ client: options.client,
2869
+ secret: randomBytes(32).toString("base64url")
2870
+ }];
2871
+ const secret = routes[0]?.secret ?? randomBytes(32).toString("base64url");
2668
2872
  const sockets = /* @__PURE__ */ new Set();
2669
2873
  const server = createServer((req, res) => {
2670
2874
  handle(req, res);
@@ -2678,18 +2882,22 @@ function createTraeShim(options) {
2678
2882
  server.once("error", reject);
2679
2883
  });
2680
2884
  server.listen(0, "127.0.0.1");
2681
- function bearerOk(req) {
2885
+ /** Route whose secret matches the request's bearer, or `undefined`. */
2886
+ function routeOf(req) {
2682
2887
  const match = typeof req.headers.authorization === "string" ? /^Bearer\s+(.+)$/i.exec(req.headers.authorization.trim()) : null;
2683
- if (match === null) return false;
2888
+ if (match === null) return void 0;
2684
2889
  const actual = Buffer.from(match[1] ?? "");
2685
- const expected = Buffer.from(secret);
2686
- return actual.length === expected.length && timingSafeEqual(actual, expected);
2890
+ for (const route of routes) {
2891
+ const expected = Buffer.from(route.secret);
2892
+ if (actual.length === expected.length && timingSafeEqual(actual, expected)) return route;
2893
+ }
2687
2894
  }
2688
2895
  async function handle(req, res) {
2689
2896
  try {
2690
2897
  if (!hostIsLoopback(req.headers.host)) return writeError(res, 403, "host_not_allowed", "Host must be loopback");
2691
2898
  if (!originIsLoopback(req.headers.origin)) return writeError(res, 403, "origin_not_allowed", "Origin must be loopback");
2692
- if (!bearerOk(req)) return writeError(res, 401, "unauthorized", "Missing or invalid bearer");
2899
+ const route = routeOf(req);
2900
+ if (route === void 0) return writeError(res, 401, "unauthorized", "Missing or invalid bearer");
2693
2901
  const url = req.url ?? "/";
2694
2902
  if (req.method === "GET" && (url === "/healthz" || url === "/healthz/")) return writeJson(res, 200, { ok: true });
2695
2903
  if (req.method === "GET" && (url === "/v1/models" || url === "/v1/models/")) return writeJson(res, 200, {
@@ -2723,7 +2931,7 @@ function createTraeShim(options) {
2723
2931
  const abort = () => controller.abort();
2724
2932
  req.once("aborted", abort);
2725
2933
  req.socket.once("close", abort);
2726
- const result = await options.client.chatStream(raw, controller.signal);
2934
+ const result = await route.client.chatStream(raw, controller.signal);
2727
2935
  if (!result.ok) return writeError(res, STATUS_BY_KIND[result.kind], result.kind, result.message);
2728
2936
  res.writeHead(200, {
2729
2937
  "Content-Type": "text/event-stream",
@@ -2759,6 +2967,12 @@ function createTraeShim(options) {
2759
2967
  return `http://127.0.0.1:${address.port}`;
2760
2968
  },
2761
2969
  token: () => secret,
2970
+ tokenOf(label) {
2971
+ if (label === void 0) return secret;
2972
+ const route = routes.find((entry) => entry.label === label);
2973
+ if (route === void 0) throw new Error(`trae shim has no route named ${label}`);
2974
+ return route.secret;
2975
+ },
2762
2976
  close: () => new Promise((resolve, reject) => {
2763
2977
  for (const socket of sockets) socket.destroy();
2764
2978
  server.close((error) => error === void 0 ? resolve() : reject(error));
@@ -3107,7 +3321,7 @@ var TraeSoloUpstreamClient = class {
3107
3321
  async fetchModels(signal) {
3108
3322
  const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
3109
3323
  const region = regionOfCredential(credential);
3110
- const base = this.options.baseUrl ?? REGION_GATEWAYS[region].chat;
3324
+ const base = this.options.baseUrl ?? this.chatBaseOf(credential);
3111
3325
  const headers = {
3112
3326
  ...buildTraeCnHeaders(credential, identity),
3113
3327
  Accept: "application/json"
@@ -3193,7 +3407,19 @@ var TraeSoloUpstreamClient = class {
3193
3407
  }
3194
3408
  const [credential, identity] = await Promise.all([this.options.credential(), this.options.identity()]);
3195
3409
  const headers = buildTraeCnHeaders(credential, identity);
3196
- const base = this.options.baseUrl ?? REGION_GATEWAYS[regionOfCredential(credential)].chat;
3410
+ const base = this.options.baseUrl ?? this.chatBaseOf(credential);
3411
+ if (this.options.route === "enterprise") try {
3412
+ const parsed = JSON.parse(prepared);
3413
+ if (parsed["session_id"] === void 0) parsed["session_id"] = randomUUID();
3414
+ prepared = JSON.stringify(parsed);
3415
+ } catch {
3416
+ return {
3417
+ ok: false,
3418
+ status: 400,
3419
+ kind: "client",
3420
+ message: "invalid JSON request"
3421
+ };
3422
+ }
3197
3423
  let response;
3198
3424
  try {
3199
3425
  response = await this.fetchImpl(traeEndpoint(base, TRAE_SOLO_CHAT_PATH), {
@@ -3210,10 +3436,36 @@ var TraeSoloUpstreamClient = class {
3210
3436
  message: `transport error: ${String(error)}`
3211
3437
  };
3212
3438
  }
3213
- if (response.ok) return {
3214
- ok: true,
3215
- response
3216
- };
3439
+ if (response.ok) {
3440
+ /**
3441
+ * The enterprise gateway reports a refusal as **HTTP 200** whose first
3442
+ * SSE frame is `event:error`. Handing that stream back as a success would
3443
+ * surface the refusal as an empty assistant turn, so it is converted to
3444
+ * a real error here — the same shape the consumer gateway uses for its
3445
+ * non-2xx failures.
3446
+ */
3447
+ if (this.options.route !== "enterprise") return {
3448
+ ok: true,
3449
+ response
3450
+ };
3451
+ const classified = await classifyEnterpriseResponse(response);
3452
+ if ("response" in classified) return {
3453
+ ok: true,
3454
+ response: classified.response
3455
+ };
3456
+ this.options.log?.("dsh-connect-trae: enterprise llm_utils_chat refused", {
3457
+ status: 200,
3458
+ model: JSON.parse(prepared)["model"],
3459
+ configName: JSON.parse(prepared)["config_name"],
3460
+ body: classified.refusal
3461
+ });
3462
+ return {
3463
+ ok: false,
3464
+ status: 200,
3465
+ kind: "client",
3466
+ message: classified.refusal
3467
+ };
3468
+ }
3217
3469
  const text = (await response.text()).slice(0, 1024);
3218
3470
  this.options.log?.("dsh-connect-trae: llm_utils_chat rejected", {
3219
3471
  status: response.status,
@@ -3229,7 +3481,77 @@ var TraeSoloUpstreamClient = class {
3229
3481
  message: text || `Trae SOLO returned HTTP ${response.status}`
3230
3482
  };
3231
3483
  }
3484
+ /** Chat gateway for this client's pinned route. */
3485
+ chatBaseOf(credential) {
3486
+ if (this.options.route === "enterprise") return TRAE_ENTERPRISE_HOST;
3487
+ return REGION_GATEWAYS[regionOfCredential(credential)].chat;
3488
+ }
3232
3489
  };
3490
+ /**
3491
+ * Return the refusal frame of an enterprise 200, or `undefined` when the stream
3492
+ * is a real answer.
3493
+ *
3494
+ * Reads (and re-plays) only the first SSE frame: a refusal is emitted before
3495
+ * any output token, so peeking that far is enough to classify the call, and
3496
+ * the stream is rebuilt so a caller that got `undefined` still sees every byte.
3497
+ */
3498
+ function concatStream(head, reader) {
3499
+ return new ReadableStream({ start(controller) {
3500
+ if (head !== "") controller.enqueue(new TextEncoder().encode(head));
3501
+ const drain = () => {
3502
+ reader.read().then((result) => {
3503
+ if (result.done) {
3504
+ controller.close();
3505
+ return;
3506
+ }
3507
+ controller.enqueue(result.value);
3508
+ drain();
3509
+ }).catch((error) => controller.error(error));
3510
+ };
3511
+ drain();
3512
+ } });
3513
+ }
3514
+ /**
3515
+ * Classify an enterprise 200.
3516
+ *
3517
+ * Returns `{ refusal }` when the first SSE frame is `event:error`, otherwise
3518
+ * `{ response }` carrying a REBUILT response whose body re-emits every byte —
3519
+ * including the peeked frame. A refusal is emitted before any output token, so
3520
+ * peeking one frame is enough to classify the call.
3521
+ *
3522
+ * The response is rebuilt rather than patched: the original's body has been
3523
+ * partially read, so its `text()`/`json()` would keep failing with "Body has
3524
+ * already been read" even after `body` is swapped.
3525
+ */
3526
+ async function classifyEnterpriseResponse(response) {
3527
+ const body = response.body;
3528
+ if (body === null) return { response };
3529
+ const reader = body.getReader();
3530
+ const decoder = new TextDecoder();
3531
+ let head = "";
3532
+ try {
3533
+ while (!head.includes("\n\n")) {
3534
+ const { done, value } = await reader.read();
3535
+ if (done) break;
3536
+ head += decoder.decode(value, { stream: true });
3537
+ if (head.length > 8192) break;
3538
+ }
3539
+ } catch {
3540
+ return { response };
3541
+ }
3542
+ const frame = head.split("\n\n")[0] ?? "";
3543
+ const event = /^event:(\S+)/m.exec(frame)?.[1];
3544
+ const data = frame.split("\n").find((line) => line.startsWith("data:"))?.slice(5).trim() ?? "";
3545
+ if (event !== "error") return { response: new Response(concatStream(head, reader), {
3546
+ status: response.status,
3547
+ statusText: response.statusText,
3548
+ headers: response.headers
3549
+ }) };
3550
+ try {
3551
+ await reader.cancel();
3552
+ } catch {}
3553
+ return { refusal: data === "" ? "enterprise gateway refused the request" : data };
3554
+ }
3233
3555
  //#endregion
3234
3556
  //#region src/sse.ts
3235
3557
  /** Incremental SSE decoder supporting CRLF, chunk splits and multi-line data. */
@@ -3370,6 +3692,13 @@ function decodeTraeEvent(event) {
3370
3692
  * two different statements, and a user hits the second without touching the
3371
3693
  * first (issue #33).
3372
3694
  * - `4023` — the model name is unknown on the route that was asked.
3695
+ * - `4222` — the ENTERPRISE tenant's allowance is exhausted. Measured
3696
+ * 2026-10-17 on a live tenant account whose enterprise credit ran out: the
3697
+ * gateway answered HTTP 200 with `{"code":4222,"message":""}` for a model
3698
+ * that streamed normally moments earlier (and still streams on the consumer
3699
+ * gateway). Like `4008` it is a billing fact rather than a config one, but
3700
+ * it is specific to the tenant pool -- the consumer balance is unaffected,
3701
+ * which is why it must not be folded into the `4008` reading.
3373
3702
  * - `2001` — the upstream has not published its function config for the route
3374
3703
  * that was asked. Evidence is issue #39: on a live CN account every SOLO
3375
3704
  * function's `get_detail_param` answered HTTP 200 with `config_info_list:
@@ -3387,7 +3716,8 @@ const TRAE_ERROR_HINTS = {
3387
3716
  4001: "Trae does not serve this model under the SOLO function the request used (config_name rejected)",
3388
3717
  4008: "Trae reports the account's SOLO quota is exhausted for this request",
3389
3718
  4011: "Trae throttled the SOLO channel for this account (a rate limit, not the credit balance)",
3390
- 4023: "Trae does not recognise this model name on the SOLO route that was asked"
3719
+ 4023: "Trae does not recognise this model name on the SOLO route that was asked",
3720
+ 4222: "The enterprise tenant allowance is exhausted (tenant spend is separate from the personal credit balance; the consumer gateway still has its own)"
3391
3721
  };
3392
3722
  /** `extra` is a JSON *string* on the wire; unwrap it to read `plan`. */
3393
3723
  function traeErrorPlan(extra) {
@@ -3717,14 +4047,29 @@ var TraeSoloBridge = class {
3717
4047
  try {
3718
4048
  const input = JSON.parse(bodyJson);
3719
4049
  if (typeof input["model"] === "string" && input["model"] !== "") model = input["model"];
3720
- const entry = this.catalog?.current().find((item) => item.id === model);
4050
+ const entryKey = model.trim().toLowerCase();
4051
+ const entry = this.catalog?.current().find((item) => item.id === model || item.id.trim().toLowerCase() === entryKey);
3721
4052
  const fromCatalog = entry?.wireConfigName === void 0 ? void 0 : {
3722
4053
  configName: entry.wireConfigName,
3723
4054
  ...entry.wireFunction === void 0 ? {} : { function: entry.wireFunction }
3724
4055
  };
3725
4056
  const fromResolver = this.wireResolver?.(model) ?? this.wireResolver?.(entry?.name ?? "");
3726
4057
  const target = fromCatalog ?? fromResolver;
3727
- const wireModel = target?.configName ?? model;
4058
+ /**
4059
+ * Last line of defence: the discovered row's own id.
4060
+ *
4061
+ * For models whose directory id IS the gateway `config_name` the merge
4062
+ * deliberately emits no `wireConfigName` (there is nothing to map), so a
4063
+ * requested id that differs only by case has neither mapping source —
4064
+ * and the row we just matched case-blindly is exactly the canonical
4065
+ * spelling. Sending it is what a correct mapping would have produced.
4066
+ *
4067
+ * Deliberately NOT derived from the row's `name`: the rosters spell most
4068
+ * ids in the opposite case of their display names (`glm-5.3` vs
4069
+ * `GLM-5.3`) and the gateway accepts the LOWERCASE id for those — a
4070
+ * name-derived rewrite answered `4001` for 4 of 5 probed models.
4071
+ */
4072
+ const wireModel = target?.configName ?? entry?.id ?? model;
3728
4073
  const wireFunction = target?.function ?? entry?.wireFunction;
3729
4074
  if (wireModel !== input["model"]) input["model"] = wireModel;
3730
4075
  if (wireFunction !== void 0 && input["function"] !== wireFunction) input["function"] = wireFunction;
@@ -5469,16 +5814,26 @@ function toCheckin(status) {
5469
5814
  };
5470
5815
  }
5471
5816
  /**
5472
- * Assemble one region's card document. `region` is the tab the card is on; the
5473
- * region-scoped store already answers with only that region's accounts, so the
5817
+ * Assemble one region's card document. `panel` is the tab the card is on; the
5818
+ * panel-scoped store already answers with only that region's accounts, so the
5474
5819
  * document's model slots and account list are that region's by construction.
5475
5820
  * Sign-in state is read-only; credit is a live billing answer whose failure
5476
5821
  * degrades to `creditsError` rather than failing the whole document.
5477
5822
  */
5478
- async function traeWebUsage(deps, region) {
5479
- const store = deps.store(region);
5480
- const enabled = deps.regionEnabled(region);
5481
- const accounts = await store.accounts();
5823
+ async function traeWebUsage(deps, panel) {
5824
+ const store = deps.store(panel);
5825
+ const enabled = deps.regionEnabled(panel);
5826
+ /**
5827
+ * Accounts this panel may actually bill.
5828
+ *
5829
+ * Filtered ONCE, at the source, because this list is spliced into every
5830
+ * response shape below (`signed-out`, `signed-in`, the credits route). The
5831
+ * enterprise pool can only bill a tenant account, and the tenant gateway does
5832
+ * not make that obvious: it answers a personal token's DIRECTORY read in
5833
+ * full and only refuses its chat (`4019`). Listing a personal account there
5834
+ * would offer a binding that cannot work.
5835
+ */
5836
+ const accounts = (await store.accounts()).filter((account) => panel !== "enterprise" || account.enterprise);
5482
5837
  if ((await store.status()).state !== "signed-in") {
5483
5838
  const { failures } = await store.diagnose();
5484
5839
  return {
@@ -5507,26 +5862,26 @@ async function traeWebUsage(deps, region) {
5507
5862
  }
5508
5863
  let appLogin;
5509
5864
  try {
5510
- appLogin = await deps.liveLogin?.(region);
5865
+ appLogin = await deps.liveLogin?.(panel);
5511
5866
  } catch {}
5512
5867
  const account = {
5513
5868
  accountId: accounts.find((item) => item.selected)?.id ?? "",
5514
5869
  accountName: credential.accountName ?? credential.userId,
5515
5870
  tokenExpiresAtMs: credential.expiresAtMs,
5516
5871
  ...credential.refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs: credential.refreshExpiresAtMs },
5517
- region,
5872
+ region: regionOfPanel(panel),
5518
5873
  enabled,
5519
5874
  accounts,
5520
5875
  ...appLogin === void 0 ? {} : { appLogin },
5521
- models: deps.displayModels(region).map((model) => ({
5876
+ models: deps.displayModels(panel).map((model) => ({
5522
5877
  ...model,
5523
5878
  ...model.input === void 0 ? {} : { input: [...model.input] }
5524
5879
  })),
5525
- enabledModelIds: [...deps.enabledModelIds(region)],
5526
- ...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic(region) }
5880
+ enabledModelIds: [...deps.enabledModelIds(panel)],
5881
+ ...deps.rawDiagnostic === void 0 ? {} : { rawChat: deps.rawDiagnostic(panel) }
5527
5882
  };
5528
- const client = deps.client(region);
5529
- if (region === "ai") try {
5883
+ const client = deps.client(panel);
5884
+ if (regionOfPanel(panel) === "ai") try {
5530
5885
  const payStatus = await client.payStatus();
5531
5886
  return {
5532
5887
  status: "signed-in",
@@ -5540,9 +5895,25 @@ async function traeWebUsage(deps, region) {
5540
5895
  payStatusError: safeMessage(error)
5541
5896
  };
5542
5897
  }
5898
+ /**
5899
+ * The enterprise panel shows NO credit figure, deliberately.
5900
+ *
5901
+ * `client.snapshot()` reads the CONSUMER packs, and that is the personal
5902
+ * balance — not what a tenant turn spends. There is no enterprise counterpart
5903
+ * to read: every tenant quota endpoint answers 404 (probed 2026-10-17), and
5904
+ * the tenant console at console.enterprise.trae.cn is the only place the
5905
+ * enterprise balance exists. Showing the personal number under an enterprise
5906
+ * heading would be actively misleading, so the panel reports the absence
5907
+ * explicitly and the card explains where the real figure lives.
5908
+ */
5909
+ if (panel === "enterprise") return {
5910
+ status: "signed-in",
5911
+ ...account,
5912
+ creditsUnavailable: "enterprise"
5913
+ };
5543
5914
  const [snapshotResult, checkinResult] = await Promise.allSettled([client.snapshot(), client.checkinStatus()]);
5544
5915
  const credits = snapshotResult.status === "fulfilled" ? toCredits(snapshotResult.value) : void 0;
5545
- const alternatives = credits !== void 0 && credits.workAvailable <= 0 && credits.generalAvailable <= 0 ? await creditAlternatives(deps, region, accounts) : void 0;
5916
+ const alternatives = credits !== void 0 && credits.workAvailable <= 0 && credits.generalAvailable <= 0 ? await creditAlternatives(deps, panel, accounts) : void 0;
5546
5917
  return {
5547
5918
  status: "signed-in",
5548
5919
  ...account,
@@ -5563,13 +5934,13 @@ async function traeWebUsage(deps, region) {
5563
5934
  * problem to explain. The list is capped so a machine with many stale
5564
5935
  * credentials cannot turn one status read into a burst of upstream calls.
5565
5936
  */
5566
- async function creditAlternatives(deps, region, accounts) {
5937
+ async function creditAlternatives(deps, panel, accounts) {
5567
5938
  if (deps.creditsOfAccount === void 0) return void 0;
5568
- const candidates = accounts.filter((account) => !account.selected && account.region === region).slice(0, TRAE_CREDIT_ALTERNATIVE_LIMIT);
5939
+ const candidates = accounts.filter((account) => !account.selected && account.region === regionOfPanel(panel)).filter((account) => panel !== "enterprise" || account.enterprise).slice(0, TRAE_CREDIT_ALTERNATIVE_LIMIT);
5569
5940
  if (candidates.length === 0) return void 0;
5570
5941
  const alternatives = (await Promise.all(candidates.map(async (account) => {
5571
5942
  try {
5572
- const credits = await deps.creditsOfAccount(region, account.id);
5943
+ const credits = await deps.creditsOfAccount(panel, account.id);
5573
5944
  if (credits === void 0) return void 0;
5574
5945
  if (credits.workAvailable <= 0 && credits.generalAvailable <= 0) return void 0;
5575
5946
  return {
@@ -5586,11 +5957,11 @@ async function creditAlternatives(deps, region, accounts) {
5586
5957
  return alternatives.length === 0 ? void 0 : alternatives;
5587
5958
  }
5588
5959
  /**
5589
- * The region a request addresses, or a 400 answer. Absent parameter means the
5960
+ * The panel a request addresses, or a 400 answer. Absent parameter means the
5590
5961
  * domestic tab; an unknown value is refused rather than guessed.
5591
5962
  */
5592
5963
  /**
5593
- * The general balance of every account in one region.
5964
+ * The general balance of every account in one panel.
5594
5965
  *
5595
5966
  * One upstream read per account — the same call `creditsOfAccount` already
5596
5967
  * makes for the exhausted-balance alternatives, widened to every account and to
@@ -5601,16 +5972,16 @@ async function creditAlternatives(deps, region, accounts) {
5601
5972
  * figure: the table lists how many accounts exist even when one cannot be
5602
5973
  * quoted, rather than dropping it and making the switch unavailable.
5603
5974
  */
5604
- async function traeAccountCredits(deps, region) {
5605
- const accounts = (await deps.store(region).accounts()).filter((account) => account.region === region);
5975
+ async function traeAccountCredits(deps, panel) {
5976
+ const accounts = (await deps.store(panel).accounts()).filter((account) => account.region === regionOfPanel(panel)).filter((account) => panel !== "enterprise" || account.enterprise);
5606
5977
  if (accounts.length === 0) return [];
5607
5978
  return Promise.all(accounts.map(async (account) => {
5608
5979
  let generalAvailable;
5609
5980
  if (account.selected) try {
5610
- generalAvailable = toCredits(await deps.client(region).snapshot()).generalAvailable;
5981
+ generalAvailable = toCredits(await deps.client(panel).snapshot()).generalAvailable;
5611
5982
  } catch {}
5612
5983
  else if (deps.creditsOfAccount !== void 0) try {
5613
- generalAvailable = (await deps.creditsOfAccount(region, account.id))?.generalAvailable;
5984
+ generalAvailable = (await deps.creditsOfAccount(panel, account.id))?.generalAvailable;
5614
5985
  } catch {}
5615
5986
  return {
5616
5987
  id: account.id,
@@ -5621,12 +5992,12 @@ async function traeAccountCredits(deps, region) {
5621
5992
  }));
5622
5993
  }
5623
5994
  function requestRegion(req, res) {
5624
- const region = regionOfTraeStatusUrl(req.url ?? "/");
5625
- if (region === void 0) {
5995
+ const panel = regionOfTraeStatusUrl(req.url ?? "/");
5996
+ if (panel === void 0) {
5626
5997
  json(res, 400, { error: "unknown region" });
5627
5998
  return;
5628
5999
  }
5629
- return region;
6000
+ return panel;
5630
6001
  }
5631
6002
  /** Mount the GET usage route on an optional webServer context. */
5632
6003
  function registerTraeUsageRoute(ctx, deps) {
@@ -5643,17 +6014,17 @@ function registerTraeUsageRoute(ctx, deps) {
5643
6014
  json(res, 403, { error: "origin-not-trusted" });
5644
6015
  return;
5645
6016
  }
5646
- const region = requestRegion(req, res);
5647
- if (region === void 0) return;
6017
+ const panel = requestRegion(req, res);
6018
+ if (panel === void 0) return;
5648
6019
  try {
5649
- json(res, 200, await traeWebUsage(deps, region));
6020
+ json(res, 200, await traeWebUsage(deps, panel));
5650
6021
  } catch (error) {
5651
6022
  json(res, 500, { error: safeMessage(error) });
5652
6023
  }
5653
6024
  }
5654
6025
  });
5655
6026
  /**
5656
- * General balance of every account in the region — read only when the
6027
+ * General balance of every account in the panel — read only when the
5657
6028
  * composer panel is opened. One upstream read per account, paid once per
5658
6029
  * open rather than on the 5-minute cycle (see
5659
6030
  * {@link TRAE_ACCOUNT_CREDITS_PATH}).
@@ -5664,10 +6035,10 @@ function registerTraeUsageRoute(ctx, deps) {
5664
6035
  handler: async (req, res) => {
5665
6036
  if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
5666
6037
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
5667
- const region = requestRegion(req, res);
5668
- if (region === void 0) return;
6038
+ const panel = requestRegion(req, res);
6039
+ if (panel === void 0) return;
5669
6040
  try {
5670
- json(res, 200, { accounts: await traeAccountCredits(deps, region) });
6041
+ json(res, 200, { accounts: await traeAccountCredits(deps, panel) });
5671
6042
  } catch (error) {
5672
6043
  json(res, 500, { error: safeMessage(error) });
5673
6044
  }
@@ -5687,12 +6058,12 @@ function registerTraeUsageRoute(ctx, deps) {
5687
6058
  if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
5688
6059
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
5689
6060
  if (deps.accountStateOf === void 0) return json(res, 503, { error: "account status unavailable" });
5690
- const region = requestRegion(req, res);
5691
- if (region === void 0) return;
6061
+ const panel = requestRegion(req, res);
6062
+ if (panel === void 0) return;
5692
6063
  const accountId = new URL(req.url ?? "/", "http://localhost").searchParams.get("accountId");
5693
6064
  if (accountId === null || accountId.trim() === "") return json(res, 400, { error: "missing accountId" });
5694
6065
  try {
5695
- const state = await deps.accountStateOf(region, accountId);
6066
+ const state = await deps.accountStateOf(panel, accountId);
5696
6067
  if (state === void 0) return json(res, 404, { error: "unknown account" });
5697
6068
  json(res, 200, state);
5698
6069
  } catch (error) {
@@ -5706,10 +6077,10 @@ function registerTraeUsageRoute(ctx, deps) {
5706
6077
  handler: async (req, res) => {
5707
6078
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
5708
6079
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
5709
- const region = requestRegion(req, res);
5710
- if (region === void 0) return;
6080
+ const panel = requestRegion(req, res);
6081
+ if (panel === void 0) return;
5711
6082
  try {
5712
- json(res, 200, { accounts: await deps.store(region).accounts() });
6083
+ json(res, 200, { accounts: (await deps.store(panel).accounts()).filter((account) => panel !== "enterprise" || account.enterprise) });
5713
6084
  } catch (error) {
5714
6085
  json(res, 500, { error: safeMessage(error) });
5715
6086
  }
@@ -5730,15 +6101,15 @@ function registerTraeUsageRoute(ctx, deps) {
5730
6101
  handler: async (req, res) => {
5731
6102
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
5732
6103
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
5733
- const region = requestRegion(req, res);
5734
- if (region === void 0) return;
6104
+ const panel = requestRegion(req, res);
6105
+ if (panel === void 0) return;
5735
6106
  const action = transferActionOf(req.url ?? "");
5736
6107
  if (action === void 0) return json(res, 400, { error: "unknown transfer action" });
5737
6108
  try {
5738
6109
  const body = await readTransferBody(req);
5739
6110
  if (action === "export") {
5740
6111
  const wanted = new Set(Array.isArray(body["accountIds"]) ? body["accountIds"].filter((id) => typeof id === "string") : []);
5741
- const store = deps.store(region);
6112
+ const store = deps.store(panel);
5742
6113
  const records = [];
5743
6114
  for (const account of await store.accounts()) {
5744
6115
  if (!wanted.has(account.id)) continue;
@@ -5765,8 +6136,8 @@ function registerTraeUsageRoute(ctx, deps) {
5765
6136
  const text = typeof body["text"] === "string" ? body["text"] : "";
5766
6137
  if (action === "preview") return json(res, 200, previewTransferFile(text));
5767
6138
  const indexes = Array.isArray(body["indexes"]) ? body["indexes"].filter((index) => typeof index === "number" && Number.isInteger(index)) : [];
5768
- const { changes, imported, skipped } = mergeTransferRecords(parseTransferFile(text), indexes, region);
5769
- if (changes.size > 0) await deps.store(region).importAccounts([...changes.values()]);
6139
+ const { changes, imported, skipped } = mergeTransferRecords(parseTransferFile(text), indexes, regionOfPanel(panel));
6140
+ if (changes.size > 0) await deps.store(panel).importAccounts([...changes.values()]);
5770
6141
  json(res, 200, {
5771
6142
  imported,
5772
6143
  skipped
@@ -5783,10 +6154,10 @@ function registerTraeUsageRoute(ctx, deps) {
5783
6154
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
5784
6155
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
5785
6156
  if (deps.discoverModels === void 0) return json(res, 503, { error: "model refresh unavailable" });
5786
- const region = requestRegion(req, res);
5787
- if (region === void 0) return;
6157
+ const panel = requestRegion(req, res);
6158
+ if (panel === void 0) return;
5788
6159
  try {
5789
- json(res, 200, { models: await deps.discoverModels(region) });
6160
+ json(res, 200, { models: await deps.discoverModels(panel) });
5790
6161
  } catch (error) {
5791
6162
  json(res, 500, { error: safeMessage(error) });
5792
6163
  }
@@ -5809,8 +6180,8 @@ function registerTraeUsageRoute(ctx, deps) {
5809
6180
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
5810
6181
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
5811
6182
  if (deps.probeModels === void 0) return json(res, 503, { error: "model test unavailable" });
5812
- const region = requestRegion(req, res);
5813
- if (region === void 0) return;
6183
+ const panel = requestRegion(req, res);
6184
+ if (panel === void 0) return;
5814
6185
  let ids;
5815
6186
  try {
5816
6187
  ids = await readModelTestBody(req);
@@ -5820,7 +6191,7 @@ function registerTraeUsageRoute(ctx, deps) {
5820
6191
  if (ids.length === 0) return json(res, 400, { error: "no models to test" });
5821
6192
  if (ids.length > 64) return json(res, 400, { error: `too many models (limit ${String(64)})` });
5822
6193
  try {
5823
- json(res, 200, await deps.probeModels(region, ids));
6194
+ json(res, 200, await deps.probeModels(panel, ids));
5824
6195
  } catch (error) {
5825
6196
  json(res, 500, { error: safeMessage(error) });
5826
6197
  }
@@ -5852,9 +6223,9 @@ function registerTraeUsageRoute(ctx, deps) {
5852
6223
  handler: async (req, res) => {
5853
6224
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
5854
6225
  if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
5855
- const region = requestRegion(req, res);
5856
- if (region === void 0) return;
5857
- if (region !== "cn") return json(res, 404, { error: "check-in is not available for the international region" });
6226
+ const panel = requestRegion(req, res);
6227
+ if (panel === void 0) return;
6228
+ if (regionOfPanel(panel) !== "cn") return json(res, 404, { error: "check-in is not available for the international region" });
5858
6229
  try {
5859
6230
  let accountId;
5860
6231
  try {
@@ -5864,7 +6235,7 @@ function registerTraeUsageRoute(ctx, deps) {
5864
6235
  }
5865
6236
  if (accountId !== void 0) {
5866
6237
  if (deps.claimCheckinOf === void 0) return json(res, 503, { error: "check-in by account unavailable" });
5867
- const outcome = await deps.claimCheckinOf(region, accountId);
6238
+ const outcome = await deps.claimCheckinOf(panel, accountId);
5868
6239
  if (outcome === void 0) return json(res, 404, { error: "unknown account" });
5869
6240
  const { claim, checkin: after } = outcome;
5870
6241
  const deviceClaimed = claim.code === CHECKIN_DEVICE_ALREADY_CLAIMED;
@@ -5877,7 +6248,7 @@ function registerTraeUsageRoute(ctx, deps) {
5877
6248
  checkin: toCheckin(after)
5878
6249
  });
5879
6250
  }
5880
- const client = deps.client(region);
6251
+ const client = deps.client(panel);
5881
6252
  const current = await client.checkinStatus();
5882
6253
  if (!current.enabled) return json(res, 409, { error: "check-in is not enabled for this account" });
5883
6254
  if (current.checkedIn) return json(res, 200, {
@@ -6146,18 +6517,24 @@ function settingsNamespaceOf(ctx) {
6146
6517
  return typeof id === "string" && id !== "" ? id : TRAE_SETTINGS_NS;
6147
6518
  }
6148
6519
  /**
6149
- * One region's saved state. A config written before the region split has only
6520
+ * One panel's saved state. A config written before the region split has only
6150
6521
  * the flat fields: those were always captured from the CN endpoint (the
6151
6522
  * plugin had no international support), so they are read as the CN state and
6152
6523
  * only when no explicit CN slot exists. The ai region never inherits them —
6153
6524
  * that inheritance is exactly the bug where a stale CN directory would be
6154
6525
  * intersected with the international catalog and silently drop the user's
6155
6526
  * picks (the same failure workbuddy fixed with its region split).
6527
+ *
6528
+ * Keyed by PANEL (`cn` | `enterprise` | `ai`), because each panel carries its
6529
+ * own switch, account binding and model selection. `enterprise` has no legacy
6530
+ * equivalent: it starts from an EMPTY state (its own directory, its own model
6531
+ * picks) and never inherits CN's, so switching it on cannot silently activate
6532
+ * a model list the user never chose for the tenant pool.
6156
6533
  */
6157
- function regionStateOf(config, region) {
6158
- const stored = unwrapVolatile(unwrapVolatile(config.regions)?.[region]);
6534
+ function regionStateOf(config, panel) {
6535
+ const stored = unwrapVolatile(unwrapVolatile(config.regions)?.[panel]);
6159
6536
  if (stored !== void 0) return stored;
6160
- if (region !== "cn") return {};
6537
+ if (panel !== "cn") return {};
6161
6538
  return {
6162
6539
  ...config.lastCatalog === void 0 ? {} : { lastCatalog: config.lastCatalog },
6163
6540
  ...config.enabledModelIds === void 0 ? {} : { enabledModelIds: config.enabledModelIds },
@@ -6166,15 +6543,23 @@ function regionStateOf(config, region) {
6166
6543
  };
6167
6544
  }
6168
6545
  /**
6169
- * Whether one region's provider is switched on (issue #11). Opt-out semantics:
6546
+ * Whether one panel's provider is switched on (issue #11). Opt-out semantics:
6170
6547
  * only an explicit `false` disables it, so every config written before this
6171
6548
  * switch existed — including the pre-region-split flat fields, which never
6172
6549
  * carry `enabled` — keeps both providers running exactly as before. The card
6173
6550
  * reads the same rule through `regionEnabledOf`, so the two halves can never
6174
6551
  * disagree about a region's state.
6552
+ *
6553
+ * The ENTERPRISE panel is the one deliberate exception: it defaults OFF. A
6554
+ * consumer-only account (`tenant 7o2d894p7dr0o4`) cannot use that pool at all,
6555
+ * so opting every existing user into a second CN provider would put a route in
6556
+ * their model picker that fails on first use. It is switched on from its card
6557
+ * tab, by someone whose account actually belongs to a tenant.
6175
6558
  */
6176
- function regionEnabled(config, region) {
6177
- return regionStateOf(config, region).enabled !== false;
6559
+ function regionEnabled(config, panel) {
6560
+ const stored = regionStateOf(config, panel).enabled;
6561
+ if (stored !== void 0) return stored !== false;
6562
+ return panel !== "enterprise";
6178
6563
  }
6179
6564
  /**
6180
6565
  * WHERE one region's credit readout sits (learned from workbuddy #62).
@@ -6216,10 +6601,16 @@ const modelConfig = z.object({
6216
6601
  input: z.array(z.union(["text", "image"])),
6217
6602
  creditMultiplier: z.number(),
6218
6603
  wireFunction: z.string(),
6604
+ wireConfigName: z.string(),
6219
6605
  multimodal: z.boolean()
6220
6606
  });
6607
+ /**
6608
+ * One panel's saved state. Panels are keyed `cn | enterprise | ai` — see
6609
+ * {@link TraePanel}; `enterprise` is a second CN provider with its own switch,
6610
+ * account binding and model selection.
6611
+ */
6221
6612
  const regionStateConfig = z.object({
6222
- enabled: z.boolean().default(true).description("Whether this region's provider is offered to DSH (opt-out; false withdraws it entirely)"),
6613
+ enabled: z.boolean().description("Whether this panel's provider is offered to DSH (false withdraws it entirely)"),
6223
6614
  lastCatalog: z.array(modelConfig).default([]),
6224
6615
  enabledModelIds: z.array(z.string()).default([]),
6225
6616
  imageModelIds: z.array(z.string()).default([]),
@@ -6228,7 +6619,8 @@ const regionStateConfig = z.object({
6228
6619
  });
6229
6620
  const accountSelectionConfig = z.object({
6230
6621
  cn: z.string().description("Selected domestic (CN) account id (never a token)"),
6231
- ai: z.string().description("Selected international account id (never a token)")
6622
+ ai: z.string().description("Selected international account id (never a token)"),
6623
+ enterprise: z.string().description("Selected account id for the CN enterprise pool (never a token)")
6232
6624
  });
6233
6625
  const Config = z.object({
6234
6626
  authFile: asVolatile(z.string().description("Optional Trae storage.json path override")),
@@ -6242,8 +6634,8 @@ const Config = z.object({
6242
6634
  accountId: z.string().description("Deprecated: pre-split account selector, attributed to its own region"),
6243
6635
  showPointsInMainUi: asVolatile(z.boolean().default(false).description("在输入框工具栏显示积分读数「Trae CN · <数值>」(仅当选中的模型属于本插件时出现,可点击查看账号与积分明细)")),
6244
6636
  followAppLogin: asVolatile(z.boolean().default(false).description("跟随 Trae 桌面端当前登录账号换绑(打开后每次配置生效时重绑到 App 现在登录的账号)")),
6245
- accounts: asVolatile(accountSelectionConfig.description("Per-region account selections, keyed cn | ai")),
6246
- regions: asVolatile(z.dict(regionStateConfig).default({}).description("Per-region model directory and selection, keyed cn | ai")),
6637
+ accounts: asVolatile(accountSelectionConfig.description("Per-panel account selections, keyed cn | enterprise | ai")),
6638
+ regions: asVolatile(z.dict(regionStateConfig).default({}).description("Per-panel model directory and selection, keyed cn | enterprise | ai")),
6247
6639
  lastCatalog: z.array(modelConfig).description("Deprecated: pre-region-split CN model directory"),
6248
6640
  enabledModelIds: z.array(z.string()).default([]).description("Deprecated: pre-region-split CN selection"),
6249
6641
  contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Deprecated: pre-region-split CN context budgets"),
@@ -6255,8 +6647,8 @@ const REGION_KEYS = ["cn", "ai"];
6255
6647
  function apply(ctx, config) {
6256
6648
  const settingsNs = settingsNamespaceOf(ctx);
6257
6649
  let current = () => config;
6258
- const enabledSet = (value, region) => new Set(regionStateOf(value, region).enabledModelIds ?? []);
6259
- const imageSet = (value, region) => new Set(regionStateOf(value, region).imageModelIds ?? []);
6650
+ const enabledSet = (value, panel) => new Set(regionStateOf(value, panel).enabledModelIds ?? []);
6651
+ const imageSet = (value, panel) => new Set(regionStateOf(value, panel).imageModelIds ?? []);
6260
6652
  /**
6261
6653
  * Per-region wire state. Each region's discovery owns its own maps: the CN
6262
6654
  * and international rosters overlap (and spell some ids differently), so a
@@ -6269,30 +6661,30 @@ function apply(ctx, config) {
6269
6661
  * `lastCatalog` / `models` / `enabledModelIds` that still lists it.
6270
6662
  */
6271
6663
  const wireState = {};
6272
- for (const region of REGION_KEYS) wireState[region] = {
6664
+ for (const panel of TRAE_PANELS) wireState[panel] = {
6273
6665
  callableKeys: /* @__PURE__ */ new Set(),
6274
6666
  resolved: false,
6275
6667
  byId: /* @__PURE__ */ new Map(),
6276
6668
  byName: /* @__PURE__ */ new Map()
6277
6669
  };
6278
- const dropDeadModels = (rows, region) => {
6279
- const wire = wireState[region];
6670
+ const dropDeadModels = (rows, panel) => {
6671
+ const wire = wireState[panel];
6280
6672
  if (!wire.resolved) return rows;
6281
6673
  return rows.filter((model) => wire.callableKeys.has(model.id.trim().toLowerCase()) || wire.callableKeys.has(model.name.trim().toLowerCase()));
6282
6674
  };
6283
- const fallbackModels = (value, region) => applyImageSelection(fallbackModelsFor(region), imageSet(value, region));
6284
- const derive = (value, raw, region) => {
6285
- const selectedImages = imageSet(value, region);
6286
- const derived = deriveCatalog(applyImageSelection(sanitizeCatalog(dropDeadModels(raw, region)), selectedImages), enabledSet(value, region), regionStateOf(value, region).contextBudgets ?? {});
6287
- return derived.length > 0 ? derived : fallbackModels(value, region);
6675
+ const fallbackModels = (value, panel) => applyImageSelection(fallbackModelsFor(regionOfPanel(panel)), imageSet(value, panel));
6676
+ const derive = (value, raw, panel) => {
6677
+ const selectedImages = imageSet(value, panel);
6678
+ const derived = deriveCatalog(applyImageSelection(sanitizeCatalog(dropDeadModels(raw, panel)), selectedImages), enabledSet(value, panel), regionStateOf(value, panel).contextBudgets ?? {});
6679
+ return derived.length > 0 ? derived : fallbackModels(value, panel);
6288
6680
  };
6289
- const configuredModels = (value, region) => {
6290
- const state = regionStateOf(value, region);
6291
- return state.lastCatalog?.length ? derive(value, state.lastCatalog, region) : region === "cn" && value.models?.length ? derive(value, value.models, region) : fallbackModels(value, region);
6681
+ const configuredModels = (value, panel) => {
6682
+ const state = regionStateOf(value, panel);
6683
+ return state.lastCatalog?.length ? derive(value, state.lastCatalog, panel) : panel === "cn" && value.models?.length ? derive(value, value.models, panel) : fallbackModels(value, panel);
6292
6684
  };
6293
- const displayModels = (value, region) => {
6294
- const state = regionStateOf(value, region);
6295
- return state.lastCatalog?.length ? dropDeadModels(sanitizeCatalog(state.lastCatalog), region) : region === "cn" && value.models?.length ? dropDeadModels(sanitizeCatalog(value.models), region) : fallbackModelsFor(region);
6685
+ const displayModels = (value, panel) => {
6686
+ const state = regionStateOf(value, panel);
6687
+ return state.lastCatalog?.length ? dropDeadModels(sanitizeCatalog(state.lastCatalog), panel) : panel === "cn" && value.models?.length ? dropDeadModels(sanitizeCatalog(value.models), panel) : fallbackModelsFor(regionOfPanel(panel));
6296
6688
  };
6297
6689
  /**
6298
6690
  * Legacy migration for the pre-split single `accountId`: its region is
@@ -6302,9 +6694,20 @@ function apply(ctx, config) {
6302
6694
  * inheriting a selection that belongs to the other side of the split.
6303
6695
  */
6304
6696
  let legacyAccountRegion;
6305
- const effectiveAccountFor = (region, value) => {
6306
- const explicit = unwrapVolatile(value.accounts)?.[region];
6697
+ /**
6698
+ * Which account a panel is bound to.
6699
+ *
6700
+ * Keyed by PANEL: the enterprise pool draws on the same CN credentials but
6701
+ * keeps its OWN binding, so pointing the consumer tab at a personal account
6702
+ * never moves the enterprise tab (and vice versa). A panel with no explicit
6703
+ * binding falls through to that REGION's default selection, which is the
6704
+ * store's own "preferred account for this region".
6705
+ */
6706
+ const effectiveAccountFor = (panel, value) => {
6707
+ const explicit = unwrapVolatile(value.accounts)?.[panel];
6307
6708
  if (explicit !== void 0) return explicit;
6709
+ const region = regionOfPanel(panel);
6710
+ if (panel === "enterprise") return void 0;
6308
6711
  return legacyAccountRegion === region ? value.accountId : void 0;
6309
6712
  };
6310
6713
  /**
@@ -6326,10 +6729,29 @@ function apply(ctx, config) {
6326
6729
  }
6327
6730
  }
6328
6731
  }
6732
+ /**
6733
+ * One stack per PANEL.
6734
+ *
6735
+ * `cn` and `enterprise` share a credential REGION but not a directory, a wire
6736
+ * map or a model selection: the two pools publish different rosters, so a
6737
+ * shared catalog would let one pool's answer filter the other's models. Each
6738
+ * panel therefore gets its own catalog, wire state, clients and shim routes,
6739
+ * and the credential store below is the only thing they have in common.
6740
+ */
6329
6741
  const stacks = {};
6330
- for (const region of REGION_KEYS) {
6742
+ for (const panel of TRAE_PANELS) {
6743
+ const region = regionOfPanel(panel);
6744
+ /**
6745
+ * Whether tenant (enterprise) accounts are also offered by the CONSUMER
6746
+ * panels.
6747
+ *
6748
+ * Off by default, so a published build offers an enterprise login only
6749
+ * under the enterprise panel. Opt-in per machine, and intentionally not
6750
+ * surfaced in user-facing documentation.
6751
+ */
6752
+ const tenantLocal = process.env["DSH_TRAE_TENANT_IN_CONSUMER"] === "1";
6331
6753
  const catalog = new TraeCatalog(region);
6332
- const wire = wireState[region];
6754
+ const wire = wireState[panel];
6333
6755
  /**
6334
6756
  * Best-effort device identity for this region's installation. Prefer the
6335
6757
  * selected credential's own edition so an international account reads its
@@ -6382,6 +6804,7 @@ function apply(ctx, config) {
6382
6804
  };
6383
6805
  const store = new TraeCredentialStore({
6384
6806
  region,
6807
+ ...panel === "enterprise" ? { requireTenant: true } : { hideTenantAccounts: !tenantLocal },
6385
6808
  ...unwrapVolatile(config.authFile) === void 0 ? {} : { storagePath: unwrapVolatile(config.authFile) },
6386
6809
  edition: unwrapVolatile(config.edition) ?? "auto",
6387
6810
  refresh: async (credential) => refreshTraeCredential(credential, void 0, await refreshDeviceSafe())
@@ -6389,19 +6812,65 @@ function apply(ctx, config) {
6389
6812
  const solo = new TraeSoloUpstreamClient({
6390
6813
  credential: () => store.resolve(),
6391
6814
  identity,
6815
+ ...panel === "enterprise" ? { route: "enterprise" } : {},
6392
6816
  log: (message, detail) => ctx.logger.warn(message, detail)
6393
6817
  });
6394
6818
  const remoteCatalog = new TraeSoloRemoteCatalogClient({ credential: () => store.resolve() });
6395
6819
  const wireResolver = (displayId) => wire.byId.get(displayId) ?? wire.byName.get(displayId.trim().toLowerCase());
6396
- const upstream = new TraeSoloBridge(solo, catalog, wireResolver);
6397
- const delegating = new TraeDelegatingUpstreamClient(upstream);
6820
+ /**
6821
+ * One client + bridge per provider id.
6822
+ *
6823
+ * `routeOfTraeProvider` pins each to its billing pool: the consumer ids get
6824
+ * the region gateway exactly as before, `trae-enterprise` gets the tenant
6825
+ * gateway and stamps the `session_id` it requires. The discovery client
6826
+ * below stays UNROUTED on purpose — it owns the roster/probe path and, for
6827
+ * a tenant credential, reads both pools so the shared catalogue keeps every
6828
+ * model either one serves.
6829
+ */
6830
+ /**
6831
+ * Latest successful discovery for this panel, mutated by discoverModels
6832
+ * and READ by applySelection (via the stack): a settings write rebuilds
6833
+ * the catalog from saved rows that may predate wireConfigName, and the
6834
+ * merge below is what keeps the case mapping alive across it.
6835
+ */
6836
+ const latestDiscovery = [];
6837
+ const routeClients = {};
6838
+ const shimRoutes = [];
6839
+ for (const provider of [providerOfPanel(panel)]) {
6840
+ const route = routeOfTraeProvider(provider) ?? "solo";
6841
+ const client = new TraeSoloUpstreamClient({
6842
+ credential: () => store.resolve(),
6843
+ identity,
6844
+ route,
6845
+ log: (message, detail) => ctx.logger.warn(message, detail)
6846
+ });
6847
+ const delegating = new TraeDelegatingUpstreamClient(new TraeSoloBridge(client, catalog, wireResolver));
6848
+ routeClients[provider] = {
6849
+ provider,
6850
+ route,
6851
+ client,
6852
+ upstream: delegating,
6853
+ secret: ""
6854
+ };
6855
+ shimRoutes.push({
6856
+ label: route,
6857
+ client: delegating
6858
+ });
6859
+ }
6398
6860
  const shim = createTraeShim({
6399
6861
  catalog,
6400
- client: delegating,
6862
+ routes: shimRoutes,
6401
6863
  logger: ctx.logger
6402
6864
  });
6865
+ for (const entry of Object.values(routeClients)) entry.secret = shim.tokenOf(entry.route);
6866
+ /**
6867
+ * The region's PRIMARY provider plumbing, used by region-wide paths that
6868
+ * are not pool-specific: model probing, Raw-Chat status, and the stack's
6869
+ * `delegating` handle. Per-provider turns never go through this.
6870
+ */
6871
+ const primary = routeClients[providerOfPanel(panel)];
6403
6872
  let rawDiagnostic = () => ({ state: "disabled" });
6404
- if (region === "cn") (async () => {
6873
+ if (panel === "cn") (async () => {
6405
6874
  try {
6406
6875
  const resolved = await resolveTraeRawRuntime(store, "qwen-3.7-plus");
6407
6876
  const gateway = createTraeRawGateway({
@@ -6415,7 +6884,7 @@ function apply(ctx, config) {
6415
6884
  runtime: resolved.runtime
6416
6885
  }
6417
6886
  }),
6418
- solo: upstream,
6887
+ solo: new TraeSoloBridge(primary.client, catalog, wireResolver),
6419
6888
  endpoint: `${REGION_GATEWAYS.cn.chat}/api/ide/v2/llm_raw_chat`,
6420
6889
  edition: resolved.identity.edition,
6421
6890
  identity: {
@@ -6425,7 +6894,7 @@ function apply(ctx, config) {
6425
6894
  runtime: resolved.runtime,
6426
6895
  enabled: false
6427
6896
  });
6428
- delegating.replace(gateway.upstream);
6897
+ primary.upstream.replace(gateway.upstream);
6429
6898
  rawDiagnostic = () => gateway.diagnostic();
6430
6899
  ctx.effect(() => () => gateway.invalidate(), "dsh-connect-trae: Raw capability invalidation");
6431
6900
  } catch (error) {
@@ -6436,12 +6905,14 @@ function apply(ctx, config) {
6436
6905
  credential: () => store.resolve(),
6437
6906
  deviceId: async () => (await identity()).deviceId
6438
6907
  });
6439
- stacks[region] = {
6908
+ stacks[panel] = {
6440
6909
  region,
6441
6910
  store,
6442
6911
  catalog,
6443
6912
  shim,
6444
- delegating,
6913
+ lastDiscovery: latestDiscovery,
6914
+ delegating: routeClients[providerOfPanel(panel)]?.upstream,
6915
+ routeClients,
6445
6916
  usageClient,
6446
6917
  /**
6447
6918
  * Credit balance of one named account, WITHOUT rebinding the store
@@ -6510,6 +6981,8 @@ function apply(ctx, config) {
6510
6981
  wire.byId.set(model.id, target);
6511
6982
  wire.byName.set(model.name.trim().toLowerCase(), target);
6512
6983
  }
6984
+ latestDiscovery.length = 0;
6985
+ latestDiscovery.push(...merged);
6513
6986
  return merged;
6514
6987
  },
6515
6988
  /**
@@ -6532,7 +7005,7 @@ function apply(ctx, config) {
6532
7005
  const skipped = [];
6533
7006
  for (const id of ids) if (known.has(id.trim().toLowerCase())) testable.push(id);
6534
7007
  else skipped.push(id);
6535
- const results = await probeModelsSequentially((body, signal) => upstream.chatStream(body, signal), testable);
7008
+ const results = await probeModelsSequentially((body, signal) => primary.upstream.chatStream(body, signal), testable);
6536
7009
  return skipped.length === 0 ? { results } : {
6537
7010
  results,
6538
7011
  skipped
@@ -6548,9 +7021,10 @@ function apply(ctx, config) {
6548
7021
  * never moves from here — the card's hint and the follow switch below are
6549
7022
  * the only writers.
6550
7023
  */
6551
- const liveLoginOf = async (region) => {
7024
+ const liveLoginOf = async (panel) => {
7025
+ const region = regionOfPanel(panel);
6552
7026
  try {
6553
- const live = await stacks[region].store.liveLogin();
7027
+ const live = await stacks[panel].store.liveLogin();
6554
7028
  if (live === void 0 || live.region !== region) return void 0;
6555
7029
  return {
6556
7030
  id: live.id,
@@ -6574,30 +7048,30 @@ function apply(ctx, config) {
6574
7048
  * event (`loader/volatile-update`) is what re-runs `applySelection` with the
6575
7049
  * updated document — the same channel every card write already uses.
6576
7050
  */
6577
- const followAppLogin = async (region) => {
6578
- const live = await liveLoginOf(region);
7051
+ const followAppLogin = async (panel) => {
7052
+ const live = await liveLoginOf(panel);
6579
7053
  if (live === void 0 || writeSettings === void 0) return void 0;
6580
7054
  const configured = configuredAccountsOf(current());
6581
- if (configured[region] === live.id) return live;
7055
+ if (configured[panel] === live.id) return live;
6582
7056
  await writeSettings({ accounts: {
6583
7057
  ...configured,
6584
- [region]: live.id
7058
+ [panel]: live.id
6585
7059
  } });
6586
7060
  return live;
6587
7061
  };
6588
7062
  ctx.inject(["webServer"], (webCtx) => registerTraeUsageRoute(webCtx, {
6589
- store: (region) => stacks[region].store,
6590
- client: (region) => stacks[region].usageClient,
6591
- displayModels: (region) => displayModels(current(), region),
6592
- enabledModelIds: (region) => regionStateOf(current(), region).enabledModelIds ?? [],
6593
- regionEnabled: (region) => regionEnabled(current(), region),
6594
- discoverModels: (region, signal) => stacks[region].discoverModels(signal),
6595
- probeModels: (region, ids) => stacks[region].probeModels(ids),
6596
- rawDiagnostic: (region) => stacks[region].rawDiagnostic(),
6597
- creditsOfAccount: (region, accountId) => stacks[region].creditsOfAccount(accountId),
6598
- accountStateOf: (region, accountId) => stacks[region].accountStateOf(accountId),
6599
- claimCheckinOf: (region, accountId) => stacks[region].claimCheckinOf(accountId),
6600
- liveLogin: (region) => liveLoginOf(region)
7063
+ store: (panel) => stacks[panel].store,
7064
+ client: (panel) => stacks[panel].usageClient,
7065
+ displayModels: (panel) => displayModels(current(), panel),
7066
+ enabledModelIds: (panel) => regionStateOf(current(), panel).enabledModelIds ?? [],
7067
+ regionEnabled: (panel) => regionEnabled(current(), panel),
7068
+ discoverModels: (panel, signal) => stacks[panel].discoverModels(signal),
7069
+ probeModels: (panel, ids) => stacks[panel].probeModels(ids),
7070
+ rawDiagnostic: (panel) => stacks[panel].rawDiagnostic(),
7071
+ creditsOfAccount: (panel, accountId) => stacks[panel].creditsOfAccount(accountId),
7072
+ accountStateOf: (panel, accountId) => stacks[panel].accountStateOf(accountId),
7073
+ claimCheckinOf: (panel, accountId) => stacks[panel].claimCheckinOf(accountId),
7074
+ liveLogin: (panel) => liveLoginOf(panel)
6601
7075
  }));
6602
7076
  /**
6603
7077
  * Live registration handles per region, filled once the shim is listening.
@@ -6609,8 +7083,18 @@ function apply(ctx, config) {
6609
7083
  * card tab alone would leave every model selectable.
6610
7084
  */
6611
7085
  const registration = {
6612
- cn: {},
6613
- ai: {}
7086
+ cn: { adapters: [] },
7087
+ enterprise: { adapters: [] },
7088
+ ai: { adapters: [] }
7089
+ };
7090
+ /** Release every live registration this apply() created. */
7091
+ const disposeRegistration = () => {
7092
+ for (const panel of TRAE_PANELS) {
7093
+ for (const entry of registration[panel].adapters) entry.handle();
7094
+ registration[panel].adapters.length = 0;
7095
+ }
7096
+ registration.cn.directory?.();
7097
+ delete registration.cn.directory;
6614
7098
  };
6615
7099
  /**
6616
7100
  * Publish each region's on/off state to the harness (issue #11). Both swaps
@@ -6622,21 +7106,53 @@ function apply(ctx, config) {
6622
7106
  * lands immediately.
6623
7107
  */
6624
7108
  const syncRegionRegistration = (value) => {
6625
- for (const region of REGION_KEYS) registration[region].adapter?.replace(regionEnabled(value, region) ? [TRAE_PROVIDERS[region]] : []);
6626
- registration.cn.directory?.replace(REGION_KEYS.filter((region) => regionEnabled(value, region)).map((region) => ({
6627
- provider: TRAE_PROVIDERS[region],
6628
- displayName: TRAE_PROVIDER_DISPLAY_NAMES[region],
7109
+ for (const panel of TRAE_PANELS) {
7110
+ const enabled = regionEnabled(value, panel);
7111
+ for (const entry of registration[panel].adapters) entry.handle.replace(enabled ? [entry.provider] : []);
7112
+ }
7113
+ registration.cn.directory?.replace(TRAE_PANELS.filter((panel) => regionEnabled(value, panel)).map((panel) => ({
7114
+ provider: providerOfPanel(panel),
7115
+ displayName: displayNameOfTraeProvider(providerOfPanel(panel)),
6629
7116
  settingsNs,
6630
7117
  settingsPath: [],
6631
7118
  declared: false
6632
7119
  })));
6633
7120
  };
6634
- /** Push the current config into every region's store selection and catalog. */
7121
+ /** Push the current config into every panel's store selection and catalog. */
6635
7122
  const applySelection = (value) => {
6636
- for (const region of REGION_KEYS) {
6637
- const stack = stacks[region];
6638
- stack.store.setSource(unwrapVolatile(value.authFile), unwrapVolatile(value.edition) ?? "auto", effectiveAccountFor(region, value));
6639
- stack.catalog.set(configuredModels(value, region));
7123
+ for (const panel of TRAE_PANELS) {
7124
+ const stack = stacks[panel];
7125
+ stack.store.setSource(unwrapVolatile(value.authFile), unwrapVolatile(value.edition) ?? "auto", effectiveAccountFor(panel, value));
7126
+ /**
7127
+ * Re-merge the wire mapping the last discovery taught us.
7128
+ *
7129
+ * configuredModels rebuilds from the SAVED rows, and rows saved before
7130
+ * wireConfigName existed carry wireFunction alone. Every settings write
7131
+ * goes through here (card writes, follow-app-login, the initial seed),
7132
+ * so without this each write would strip the case mapping until the next
7133
+ * discovery -- the exact window that turns deepseek-v4.1-flash into a
7134
+ * 4001, because the gateway is case-sensitive and the lowercase
7135
+ * directory id is the one spelling it refuses.
7136
+ */
7137
+ const known = /* @__PURE__ */ new Map();
7138
+ for (const model of stack.lastDiscovery) {
7139
+ known.set(model.id, model);
7140
+ known.set(model.id.trim().toLowerCase(), model);
7141
+ if (model.name.trim() !== "") known.set(model.name.trim().toLowerCase(), model);
7142
+ }
7143
+ const rows = configuredModels(value, panel).map((model) => {
7144
+ const match = known.get(model.id) ?? known.get(model.id.trim().toLowerCase()) ?? (model.name.trim() !== "" ? known.get(model.name.trim().toLowerCase()) : void 0);
7145
+ if (match === void 0) return model;
7146
+ const wireConfigName = model.wireConfigName ?? match.wireConfigName;
7147
+ const wireFunction = model.wireFunction ?? match.wireFunction;
7148
+ if (wireConfigName === model.wireConfigName && wireFunction === model.wireFunction) return model;
7149
+ return {
7150
+ ...model,
7151
+ ...wireConfigName === void 0 ? {} : { wireConfigName },
7152
+ ...wireFunction === void 0 ? {} : { wireFunction }
7153
+ };
7154
+ });
7155
+ stack.catalog.set(rows);
6640
7156
  stack.invalidateAdapter();
6641
7157
  }
6642
7158
  syncRegionRegistration(value);
@@ -6695,9 +7211,9 @@ function apply(ctx, config) {
6695
7211
  if (followInFlight || !followAppLoginEnabled(current())) return;
6696
7212
  followInFlight = true;
6697
7213
  try {
6698
- for (const region of REGION_KEYS) {
6699
- if (!regionEnabled(current(), region)) continue;
6700
- await followAppLogin(region);
7214
+ for (const panel of TRAE_PANELS) {
7215
+ if (!regionEnabled(current(), panel)) continue;
7216
+ await followAppLogin(panel);
6701
7217
  }
6702
7218
  } catch (error) {
6703
7219
  ctx.logger.warn("dsh-connect-trae: follow-app-login rebind skipped", error);
@@ -6709,77 +7225,78 @@ function apply(ctx, config) {
6709
7225
  let stopped = false;
6710
7226
  ctx.effect(() => () => {
6711
7227
  stopped = true;
6712
- for (const region of REGION_KEYS) stacks[region].shim.close();
7228
+ for (const panel of TRAE_PANELS) stacks[panel].shim.close();
6713
7229
  });
6714
- Promise.all(REGION_KEYS.map((region) => stacks[region].shim.ready)).then(async () => {
7230
+ Promise.all(TRAE_PANELS.map((panel) => stacks[panel].shim.ready)).then(async () => {
6715
7231
  if (stopped) return;
6716
7232
  try {
6717
7233
  const piAi = piAiRuntimeInfo();
6718
7234
  ctx.logger.info(`dsh-connect-trae: pi-ai ${piAi.version ?? "unknown"} (${piAi.generation} generation)${piAi.inspected ? "" : " — not resolved locally, the host supplies it"}`);
6719
- for (const region of REGION_KEYS) {
6720
- const stack = stacks[region];
7235
+ for (const panel of TRAE_PANELS) {
7236
+ const stack = stacks[panel];
7237
+ const provider = providerOfPanel(panel);
7238
+ const entry = stack.routeClients[provider];
7239
+ if (entry === void 0) continue;
6721
7240
  const trae = createTraeAdapter({
6722
7241
  shim: stack.shim,
6723
7242
  catalog: stack.catalog,
6724
- provider: TRAE_PROVIDERS[region],
6725
- displayName: TRAE_PROVIDER_DISPLAY_NAMES[region],
7243
+ provider,
7244
+ displayName: displayNameOfTraeProvider(provider),
7245
+ secret: entry.secret,
6726
7246
  resolveAttachments: () => ctx.get("attachments")
6727
7247
  });
6728
7248
  stack.invalidateAdapter = () => {
6729
7249
  trae.invalidate();
6730
7250
  };
6731
- registration[region].adapter = region === "cn" ? ctx.llm.registerAdapter([TRAE_PROVIDER], trae.adapter) : ctx.llm.registerAdapter([TRAE_AI_PROVIDER], trae.adapter);
7251
+ registration[panel].adapters.push({
7252
+ provider,
7253
+ handle: ctx.llm.registerAdapter([provider], trae.adapter)
7254
+ });
6732
7255
  }
6733
7256
  ctx.llm.registerModelDiscovery(settingsNs, async (request, signal) => {
6734
- const region = regionOfTraeProvider(request.provider ?? "");
6735
- if (region === void 0) return [];
6736
- if (!regionEnabled(current(), region)) return [];
7257
+ const panel = panelOfProvider(request.provider ?? "");
7258
+ if (panel === void 0) return [];
7259
+ if (!regionEnabled(current(), panel)) return [];
6737
7260
  const cancellation = signal ?? request.signal;
6738
- return applyImageSelection(await stacks[region].discoverModels(cancellation), imageSet(current(), region)).map((model) => ({
7261
+ const next = applyImageSelection(await stacks[panel].discoverModels(cancellation), imageSet(current(), panel));
7262
+ const withRate = panel !== "enterprise";
7263
+ return next.map((model) => ({
6739
7264
  id: model.id,
6740
- name: traeModelDisplayName(model),
7265
+ name: traeModelDisplayName(model, withRate),
6741
7266
  ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
6742
7267
  ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
6743
7268
  inputModalities: traeInputModalities(model)
6744
7269
  }));
6745
7270
  });
6746
- registration.cn.directory = ctx.llm.registerConfigurableProviders(REGION_KEYS.map((region) => ({
6747
- provider: TRAE_PROVIDERS[region],
6748
- displayName: TRAE_PROVIDER_DISPLAY_NAMES[region],
7271
+ registration.cn.directory = ctx.llm.registerConfigurableProviders(TRAE_PANELS.map((panel) => ({
7272
+ provider: providerOfPanel(panel),
7273
+ displayName: displayNameOfTraeProvider(providerOfPanel(panel)),
6749
7274
  settingsNs,
6750
7275
  settingsPath: [],
6751
7276
  declared: false
6752
7277
  })));
6753
7278
  syncRegionRegistration(current());
6754
7279
  } finally {
6755
- if (registration.cn.adapter === void 0 || registration.ai.adapter === void 0 || registration.cn.directory === void 0) {
6756
- registration.cn.adapter?.();
6757
- registration.ai.adapter?.();
6758
- registration.cn.directory?.();
6759
- }
7280
+ if (registration.cn.adapters.length === 0 || registration.ai.adapters.length === 0 || registration.cn.directory === void 0) disposeRegistration();
6760
7281
  }
6761
7282
  try {
6762
7283
  ctx.effect(() => () => {
6763
- registration.cn.adapter?.();
6764
- registration.ai.adapter?.();
6765
- registration.cn.directory?.();
7284
+ disposeRegistration();
6766
7285
  });
6767
7286
  } catch {
6768
- registration.cn.adapter?.();
6769
- registration.ai.adapter?.();
6770
- registration.cn.directory?.();
7287
+ disposeRegistration();
6771
7288
  }
6772
- for (const region of REGION_KEYS) {
6773
- if (!regionEnabled(current(), region)) continue;
7289
+ for (const panel of TRAE_PANELS) {
7290
+ if (!regionEnabled(current(), panel)) continue;
6774
7291
  (async () => {
6775
- const stack = stacks[region];
7292
+ const stack = stacks[panel];
6776
7293
  try {
6777
7294
  const models = await stack.discoverModels();
6778
7295
  if (stopped) return;
6779
- stack.catalog.set(derive(current(), models, region));
7296
+ stack.catalog.set(derive(current(), models, panel));
6780
7297
  } catch (error) {
6781
- ctx.logger.warn(`dsh-connect-trae: live ${region} model directory unavailable; serving the configured catalog`, error);
6782
- stack.catalog.set(configuredModels(current(), region));
7298
+ ctx.logger.warn(`dsh-connect-trae: live ${panel} model directory unavailable; serving the configured catalog`, error);
7299
+ stack.catalog.set(configuredModels(current(), panel));
6783
7300
  }
6784
7301
  stack.invalidateAdapter();
6785
7302
  })();
@@ -6789,4 +7306,4 @@ function apply(ctx, config) {
6789
7306
  });
6790
7307
  }
6791
7308
  //#endregion
6792
- export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, discoveredCatalog, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };
7309
+ export { Config, FALLBACK_TRAE_MODELS, FALLBACK_TRAE_MODELS_AI, REGION_GATEWAYS, SseDecoder, TRAE_ACCOUNT_STATUS_PATH, TRAE_AI_PROVIDER, TRAE_CHECKIN_PATH, TRAE_CN_AGENT_TASK_PATH, TRAE_CN_TITLE_PATH, TRAE_ENTERPRISE_PROVIDER, TRAE_MODELS_TEST_PATH, TRAE_MODEL_DETAIL_FUNCTIONS, TRAE_MODEL_DETAIL_PATH, TRAE_PANELS, TRAE_PAY_BASE, TRAE_PROBE_TIMEOUT_MS, TRAE_PROVIDER, TRAE_PROVIDERS, TRAE_PROVIDER_DISPLAY_NAMES, TRAE_PROVIDER_IDS, TRAE_PROVIDER_ID_DISPLAY_NAMES, TRAE_PROVIDER_ROUTES, TRAE_RAW_CHAT_V1_PATH, TRAE_RAW_CHAT_V2_PATH, TRAE_REASONING_EFFORTS, TRAE_REGIONS, TRAE_REGION_PARAM, TRAE_SETTINGS_NS, TRAE_SOLO_CHAT_PATH, TRAE_SOLO_FUNCTION, TRAE_SOLO_MODELS_PATH, TRAE_SOLO_REMOTE_BASE, TRAE_STATE_DB_FILENAME, TRAE_STREAM_IDLE_TIMEOUT_MS, TRAE_USAGE_PATH, TRAE_VERSION_CODE_FALLBACK, TraeCatalog, TraeCredentialStore, TraeDelegatingUpstreamClient, TraeFallbackUpstreamClient, TraeGatedUpstreamClient, TraeRawCapabilityController, TraeRawCapabilityState, TraeRawChatUpstreamClient, TraeSoloBridge, TraeSoloRemoteCatalogClient, TraeSoloUpstreamClient, TraeUsageClient, UnconfiguredTraeUpstreamClient, adaptLegacyPiAiContext, adaptModernPiAiContext, apply, applyContextBudgets, applyImageSelection, applyReasoningEffort, asVolatile, bridgeTraeSoloStream, buildTraeAgentTaskBody, buildTraeCnHeaders, buildTraeFusionRawChatEnvelope, buildTraeModelDetailRequest, buildTraeRawChatDraft, buildTraeRawChatRuntimeConfig, classifyProbeError, classifyProbeResult, classifyTraeRawChatFailure, configuredAccountsOf, createTraeAdapter, createTraeRawGateway, createTraeShim, creditPosition, decidePiAiGeneration, decodeRawChatChunk, decodeTraeEvent, decryptTraeStorageValue, deriveCatalog, describeIdShape, describeNameShape, describeTraeFailure, discoveredCatalog, displayNameOfTraeProvider, fallbackModelsFor, followAppLoginEnabled, generationOf, hashTraeRawChatArg, identityHeaders, inject, isEnterpriseAccount, legacyTraeOwnAuthPath, maskUserPath, mergeTraeModelSources, name, nextRegionCreditPosition, nextRegionEnabled, nextRegionSlots, normalizeTraeCredential, normalizeTraeVersionCode, panelOfKey, panelOfProvider, parseObservedModelConfig, parseReasoningCapability, parseTraeAuthValue, parseTraeCachedModel, parseTraeModelExtraConfigLogLine, parseTraeRawChatBehaviorConfig, parseTraeRemoteModel, parseTraeStorageDocument, piAiRuntimeInfo, pickTraeStorageIdentity, prepareSoloBody, probeBody, probeModelsSequentially, probeOneModel, probeTraeRawChatCapability, providerOfPanel, rawCapabilityDiagnostic, rawCapabilityFingerprint, readTraeCachedModel, readTraeCliIdentity, readTraeIdentity, refreshTraeCredential, regionCreditPositionOf, regionEnabled, regionEnabledOf, regionOfCredential, regionOfEdition, regionOfHost, regionOfPanel, regionOfTraeProvider, regionOfTraeStatusUrl, regionOfUserRegion, regionStateOf, registerTraeUsageRoute, resolveTraeIdentity, resolveTraeRawRuntime, routeOfTraeProvider, sanitizeCatalog, settingsNamespaceOf, traeEndpoint, traeInputModalities, traeModelDisplayName, traeOwnAuthPath, traeRawChatExtraInfo, traeStateDatabaseCandidates, traeStorageCandidates, traeWebUsage, traeWindowsAppNames, unwrapVolatile, unwrapVolatileDeep, withContextAdaptation, withTraeRegion };